| @@ -6,7 +6,7 @@ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; · 5 unmodified lines |
| 6 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; | 6 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; |
| 7 | import openapiTS, { astToString } from "openapi-typescript"; | 7 | import openapiTS, { astToString } from "openapi-typescript"; |
| 8 | import { Kind, parse, type FragmentDefinitionNode, type SelectionSetNode } from "graphql"; | 8 | import { Kind, parse, type FragmentDefinitionNode, type SelectionSetNode } from "graphql"; |
| 9 | import { compactArgTypeHint, compactReturnTypeHint } from "./type_hints"; | 9 | import { compactArgKeysHint, compactArgTypeHint, compactReturnTypeHint } from "./type_hints"; |
| 10 | import type { ToolApprovalMode, ToolCredentialSpec, ToolDefinition, ToolTypeMetadata } from "./types"; | 10 | import type { ToolApprovalMode, ToolCredentialSpec, ToolDefinition, ToolTypeMetadata } from "./types"; |
| 11 | import { asRecord } from "./utils"; | 11 | import { asRecord } from "./utils"; |
| 12 | | 12 | |
| @@ -572,6 +572,7 @@ async function loadMcpTools(config: McpToolSourceConfig): Promise<ToolDefinition · 559 unmodified lines |
| 572 | return tools.map((tool) => { | 572 | return tools.map((tool) => { |
| 573 | const toolName = String(tool.name ?? "tool"); | 573 | const toolName = String(tool.name ?? "tool"); |
| 574 | const inputSchema = asRecord(tool.inputSchema); | 574 | const inputSchema = asRecord(tool.inputSchema); |
| | 575 | const argPreviewKeys = Object.keys(asRecord(inputSchema.properties)).filter((key) => key.length > 0); |
| 575 | const argsType = jsonSchemaTypeHintFallback(inputSchema); | 576 | const argsType = jsonSchemaTypeHintFallback(inputSchema); |
| 576 | const returnsType = "unknown"; | 577 | const returnsType = "unknown"; |
| 577 | return { | 578 | return { |
| @@ -584,6 +585,7 @@ async function loadMcpTools(config: McpToolSourceConfig): Promise<ToolDefinition · 6 unmodified lines |
| 584 | returnsType, | 585 | returnsType, |
| 585 | displayArgsType: compactArgTypeHint(argsType), | 586 | displayArgsType: compactArgTypeHint(argsType), |
| 586 | displayReturnsType: compactReturnTypeHint(returnsType), | 587 | displayReturnsType: compactReturnTypeHint(returnsType), |
| | 588 | ...(argPreviewKeys.length > 0 ? { argPreviewKeys } : {}), |
| 587 | }, | 589 | }, |
| 588 | _runSpec: { | 590 | _runSpec: { |
| 589 | kind: "mcp" as const, | 591 | kind: "mcp" as const, |
| @@ -693,7 +695,6 @@ function compactOpenApiPaths( · 103 unmodified lines |
| 693 | componentSchemas?: Record<string, unknown>, | 695 | componentSchemas?: Record<string, unknown>, |
| 694 | componentResponses?: Record<string, unknown>, | 696 | componentResponses?: Record<string, unknown>, |
| 695 | componentRequestBodies?: Record<string, unknown>, | 697 | componentRequestBodies?: Record<string, unknown>, |
| 696 | typeHintMode: "full" | "fast" = "full", | | |
| 697 | ): Record<string, unknown> { | 698 | ): Record<string, unknown> { |
| 698 | const paths = asRecord(pathsValue); | 699 | const paths = asRecord(pathsValue); |
| 699 | const methods = ["get", "post", "put", "delete", "patch", "head", "options"] as const; | 700 | const methods = ["get", "post", "put", "delete", "patch", "head", "options"] as const; |
| @@ -778,9 +779,6 @@ function compactOpenApiPaths( · 78 unmodified lines |
| 778 | for (const [status, responseValue] of Object.entries(responses)) { | 779 | for (const [status, responseValue] of Object.entries(responses)) { |
| 779 | if (!status.startsWith("2")) continue; | 780 | if (!status.startsWith("2")) continue; |
| 780 | responseStatus = status; | 781 | responseStatus = status; |
| 781 | if (typeHintMode === "fast" && !hasGeneratedTypes) { | | |
| 782 | break; | | |
| 783 | } | | |
| 784 | const resolvedResponse = resolveResponseRef(asRecord(responseValue), compResponses); | 782 | const resolvedResponse = resolveResponseRef(asRecord(responseValue), compResponses); |
| 785 | responseSchema = resolveSchemaRef( | 783 | responseSchema = resolveSchemaRef( |
| 786 | getPreferredResponseSchema(resolvedResponse), | 784 | getPreferredResponseSchema(resolvedResponse), |
| @@ -816,11 +814,14 @@ function compactOpenApiPaths( · 29 unmodified lines |
| 816 | ? jsonSchemaTypeHintFallback(combinedSchema, 0, compSchemas) | 814 | ? jsonSchemaTypeHintFallback(combinedSchema, 0, compSchemas) |
| 817 | : "{}"; | 815 | : "{}"; |
| 818 | compactOperation._returnsTypeHint = responseTypeHintFromSchema(responseSchema, responseStatus, compSchemas); | 816 | compactOperation._returnsTypeHint = responseTypeHintFromSchema(responseSchema, responseStatus, compSchemas); |
| 819 | } else if (typeHintMode === "fast") { | 817 | compactOperation._usesGeneratedTypes = true; |
| 820 | const mergedParameters = operationParameters.concat(sharedParameters); | 818 | const previewKeys = [ |
| 821 | const hasInputSchema = mergedParameters.length > 0 || Object.keys(rawRequestBodySchema).length > 0; | 819 | ...mergedParameters.map((param) => String(param.name ?? "")).filter((name) => name.length > 0), |
| 822 | compactOperation._argsTypeHint = hasInputSchema ? "Record<string, unknown>" : "{}"; | 820 | ...Object.keys(asRecord(requestBodySchema.properties)), |
| 823 | compactOperation._returnsTypeHint = responseStatus === "204" ? "void" : "unknown"; | 821 | ]; |
| | 822 | if (previewKeys.length > 0) { |
| | 823 | compactOperation._argPreviewKeys = [...new Set(previewKeys)]; |
| | 824 | } |
| 824 | } else { | 825 | } else { |
| 825 | // Keep full schemas for the fallback path | 826 | // Keep full schemas for the fallback path |
| 826 | if (Object.keys(requestBodySchema).length > 0) { | 827 | if (Object.keys(requestBodySchema).length > 0) { |
| @@ -868,34 +869,9 @@ export interface PreparedOpenApiSpec { · 41 unmodified lines |
| 868 | warnings: string[]; | 869 | warnings: string[]; |
| 869 | } | 870 | } |
| 870 | | 871 | |
| 871 | export interface PrepareOpenApiSpecOptions { | | |
| 872 | mode?: "full" | "fast"; | | |
| 873 | fastPathOperationThreshold?: number; | | |
| 874 | } | | |
| 875 | | | |
| 876 | const DEFAULT_FAST_PATH_OPERATION_THRESHOLD = 600; | | |
| 877 | | | |
| 878 | function countOpenApiOperations(pathsValue: unknown): number { | | |
| 879 | const paths = asRecord(pathsValue); | | |
| 880 | const methods = new Set(["get", "post", "put", "delete", "patch", "head", "options"]); | | |
| 881 | let count = 0; | | |
| 882 | | | |
| 883 | for (const pathValue of Object.values(paths)) { | | |
| 884 | const pathObject = asRecord(pathValue); | | |
| 885 | for (const key of Object.keys(pathObject)) { | | |
| 886 | if (methods.has(key)) { | | |
| 887 | count += 1; | | |
| 888 | } | | |
| 889 | } | | |
| 890 | } | | |
| 891 | | | |
| 892 | return count; | | |
| 893 | } | | |
| 894 | | | |
| 895 | export async function prepareOpenApiSpec( | 872 | export async function prepareOpenApiSpec( |
| 896 | spec: string | Record<string, unknown>, | 873 | spec: string | Record<string, unknown>, |
| 897 | sourceName = "openapi", | 874 | sourceName = "openapi", |
| 898 | options?: PrepareOpenApiSpecOptions, | | |
| 899 | ): Promise<PreparedOpenApiSpec> { | 875 | ): Promise<PreparedOpenApiSpec> { |
| 900 | const parser = SwaggerParser as unknown as { | 876 | const parser = SwaggerParser as unknown as { |
| 901 | bundle(spec: unknown): Promise<unknown>; | 877 | bundle(spec: unknown): Promise<unknown>; |
| @@ -920,32 +896,19 @@ export async function prepareOpenApiSpec( · 18 unmodified lines |
| 920 | parsed = spec; | 896 | parsed = spec; |
| 921 | } | 897 | } |
| 922 | | 898 | |
| 923 | const operationCount = countOpenApiOperations(parsed.paths); | | |
| 924 | const mode = options?.mode ?? "full"; | | |
| 925 | const fastPathOperationThreshold = | | |
| 926 | options?.fastPathOperationThreshold ?? DEFAULT_FAST_PATH_OPERATION_THRESHOLD; | | |
| 927 | const useFastPath = mode === "fast" && operationCount >= fastPathOperationThreshold; | | |
| 928 | | | |
| 929 | // ── Step 2: Generate .d.ts and bundle in parallel ───────────────────── | 899 | // ── Step 2: Generate .d.ts and bundle in parallel ───────────────────── |
| 930 | // Both now operate on the in-memory object — no additional HTTP fetches. | 900 | // Both now operate on the in-memory object — no additional HTTP fetches. |
| 931 | let dts: string | null = null; | 901 | let dts: string | null; |
| 932 | let bundled: Record<string, unknown>; | 902 | let bundled: Record<string, unknown>; |
| 933 | if (useFastPath) { | 903 | const dtsPromise = generateOpenApiDts(parsed); |
| | 904 | try { |
| | 905 | bundled = (await parser.bundle(parsed)) as Record<string, unknown>; |
| | 906 | } catch (bundleError) { |
| | 907 | const bundleMessage = bundleError instanceof Error ? bundleError.message : String(bundleError); |
| | 908 | warnings.push(`OpenAPI bundle failed for '${sourceName}', using parse-only mode: ${bundleMessage}`); |
| 934 | bundled = parsed; | 909 | bundled = parsed; |
| 935 | warnings.push( | | |
| 936 | `OpenAPI source '${sourceName}' loaded in fast mode (${operationCount} operations): skipped bundle + d.ts generation for latency`, | | |
| 937 | ); | | |
| 938 | } else { | | |
| 939 | const dtsPromise = generateOpenApiDts(parsed); | | |
| 940 | try { | | |
| 941 | bundled = (await parser.bundle(parsed)) as Record<string, unknown>; | | |
| 942 | } catch (bundleError) { | | |
| 943 | const bundleMessage = bundleError instanceof Error ? bundleError.message : String(bundleError); | | |
| 944 | warnings.push(`OpenAPI bundle failed for '${sourceName}', using parse-only mode: ${bundleMessage}`); | | |
| 945 | bundled = parsed; | | |
| 946 | } | | |
| 947 | dts = await dtsPromise; | | |
| 948 | } | 910 | } |
| | 911 | dts = await dtsPromise; |
| 949 | | 912 | |
| 950 | // ── Step 3: Extract operation IDs for compaction ─────────────────────── | 913 | // ── Step 3: Extract operation IDs for compaction ─────────────────────── |
| 951 | // If we have a .d.ts, we know which operations have full generated types. | 914 | // If we have a .d.ts, we know which operations have full generated types. |
| @@ -966,7 +929,6 @@ export async function prepareOpenApiSpec( · 14 unmodified lines |
| 966 | asRecord(asRecord(bundled.components).schemas), | 929 | asRecord(asRecord(bundled.components).schemas), |
| 967 | asRecord(asRecord(bundled.components).responses), | 930 | asRecord(asRecord(bundled.components).responses), |
| 968 | asRecord(asRecord(bundled.components).requestBodies), | 931 | asRecord(asRecord(bundled.components).requestBodies), |
| 969 | useFastPath ? "fast" : "full", | | |
| 970 | ), | 932 | ), |
| 971 | dts: dts ?? undefined, | 933 | dts: dts ?? undefined, |
| 972 | warnings, | 934 | warnings, |
| @@ -1028,6 +990,9 @@ export function buildOpenApiToolsFromPrepared( · 55 unmodified lines |
| 1028 | // Pre-computed during compaction when .d.ts types exist (schemas stripped to save space). | 990 | // Pre-computed during compaction when .d.ts types exist (schemas stripped to save space). |
| 1029 | let argsType: string; | 991 | let argsType: string; |
| 1030 | let returnsType: string; | 992 | let returnsType: string; |
| | 993 | let argPreviewKeys: string[] = Array.isArray(operation._argPreviewKeys) |
| | 994 | ? operation._argPreviewKeys.filter((value): value is string => typeof value === "string") |
| | 995 | : []; |
| 1031 | if (typeof operation._argsTypeHint === "string" && typeof operation._returnsTypeHint === "string") { | 996 | if (typeof operation._argsTypeHint === "string" && typeof operation._returnsTypeHint === "string") { |
| 1032 | argsType = operation._argsTypeHint as string; | 997 | argsType = operation._argsTypeHint as string; |
| 1033 | returnsType = operation._returnsTypeHint as string; | 998 | returnsType = operation._returnsTypeHint as string; |
| @@ -1064,8 +1029,25 @@ export function buildOpenApiToolsFromPrepared( · 30 unmodified lines |
| 1064 | | 1029 | |
| 1065 | argsType = hasInputSchema ? jsonSchemaTypeHintFallback(combinedSchema) : "{}"; | 1030 | argsType = hasInputSchema ? jsonSchemaTypeHintFallback(combinedSchema) : "{}"; |
| 1066 | returnsType = responseTypeHintFromSchema(responseSchema, responseStatus); | 1031 | returnsType = responseTypeHintFromSchema(responseSchema, responseStatus); |
| | 1032 | if (argPreviewKeys.length === 0) { |
| | 1033 | argPreviewKeys = [ |
| | 1034 | ...parameters.map((param) => param.name), |
| | 1035 | ...Object.keys(asRecord(requestBodySchema.properties)), |
| | 1036 | ].filter((name, index, all) => name.length > 0 && all.indexOf(name) === index); |
| | 1037 | } |
| 1067 | } | 1038 | } |
| 1068 | | 1039 | |
| | 1040 | const usesGeneratedTypes = Boolean(operation._usesGeneratedTypes); |
| | 1041 | const operationIdKey = JSON.stringify(operationIdRaw); |
| | 1042 | const displayArgsType = argPreviewKeys.length > 0 |
| | 1043 | ? compactArgKeysHint(argPreviewKeys) |
| | 1044 | : usesGeneratedTypes |
| | 1045 | ? `ToolInput<operations[${operationIdKey}]>` |
| | 1046 | : compactArgTypeHint(argsType); |
| | 1047 | const displayReturnsType = usesGeneratedTypes |
| | 1048 | ? `ToolOutput<operations[${operationIdKey}]>` |
| | 1049 | : compactReturnTypeHint(returnsType); |
| | 1050 | |
| 1069 | const approval = config.overrides?.[operationIdRaw]?.approval | 1051 | const approval = config.overrides?.[operationIdRaw]?.approval |
| 1070 | ?? (readMethods.has(method) | 1052 | ?? (readMethods.has(method) |
| 1071 | ? config.defaultReadApproval ?? "auto" | 1053 | ? config.defaultReadApproval ?? "auto" |
| @@ -1088,8 +1070,9 @@ export function buildOpenApiToolsFromPrepared( · 16 unmodified lines |
| 1088 | metadata: { | 1070 | metadata: { |
| 1089 | argsType, | 1071 | argsType, |
| 1090 | returnsType, | 1072 | returnsType, |
| 1091 | displayArgsType: compactArgTypeHint(argsType), | 1073 | displayArgsType, |
| 1092 | displayReturnsType: compactReturnTypeHint(returnsType), | 1074 | displayReturnsType, |
| | 1075 | ...(argPreviewKeys.length > 0 ? { argPreviewKeys } : {}), |
| 1093 | operationId: operationIdRaw, | 1076 | operationId: operationIdRaw, |
| 1094 | // Only attach sourceDts to the first tool to avoid duplicating the full .d.ts | 1077 | // Only attach sourceDts to the first tool to avoid duplicating the full .d.ts |
| 1095 | ...(sourceDts && !sourceDtsEmitted ? { sourceDts } : {}), | 1078 | ...(sourceDts && !sourceDtsEmitted ? { sourceDts } : {}), |
| @@ -1617,6 +1600,7 @@ async function loadGraphqlTools(config: GraphqlToolSourceConfig): Promise<ToolDe · 521 unmodified lines |
| 1617 | returnsType: "{ data: unknown; errors: unknown[] }", | 1600 | returnsType: "{ data: unknown; errors: unknown[] }", |
| 1618 | displayArgsType: "{ query: string; variables?: ... }", | 1601 | displayArgsType: "{ query: string; variables?: ... }", |
| 1619 | displayReturnsType: "{ data: ...; errors: unknown[] }", | 1602 | displayReturnsType: "{ data: ...; errors: unknown[] }", |
| | 1603 | argPreviewKeys: ["query", "variables"], |
| 1620 | }, | 1604 | }, |
| 1621 | credential: credentialSpec, | 1605 | credential: credentialSpec, |
| 1622 | // Tag as graphql source so invokeTool knows to do dynamic path extraction | 1606 | // Tag as graphql source so invokeTool knows to do dynamic path extraction |
| @@ -1681,6 +1665,7 @@ async function loadGraphqlTools(config: GraphqlToolSourceConfig): Promise<ToolDe · 58 unmodified lines |
| 1681 | const approval = config.overrides?.[field.name]?.approval ?? defaultApproval; | 1665 | const approval = config.overrides?.[field.name]?.approval ?? defaultApproval; |
| 1682 | const argsType = gqlFieldArgsTypeHint(field.args, typeMap); | 1666 | const argsType = gqlFieldArgsTypeHint(field.args, typeMap); |
| 1683 | const returnsType = `{ data: ${gqlTypeToHint(field.type, typeMap)}; errors: unknown[] }`; | 1667 | const returnsType = `{ data: ${gqlTypeToHint(field.type, typeMap)}; errors: unknown[] }`; |
| | 1668 | const argPreviewKeys = field.args.map((arg) => arg.name).filter((name) => name.length > 0); |
| 1684 | | 1669 | |
| 1685 | // Build the example query for the description | 1670 | // Build the example query for the description |
| 1686 | const exampleQuery = buildFieldQuery(operationType, field.name, field.args, field.type, typeMap); | 1671 | const exampleQuery = buildFieldQuery(operationType, field.name, field.args, field.type, typeMap); |
| @@ -1701,6 +1686,7 @@ async function loadGraphqlTools(config: GraphqlToolSourceConfig): Promise<ToolDe · 14 unmodified lines |
| 1701 | returnsType, | 1686 | returnsType, |
| 1702 | displayArgsType: compactArgTypeHint(argsType), | 1687 | displayArgsType: compactArgTypeHint(argsType), |
| 1703 | displayReturnsType: "{ data: ...; errors: unknown[] }", | 1688 | displayReturnsType: "{ data: ...; errors: unknown[] }", |
| | 1689 | ...(argPreviewKeys.length > 0 ? { argPreviewKeys } : {}), |
| 1704 | }, | 1690 | }, |
| 1705 | _runSpec: { | 1691 | _runSpec: { |
| 1706 | kind: "graphql_field" as const, | 1692 | kind: "graphql_field" as const, |