import { type Tool } from "@modelcontextprotocol/server"; import type { ActionEntry } from "../agent/production-agent.js"; import { type MCPConfig } from "./build-server.js"; /** * Schema rules a model provider, or the host adapter in front of it, applies * to the tool list an MCP host forwards. A host sends every advertised tool on * every turn, so one schema a provider rejects fails the whole session, and a * schema a host adapter narrows makes valid calls look invalid. * * Each rule records its basis: * - `documented`: the provider's or protocol's published reference says so. * - `observed`: a validator returned the quoted error; the date and path are * in the source line. Observed behavior can change without notice. * - `host`: a host adapter rewrites the schema before the provider sees it, * and the rewrite would narrow it. * * Which field each provider receives: * - Anthropic: Messages `tools[].input_schema`, as Claude Code sends it. * - OpenAI: Responses `tools[]` with `type: "function"` and `strict: false`, * as Codex sends MCP tools (`tool_definition_to_responses_api_tool` in * https://github.com/openai/codex/blob/main/codex-rs/tools/src/responses_api.rs). * - Gemini: `functionDeclarations[].parametersJsonSchema`, which Gemini CLI * fills with the MCP `inputSchema` (`parametersJsonSchema: * this.toolDef.inputSchema` in * https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/tools/mcp-client.ts), * not the OpenAPI-subset `parameters` field. The OpenRouter smoke reaches * Gemini through `parameters`, so its results are a diagnostic, not this * field's contract. * * Gemini documents that in `any` (forced) mode "the API may reject very large * or deeply nested schemas" * (https://ai.google.dev/gemini-api/docs/function-calling, "Notes and * limitations") without a bound. The one cause observed in the template * catalogs, `maxItems`, is a rule below; any other the smoke script finds is * recorded as its own outcome. */ export declare const SCHEMA_PROVIDERS: readonly ["anthropic", "openai", "gemini"]; export type SchemaProvider = (typeof SCHEMA_PROVIDERS)[number]; export type ProviderRuleBasis = "documented" | "observed" | "host"; export interface ProviderSchemaRule { id: string; providers: readonly SchemaProvider[]; basis: ProviderRuleBasis; source: string; message: string; } export declare const PROVIDER_SCHEMA_RULES: { readonly anthropicToolName: { readonly id: "anthropic-tool-name"; readonly providers: readonly ["anthropic"]; readonly basis: "documented"; readonly source: "https://platform.claude.com/docs/en/api/messages/create — tools[].name matches ^[a-zA-Z0-9_-]{1,128}$"; readonly message: "tool name has characters or a length Anthropic rejects"; }; readonly openaiToolName: { readonly id: "openai-tool-name"; readonly providers: readonly ["openai"]; readonly basis: "documented"; readonly source: "https://platform.openai.com/docs/api-reference/responses/create — function names are a-z, A-Z, 0-9, underscores and dashes, at most 64 characters"; readonly message: "tool name has characters or a length OpenAI rejects"; }; readonly geminiToolName: { readonly id: "gemini-tool-name"; readonly providers: readonly ["gemini"]; readonly basis: "documented"; readonly source: "https://ai.google.dev/api/generate-content#FunctionDeclaration — a-z, A-Z, 0-9, underscores, colons, dots and dashes, at most 128 characters; the Vertex reference adds that it starts with a letter or underscore"; readonly message: "tool name has characters or a length Gemini rejects"; }; readonly rootObject: { readonly id: "root-object"; readonly providers: readonly ["anthropic", "openai", "gemini"]; readonly basis: "documented"; readonly source: 'https://modelcontextprotocol.io/specification/2025-11-25/schema#tool — inputSchema has type "object"; https://ai.google.dev/api/generate-content#FunctionDeclaration — parametersJsonSchema "must describe an object where the properties are the parameters"'; readonly message: 'root type is not "object"'; }; readonly draft2020: { readonly id: "json-schema-2020-12"; readonly providers: readonly ["anthropic", "openai", "gemini"]; readonly basis: "documented"; readonly source: "https://modelcontextprotocol.io/specification/2025-11-25/server/tools — a schema without $schema is JSON Schema 2020-12"; readonly message: "not a valid JSON Schema 2020-12 document"; }; readonly rootComposition: { readonly id: "root-composition"; readonly providers: readonly ["anthropic"]; readonly basis: "observed"; readonly source: 'Anthropic Messages via OpenRouter, 2026-10-07: "input_schema does not support oneOf, allOf, or anyOf at the top level". OpenAI (strict: false) and Gemini accepted the same schema in that run.'; readonly message: "root uses anyOf, oneOf or allOf"; }; readonly gatewayOneOf: { readonly id: "gateway-one-of"; readonly providers: readonly ["openai"]; readonly basis: "observed"; readonly source: "Builder gateway OpenAI path, recorded at stripUnsupportedSchemaKeywords in action.ts: \"'oneOf' is not permitted\". OpenAI with strict: false accepted it through OpenRouter on 2026-10-07; kept as a floor for gateway-backed hosts."; readonly message: "uses oneOf"; }; readonly gatewayPropertyNames: { readonly id: "gateway-property-names"; readonly providers: readonly ["openai"]; readonly basis: "observed"; readonly source: "Builder gateway OpenAI path, recorded at stripUnsupportedSchemaKeywords in action.ts: 400 invalid_function_parameters. OpenAI with strict: false accepted it through OpenRouter on 2026-10-07; kept as a floor for gateway-backed hosts."; readonly message: "uses propertyNames"; }; readonly gatewayTypeless: { readonly id: "gateway-typeless"; readonly providers: readonly ["openai"]; readonly basis: "observed"; readonly source: "Builder gateway OpenAI path, recorded at stripUnsupportedSchemaKeywords in action.ts: \"schema must have a 'type' key\". OpenAI with strict: false accepted it through OpenRouter on 2026-10-07; kept as a floor for gateway-backed hosts."; readonly message: "schema position has no type"; }; readonly codexArrayItems: { readonly id: "codex-array-items"; readonly providers: readonly ["openai"]; readonly basis: "host"; readonly source: 'Codex fills a missing items with { "type": "string" } (ensure_default_children_for_schema_types in https://github.com/openai/codex/blob/main/codex-rs/tools/src/json_schema.rs), which narrows the array'; readonly message: "array has no items schema"; }; readonly geminiArrayItems: { readonly id: "gemini-array-items"; readonly providers: readonly ["gemini"]; readonly basis: "observed"; readonly source: 'Gemini through OpenRouter\'s OpenAPI parameters path, 2026-10-07: "parameters.properties[list].items: missing field". Not yet observed on parametersJsonSchema, the field Gemini CLI fills.'; readonly message: "array has no items schema"; }; readonly codexEnumType: { readonly id: "codex-enum-type"; readonly providers: readonly ["openai"]; readonly basis: "host"; readonly source: 'Codex lowers const to a one-value enum and types a typeless enum as "string" (sanitize_json_schema in https://github.com/openai/codex/blob/main/codex-rs/tools/src/json_schema.rs), which rejects every non-string value'; readonly message: "enum or const has no explicit type"; }; readonly booleanSubschema: { readonly id: "boolean-subschema"; readonly providers: readonly ["openai"]; readonly basis: "host"; readonly source: 'Codex rewrites a boolean schema to { "type": "string" } (sanitize_json_schema in https://github.com/openai/codex/blob/main/codex-rs/tools/src/json_schema.rs); only additionalProperties keeps its boolean'; readonly message: "boolean subschema outside additionalProperties"; }; readonly geminiForcedMaxItems: { readonly id: "gemini-forced-max-items"; readonly providers: readonly ["gemini"]; readonly basis: "observed"; readonly source: "Gemini through OpenRouter's OpenAPI parameters path, 2026-10-17: a forced call (tool_choice naming the function) returned 400 INVALID_ARGUMENT for 1,000 string items and for 100 of Content's add-database-item property entries, and succeeded once maxItems was removed. The bound depends on the item schema, so no smaller maxItems is safe. Not yet observed on parametersJsonSchema, the field Gemini CLI fills."; readonly message: "array declares maxItems, which Gemini's forced mode may refuse"; }; readonly geminiStringConst: { readonly id: "gemini-string-const"; readonly providers: readonly ["gemini"]; readonly basis: "documented"; readonly source: 'Gemini\'s Schema has enum but no const field (https://ai.google.dev/api/generate-content#v1beta.Schema), and its JSON Schema support list names enum, not const (https://ai.google.dev/gemini-api/docs/structured-output). Through OpenRouter, 2026-10-17, forced add-database-item calls answered a const "text" discriminator with "STRING", "STATUS" or "string" in 3 of 4 calls, and with "text" in 4 of 4 once it was a one-value enum.'; readonly message: "string const, which Gemini ignores; a one-value enum says the same"; }; readonly geminiStringEnum: { readonly id: "gemini-string-enum"; readonly providers: readonly ["gemini"]; readonly basis: "documented"; readonly source: "Gemini's Schema.enum is a list of strings, the possible values of a STRING (https://ai.google.dev/api/generate-content#v1beta.Schema)"; readonly message: "enum lists a value that is not a string, which Gemini's enum cannot hold"; }; readonly geminiTypes: { readonly id: "gemini-json-types"; readonly providers: readonly ["gemini"]; readonly basis: "documented"; readonly source: "https://ai.google.dev/gemini-api/docs/structured-output — the supported JSON Schema types are string, number, integer, boolean, object, array and null"; readonly message: "type is not one of Gemini's supported JSON Schema types"; }; }; export interface ProviderSchemaViolation { provider: SchemaProvider; tool: string; rule: string; message: string; path: string; } /** Every pinned provider rule the tool breaks; empty when all accept it. */ export declare function providerSchemaViolations(tool: { name: string; inputSchema: unknown; }): ProviderSchemaViolation[]; export declare const MCP_CATALOG_MODES: readonly ["default", "full", "app", "oauth-read", "directory"]; export type McpCatalogMode = (typeof MCP_CATALOG_MODES)[number]; export interface McpDirectoryProfileFixture { connectorCatalog: string[]; widgetDomain?: string; [key: string]: unknown; } /** * Where a template keeps its ChatGPT directory profile. The conformance * helpers find it here, so a template that adds one gets its directory * catalog checked without editing its spec. */ export declare const DIRECTORY_PROFILE_MODULE = "server/lib/chatgpt-directory-tools.ts"; /** The catalog modes a template serves: directory only with a profile. */ export declare function templateMcpCatalogModes(projectRoot: string): McpCatalogMode[]; /** * Lists the tools one catalog mode advertises, through the same `tools/list` * handler a host calls. The modes mirror how a request selects its catalog: * the compact default, `--full-catalog` on the full action surface, * `catalogMode: "app"`, a full-catalog OAuth token limited to `mcp:read`, and * the reviewed directory profile. */ export declare function listMcpCatalogTools(base: MCPConfig, mode: McpCatalogMode, directoryProfile?: McpDirectoryProfileFixture, directoryActions?: Record): Promise; type ImportModule = (file: string) => Promise; /** * The action surface a template serves to external agents: its own actions * plus the framework's sharing and review kits, filtered the way the agent * chat plugin filters them. Discovery predicates are dropped so every tool * that some caller could be shown is checked. * * `importModule` loads the generated registry; callers outside the template's * own Vitest run pass a loader that applies the template's path aliases. */ export declare function loadTemplateMcpActions(projectRoot: string, importModule?: ImportModule): Promise>; /** The template's directory profile, or undefined when it has none. */ export declare function loadTemplateDirectoryProfile(projectRoot: string, importModule?: ImportModule): Promise; export interface TemplateMcpConformance { appId: string; actions: Record; modes: McpCatalogMode[]; listCatalog(mode: McpCatalogMode): Promise; } /** * Everything a template's provider-schema spec needs: its MCP config and * directory profile, loaded once, and a lister for each catalog it serves. */ export declare function loadTemplateMcpConformance(projectRoot: string, importModule?: ImportModule): Promise; export declare function withoutDiscoveryPredicates(actions: Record): Record; export declare function conformanceMcpConfig(appId: string, actions: Record): MCPConfig; export {};