Files
shockbot/mcp/shared.ts
T
Colin McDonnell c608051b79 sanitize mcp schemas for Gemini; fix gpt-codex-mini alias; add matrix filter
Gemini's generateContent API rejects arktype's `{anyOf:[{enum:[...]}]}` string-enum
encoding, `$schema` metadata, and `anyOf` with sibling fields. Port the old
sanitizer back as an isolated module (action/mcp/geminiSanitizer.ts) and gate it
on `isGeminiRouted(ctx)` so non-gemini routes see the original schema. Wires
`resolvedModel` onto ToolContext so the sanitizer can see the upstream specifier.

Also bumps `openai/gpt-codex-mini` alias from the deprecated `codex-mini-latest`
to `gpt-5.1-codex-mini`, matching the openrouter resolve.

Adds a `filter` workflow_dispatch input + MATRIX_FILTER env that restricts the
models-live matrix to aliases matching a substring, so we can iterate on a
single provider (e.g. `filter=gemini`) without paying to run every model.
2026-04-16 23:09:32 +00:00

72 lines
2.2 KiB
TypeScript

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 { isGeminiRouted, sanitizeToolForGemini } from "./geminiSanitizer.ts";
import type { ToolContext } from "./server.ts";
export const tool = <const params>(
toolDef: Tool<any, StandardSchemaV1<params>>
): Tool<any, StandardSchemaV1<params>> => 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 }],
};
};
export const handleToolError = (error: unknown): ToolResult => {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `Error: ${errorMessage}`,
},
],
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 errorMessage = error instanceof Error ? error.message : String(error);
const prefix = toolName ? `[${toolName}]` : "tool";
log.info(`${prefix} error: ${errorMessage}`);
log.debug(`${prefix} params: ${formatJsonValue(params)}`);
return handleToolError(error);
}
};
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;
};