| @@ -6,7 +6,7 @@ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; · 5 unmodified lines |
| 6 | 6 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; |
| 7 | 7 | import openapiTS, { astToString } from "openapi-typescript"; |
| 8 | 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 | 10 | import type { ToolApprovalMode, ToolCredentialSpec, ToolDefinition, ToolTypeMetadata } from "./types"; |
| 11 | 11 | import { asRecord } from "./utils"; |
| 12 | 12 | |
| @@ -572,6 +572,7 @@ async function loadMcpTools(config: McpToolSourceConfig): Promise<ToolDefinition · 559 unmodified lines |
| 572 | 572 | return tools.map((tool) => { |
| 573 | 573 | const toolName = String(tool.name ?? "tool"); |
| 574 | 574 | const inputSchema = asRecord(tool.inputSchema); |
| 575 | + const argPreviewKeys = Object.keys(asRecord(inputSchema.properties)).filter((key) => key.length > 0); |
| 575 | 576 | const argsType = jsonSchemaTypeHintFallback(inputSchema); |
| 576 | 577 | const returnsType = "unknown"; |
| 577 | 578 | return { |
| @@ -584,6 +585,7 @@ async function loadMcpTools(config: McpToolSourceConfig): Promise<ToolDefinition · 6 unmodified lines |
| 584 | 585 | returnsType, |
| 585 | 586 | displayArgsType: compactArgTypeHint(argsType), |
| 586 | 587 | displayReturnsType: compactReturnTypeHint(returnsType), |
| 588 | + ...(argPreviewKeys.length > 0 ? { argPreviewKeys } : {}), |
| 587 | 589 | }, |
| 588 | 590 | _runSpec: { |
| 589 | 591 | kind: "mcp" as const, |
| @@ -693,7 +695,6 @@ function compactOpenApiPaths( · 103 unmodified lines |
| 693 | 695 | componentSchemas?: Record<string, unknown>, |
| 694 | 696 | componentResponses?: Record<string, unknown>, |
| 695 | 697 | componentRequestBodies?: Record<string, unknown>, |
| 696 | | − typeHintMode: "full" | "fast" = "full", |
| 697 | 698 | ): Record<string, unknown> { |
| 698 | 699 | const paths = asRecord(pathsValue); |
| 699 | 700 | const methods = ["get", "post", "put", "delete", "patch", "head", "options"] as const; |
| @@ -778,9 +779,6 @@ function compactOpenApiPaths( · 78 unmodified lines |
| 778 | 779 | for (const [status, responseValue] of Object.entries(responses)) { |
| 779 | 780 | if (!status.startsWith("2")) continue; |
| 780 | 781 | responseStatus = status; |
| 781 | | − if (typeHintMode === "fast" && !hasGeneratedTypes) { |
| 782 | | − break; |
| 783 | | − } |
| 784 | 782 | const resolvedResponse = resolveResponseRef(asRecord(responseValue), compResponses); |
| 785 | 783 | responseSchema = resolveSchemaRef( |
| 786 | 784 | getPreferredResponseSchema(resolvedResponse), |
| @@ -816,11 +814,14 @@ function compactOpenApiPaths( · 29 unmodified lines |
| 816 | 814 | ? jsonSchemaTypeHintFallback(combinedSchema, 0, compSchemas) |
| 817 | 815 | : "{}"; |
| 818 | 816 | compactOperation._returnsTypeHint = responseTypeHintFromSchema(responseSchema, responseStatus, compSchemas); |
| 819 | | − } else if (typeHintMode === "fast") { |
| 820 | | − const mergedParameters = operationParameters.concat(sharedParameters); |
| 821 | | − const hasInputSchema = mergedParameters.length > 0 || Object.keys(rawRequestBodySchema).length > 0; |
| 822 | | − compactOperation._argsTypeHint = hasInputSchema ? "Record<string, unknown>" : "{}"; |
| 823 | | − compactOperation._returnsTypeHint = responseStatus === "204" ? "void" : "unknown"; |
| 817 | + compactOperation._usesGeneratedTypes = true; |
| 818 | + const previewKeys = [ |
| 819 | + ...mergedParameters.map((param) => String(param.name ?? "")).filter((name) => name.length > 0), |
| 820 | + ...Object.keys(asRecord(requestBodySchema.properties)), |
| 821 | + ]; |
| 822 | + if (previewKeys.length > 0) { |
| 823 | + compactOperation._argPreviewKeys = [...new Set(previewKeys)]; |
| 824 | + } |
| 824 | 825 | } else { |
| 825 | 826 | // Keep full schemas for the fallback path |
| 826 | 827 | if (Object.keys(requestBodySchema).length > 0) { |
| @@ -868,34 +869,9 @@ export interface PreparedOpenApiSpec { · 41 unmodified lines |
| 868 | 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 | 872 | export async function prepareOpenApiSpec( |
| 896 | 873 | spec: string | Record<string, unknown>, |
| 897 | 874 | sourceName = "openapi", |
| 898 | | − options?: PrepareOpenApiSpecOptions, |
| 899 | 875 | ): Promise<PreparedOpenApiSpec> { |
| 900 | 876 | const parser = SwaggerParser as unknown as { |
| 901 | 877 | bundle(spec: unknown): Promise<unknown>; |
| @@ -920,32 +896,19 @@ export async function prepareOpenApiSpec( · 18 unmodified lines |
| 920 | 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 | 899 | // ── Step 2: Generate .d.ts and bundle in parallel ───────────────────── |
| 930 | 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 | 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 | 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 | 913 | // ── Step 3: Extract operation IDs for compaction ─────────────────────── |
| 951 | 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 | 929 | asRecord(asRecord(bundled.components).schemas), |
| 967 | 930 | asRecord(asRecord(bundled.components).responses), |
| 968 | 931 | asRecord(asRecord(bundled.components).requestBodies), |
| 969 | | − useFastPath ? "fast" : "full", |
| 970 | 932 | ), |
| 971 | 933 | dts: dts ?? undefined, |
| 972 | 934 | warnings, |
| @@ -1028,6 +990,9 @@ export function buildOpenApiToolsFromPrepared( · 55 unmodified lines |
| 1028 | 990 | // Pre-computed during compaction when .d.ts types exist (schemas stripped to save space). |
| 1029 | 991 | let argsType: string; |
| 1030 | 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 | 996 | if (typeof operation._argsTypeHint === "string" && typeof operation._returnsTypeHint === "string") { |
| 1032 | 997 | argsType = operation._argsTypeHint as string; |
| 1033 | 998 | returnsType = operation._returnsTypeHint as string; |
| @@ -1064,8 +1029,25 @@ export function buildOpenApiToolsFromPrepared( · 30 unmodified lines |
| 1064 | 1029 | |
| 1065 | 1030 | argsType = hasInputSchema ? jsonSchemaTypeHintFallback(combinedSchema) : "{}"; |
| 1066 | 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 | 1051 | const approval = config.overrides?.[operationIdRaw]?.approval |
| 1070 | 1052 | ?? (readMethods.has(method) |
| 1071 | 1053 | ? config.defaultReadApproval ?? "auto" |
| @@ -1088,8 +1070,9 @@ export function buildOpenApiToolsFromPrepared( · 16 unmodified lines |
| 1088 | 1070 | metadata: { |
| 1089 | 1071 | argsType, |
| 1090 | 1072 | returnsType, |
| 1091 | | − displayArgsType: compactArgTypeHint(argsType), |
| 1092 | | − displayReturnsType: compactReturnTypeHint(returnsType), |
| 1073 | + displayArgsType, |
| 1074 | + displayReturnsType, |
| 1075 | + ...(argPreviewKeys.length > 0 ? { argPreviewKeys } : {}), |
| 1093 | 1076 | operationId: operationIdRaw, |
| 1094 | 1077 | // Only attach sourceDts to the first tool to avoid duplicating the full .d.ts |
| 1095 | 1078 | ...(sourceDts && !sourceDtsEmitted ? { sourceDts } : {}), |
| @@ -1617,6 +1600,7 @@ async function loadGraphqlTools(config: GraphqlToolSourceConfig): Promise<ToolDe · 521 unmodified lines |
| 1617 | 1600 | returnsType: "{ data: unknown; errors: unknown[] }", |
| 1618 | 1601 | displayArgsType: "{ query: string; variables?: ... }", |
| 1619 | 1602 | displayReturnsType: "{ data: ...; errors: unknown[] }", |
| 1603 | + argPreviewKeys: ["query", "variables"], |
| 1620 | 1604 | }, |
| 1621 | 1605 | credential: credentialSpec, |
| 1622 | 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 | 1665 | const approval = config.overrides?.[field.name]?.approval ?? defaultApproval; |
| 1682 | 1666 | const argsType = gqlFieldArgsTypeHint(field.args, typeMap); |
| 1683 | 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 | 1670 | // Build the example query for the description |
| 1686 | 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 | 1686 | returnsType, |
| 1702 | 1687 | displayArgsType: compactArgTypeHint(argsType), |
| 1703 | 1688 | displayReturnsType: "{ data: ...; errors: unknown[] }", |
| 1689 | + ...(argPreviewKeys.length > 0 ? { argPreviewKeys } : {}), |
| 1704 | 1690 | }, |
| 1705 | 1691 | _runSpec: { |
| 1706 | 1692 | kind: "graphql_field" as const, |