UNPKG

run402-mcp

Version:

MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.

95 lines • 4.2 kB
/** * Tool arguments are strict: an argument a tool does not declare is refused, * never silently dropped. * * Every tool is registered with its strict schema (`strictInput`), so the * published JSON Schema says `additionalProperties: false` and the MCP SDK * itself refuses an unknown key. The SDK reports its refusal as plain text, * though, and every run402-mcp error carries `structuredContent`. So the * guard below validates a `tools/call` against the same strict schema before * the SDK sees it, and answers a failure itself with the structured error: * `UNKNOWN_ARGUMENT` (with the closest declared names) or `INVALID_ARGUMENTS`. * A valid call passes through untouched. */ import { z } from "zod"; import { closestMembers } from "./sandbox-proxy.js"; /** A tool's input shape as a strict object: unknown keys fail. */ export function strictInput(shape) { return z.object(shape).strict(); } /** The structured refusal for arguments that fail a tool's strict schema, or null when they pass. */ export function argumentError(tool, schema, args) { const parsed = schema.safeParse(args ?? {}); if (parsed.success) return null; const declared = Object.keys(schema.shape); const unknown = []; const issues = []; for (const issue of parsed.error.issues) { if (issue.code === "unrecognized_keys") unknown.push(...issue.keys); else issues.push({ path: issue.path.join(".") || "(arguments)", message: issue.message }); } if (unknown.length > 0) { const didYouMean = {}; for (const key of unknown) didYouMean[key] = closestMembers(key, declared); const hints = unknown .map((key) => (didYouMean[key].length > 0 ? `${key} (did you mean ${didYouMean[key].join(" or ")}?)` : key)) .join(", "); return { code: "UNKNOWN_ARGUMENT", message: `${tool} does not take ${unknown.length === 1 ? "the argument" : "the arguments"} ${hints}. It takes: ${declared.join(", ") || "no arguments"}.`, next_actions: [{ type: "edit_request", why: "Remove or rename the arguments the tool does not declare, then call it again.", tool, unknown, did_you_mean: didYouMean, accepted: declared, }], details: { unknown, ...(issues.length > 0 ? { issues } : {}) }, }; } return { code: "INVALID_ARGUMENTS", message: `${tool}: ${issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`, next_actions: [{ type: "edit_request", why: "Fix the arguments named in details.issues, then call the tool again.", tool }], details: { issues }, }; } function isToolCall(message) { const m = message; return m.method === "tools/call" && (typeof m.id === "string" || typeof m.id === "number") && typeof m.params?.name === "string"; } /** * Refuse invalid tool arguments with a structured result before the server * dispatches the call. Install after `server.connect(transport)`, which sets * the transport's `onmessage`. */ export function guardToolArguments(transport, schemas) { const dispatch = transport.onmessage; if (!dispatch) throw new Error("guardToolArguments: install after server.connect(transport)"); transport.onmessage = (message, extra) => { if (isToolCall(message)) { const schema = schemas[message.params.name]; const error = schema ? argumentError(message.params.name, schema, message.params.arguments) : null; if (error) { void transport.send({ jsonrpc: "2.0", id: message.id, result: { content: [{ type: "text", text: `Error: ${error.message}` }], structuredContent: { status: "error", error }, isError: true, }, }); return; } } dispatch.call(transport, message, extra); }; } //# sourceMappingURL=input-guard.js.map