Ctrl K
ComunidadGuías…

mcp/shared.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import type { StandardSchemaV1 } from "@standard-schema/spec";
import { encode as toonEncode } from "@toon-format/toon";
import type { FastMCP, Tool } from "fastmcp";
import { formatJsonValue, log } from "../utils/cli.ts";
import { MAX_ERROR_DETAIL_CHARS } from "../utils/shell.ts";
import { isGeminiRouted, sanitizeToolForGemini } from "./geminiSanitizer.ts";
import type { ToolContext } from "./server.ts";

/** extract the HTTP status from an unknown thrown value (octokit RequestError etc.), or undefined. */
export function getHttpStatus(err: unknown): number | undefined {
  if (typeof err !== "object" || err === null || !("status" in err)) return undefined;
  return typeof err.status === "number" ? err.status : undefined;
}

/**
 * a Pullfrog MCP tool definition. `mutates` marks a named state-changing tool
 * that must be reserved for the orchestrator and denied to subagents — it is
 * the single source of truth the subagent deny list derives from (see
 * action/agents/subagentToolGates.ts). general-purpose execution tools (`git`,
 * `shell`) are deliberately left unmarked: they can mutate but are allowed for
 * subagents and gated by command-arg validation instead. `readOnlyHint` is
 * independent scheduler metadata and must only follow an implementation audit;
 * the absence of `mutates` does not imply that a tool is read-only. a tool with
 * no parameters omits `parameters`: arktype's `type({})` emits no `properties`,
 * which OpenCode V2 forwards as is and a strict endpoint refuses (wiki/opencode-v2.md).
 */
export type PullfrogTool = Tool<any, any> & { mutates?: boolean };

export const tool = <const params>(
  toolDef: Tool<any, StandardSchemaV1<params>> & { mutates?: boolean }
): Tool<any, StandardSchemaV1<params>> & { mutates?: boolean } => toolDef;

export interface ToolResult {
  content: {
    type: "text";
    text: string;
  }[];
  isError?: boolean;
}

export const handleToolSuccess = (data: Record<string, any> | string): ToolResult => {
  const text = typeof data === "string" ? data : toonEncode(data);
  return {
    content: [{ type: "text", text }],
  };
};

/**
 * the one error text the log and the model both see. octokit's message for a non-JSON
 * response is the raw body, so a GitHub 5xx arrives as a 55 KB HTML page (#1582) or as
 * nothing at all (#1487); either way the status is the only useful fact.
 */
function formatToolError(error: unknown): string {
  const message = error instanceof Error ? error.message : String(error);
  const status = getHttpStatus(error);
  if (status !== undefined && (message.trim() === "" || message.trimStart().startsWith("<"))) {
    const transient = status >= 500 ? "; this is GitHub-side, retry shortly" : "";
    return `GitHub returned HTTP ${status} with no usable message${transient}`;
  }
  if (message.length <= MAX_ERROR_DETAIL_CHARS) return message;
  return `${message.slice(0, MAX_ERROR_DETAIL_CHARS)}…[truncated]`;
}

export const handleToolError = (message: string): ToolResult => {
  return {
    content: [
      {
        type: "text",
        text: `Error: ${message}`,
      },
    ],
    isError: true,
  };
};

/**
 * Helper to wrap a tool execute function with error handling.
 * Captures ctx in closure so tools don't need to handle try/catch.
 * @param fn - the function to execute
 * @param toolName - optional tool name for error logging
 */
export const execute = <T, R extends Record<string, any> | string>(
  fn: (params: T) => Promise<R>,
  toolName?: string
) => {
  const _fn = async (params: T): Promise<ToolResult> => {
    try {
      const result = await fn(params);
      return handleToolSuccess(result);
    } catch (error) {
      const message = formatToolError(error);
      const prefix = toolName ? `[${toolName}]` : "tool";
      log.info(`${prefix} error: ${message}`);
      log.debug(`${prefix} params: ${formatJsonValue(params ?? {})}`);
      return handleToolError(message);
    }
  };
  return _fn;
};

export const addTools = (ctx: ToolContext, server: FastMCP<any>, tools: Tool<any, any>[]) => {
  const shouldSanitize = isGeminiRouted(ctx);
  for (const tool of tools) {
    server.addTool(shouldSanitize ? sanitizeToolForGemini(tool) : tool);
  }
  return server;
};