History

refactor: make tool contracts first-class

Rhys Sullivan committed 8 months ago

Commit ff76c831 parent 31b43b323 files · +1253 −322Browse files

Unified · Split

23 files
  1. apps/web/src/views/source-detail.tsx +104 −3
  2. packages/kernel/core/src/discovery.ts +37 −12
  3. packages/kernel/core/src/index.test.ts +37 −13
  4. packages/kernel/core/src/tool-map.ts +17 −14
  5. packages/kernel/core/src/types.ts +18 −15
  6. packages/platform/control-plane/src/runtime/catalog/catalog-typescript.ts +165 −22
  7. packages/platform/control-plane/src/runtime/catalog/prettier-format.ts +2 −2
  8. packages/platform/control-plane/src/runtime/catalog/source/runtime.test.ts +16 −16
  9. packages/platform/control-plane/src/runtime/catalog/source/runtime.ts +231 −4
  10. packages/platform/control-plane/src/runtime/sources/executor-tools.ts +13 −4
  11. packages/platform/control-plane/src/runtime/sources/source-adapter-fixture-matrix.test.ts +19 −19
  12. packages/platform/control-plane/src/runtime/sources/source-inspection.test.ts +218 −20
  13. packages/platform/control-plane/src/runtime/sources/source-inspection.ts +94 −81
  14. packages/platform/control-plane/src/schema/models/source-inspection.ts +25 −0
  15. packages/platform/control-plane/src/schema/schema.test.ts +21 −0
  16. packages/sources/google-discovery/src/tools.ts +10 −6
  17. packages/sources/graphql/src/graphql-tools.ts +34 −30
  18. packages/sources/mcp/src/adapter.ts +165 −38
  19. packages/sources/mcp/src/tools.test.ts +1 −1
  20. packages/sources/mcp/src/tools.ts +8 −6
  21. packages/sources/openapi/src/extraction.real-specs.test.ts +1 −1
  22. packages/sources/openapi/src/tools.test.ts +1 −1
  23. packages/sources/openapi/src/tools.ts +16 −14
apps/web/src/views/source-detail.tsx+104−3
@@ -81,9 +81,9 @@ const readBindingStringArray = (source: Source, key: string): Array<string> | un · 80 unmodified lines
8181 : undefined;
8282 };
8383
84−const readBindingTransport = (source: Source): "auto" | "streamable-http" | "sse" | undefined => {
84+const readBindingTransport = (source: Source): "auto" | "streamable-http" | "sse" | "stdio" | undefined => {
8585 const candidate = source.binding.transport;
86− return candidate === "auto" || candidate === "streamable-http" || candidate === "sse"
86+ return candidate === "auto" || candidate === "streamable-http" || candidate === "sse" || candidate === "stdio"
8787 ? candidate
8888 : undefined;
8989 };
@@ -106,6 +106,10 @@ const refreshPayloadFromSource = (source: Source): ConnectSourcePayload | null = · 16 unmodified lines
106106 ...(readBindingTransport(source) ? { transport: readBindingTransport(source) } : {}),
107107 queryParams: readBindingStringMap(source, "queryParams"),
108108 headers: readBindingStringMap(source, "headers"),
109+ command: readBindingString(source, "command"),
110+ args: readBindingStringArray(source, "args"),
111+ env: readBindingStringMap(source, "env"),
112+ cwd: readBindingString(source, "cwd"),
109113 };
110114 case "openapi": {
111115 const specUrl = readBindingString(source, "specUrl");
@@ -364,7 +368,12 @@ function ModelView(props: { · 252 unmodified lines
364368
365369 const filteredTools = props.bundle.tools.filter((tool) => {
366370 if (terms.length === 0) return true;
367− const corpus = [tool.path, tool.method ?? ""]
371+ const corpus = [
372+ tool.path,
373+ tool.method ?? "",
374+ tool.inputTypePreview ?? "",
375+ tool.outputTypePreview ?? "",
376+ ]
368377 .join(" ")
369378 .toLowerCase();
370379 return terms.every((t) => corpus.includes(t));
@@ -733,6 +742,14 @@ function ToolDetailPanel(props: { detail: SourceInspectionToolDetail }) { · 362 unmodified lines
733742 </span>
734743 )}
735744 </div>
745+ <div className="mt-2 rounded-md border border-border bg-card/70 px-2.5 py-2">
746+ <div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60">
747+ TypeScript Signature
748+ </div>
749+ <pre className="overflow-x-auto whitespace-pre-wrap font-mono text-[12px] text-foreground">
750+ {detail.contract.callDeclaration}
751+ </pre>
752+ </div>
736753 </div>
737754 </div>
738755 </div>
@@ -740,6 +757,66 @@ function ToolDetailPanel(props: { detail: SourceInspectionToolDetail }) { · 1 unmodified line
740757 {/* Body */}
741758 <div className="flex-1 overflow-y-auto">
742759 <div className="px-5 py-4 space-y-4">
760+ <section className="overflow-hidden rounded-lg border border-border bg-card/60">
761+ <div className="border-b border-border px-3 py-2 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70">
762+ Contract
763+ </div>
764+ <div className="grid grid-cols-1 gap-3 p-3 xl:grid-cols-2">
765+ <DocumentPanel
766+ title="Input Type"
767+ body={detail.contract.input.typeDeclaration}
768+ lang="typescript"
769+ empty="No input type."
770+ />
771+ <DocumentPanel
772+ title="Output Type"
773+ body={detail.contract.output.typeDeclaration}
774+ lang="typescript"
775+ empty="No output type."
776+ />
777+ </div>
778+ </section>
779+
780+ <section className="overflow-hidden rounded-lg border border-border bg-card/60">
781+ <div className="border-b border-border px-3 py-2 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70">
782+ Schemas
783+ </div>
784+ <div className="grid grid-cols-1 gap-3 p-3 xl:grid-cols-2">
785+ <DocumentPanel
786+ title="Input Schema"
787+ body={detail.contract.input.schemaJson}
788+ lang="json"
789+ empty="No input schema."
790+ compact
791+ />
792+ <DocumentPanel
793+ title="Output Schema"
794+ body={detail.contract.output.schemaJson}
795+ lang="json"
796+ empty="No output schema."
797+ compact
798+ />
799+ {detail.contract.input.exampleJson && (
800+ <DocumentPanel
801+ title="Example Request"
802+ body={detail.contract.input.exampleJson}
803+ lang="json"
804+ empty=""
805+ compact
806+ />
807+ )}
808+ {detail.contract.output.exampleJson && (
809+ <DocumentPanel
810+ title="Example Response"
811+ body={detail.contract.output.exampleJson}
812+ lang="json"
813+ empty=""
814+ compact
815+ />
816+ )}
817+ </div>
818+ </section>
819+
743820 {detail.sections.map((section, index) => {
744821 if (section.kind === "facts") {
745822 return (
@@ -885,6 +962,30 @@ function DiscoveryView(props: { · 139 unmodified lines
885962 {item.description}
886963 </p>
887964 )}
965+ {(item.inputTypePreview || item.outputTypePreview) && (
966+ <div className="mt-2 grid grid-cols-1 gap-2 md:grid-cols-2">
967+ {item.inputTypePreview && (
968+ <div className="rounded-md border border-border/70 bg-background/70 px-2.5 py-2">
969+ <div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/55">
970+ Input
971+ </div>
972+ <div className="font-mono text-[11px] text-foreground/85 line-clamp-3">
973+ {item.inputTypePreview}
974+ </div>
975+ </div>
976+ )}
977+ {item.outputTypePreview && (
978+ <div className="rounded-md border border-border/70 bg-background/70 px-2.5 py-2">
979+ <div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/55">
980+ Output
981+ </div>
982+ <div className="font-mono text-[11px] text-foreground/85 line-clamp-3">
983+ {item.outputTypePreview}
984+ </div>
985+ </div>
986+ )}
987+ </div>
988+ )}
888989 </button>
889990 ))}
890991 </div>
packages/kernel/core/src/discovery.ts+37−12
@@ -27,12 +27,41 @@ const searchableTextForTool = (tool: ToolDescriptor): string => · 26 unmodified lines
2727 tool.path,
2828 tool.sourceKey,
2929 tool.description ?? "",
30− tool.inputTypePreview ?? "",
31− tool.outputTypePreview ?? "",
30+ tool.contract?.inputTypePreview ?? "",
31+ tool.contract?.outputTypePreview ?? "",
3232 ]
3333 .join(" ")
3434 .toLowerCase();
3535
36+const descriptorContract = (
37+ descriptor: ToolDescriptor,
38+ includeSchemas: boolean,
39+): ToolDescriptor["contract"] | undefined => {
40+ const contract = descriptor.contract;
41+ if (!contract) {
42+ return undefined;
43+ }
44+
45+ if (includeSchemas) {
46+ return contract;
47+ }
48+
49+ return {
50+ ...(contract.inputTypePreview !== undefined
51+ ? { inputTypePreview: contract.inputTypePreview }
52+ : {}),
53+ ...(contract.outputTypePreview !== undefined
54+ ? { outputTypePreview: contract.outputTypePreview }
55+ : {}),
56+ ...(contract.exampleInput !== undefined
57+ ? { exampleInput: contract.exampleInput }
58+ : {}),
59+ ...(contract.exampleOutput !== undefined
60+ ? { exampleOutput: contract.exampleOutput }
61+ : {}),
62+ };
63+};
64+
3665 const projectDescriptor = (input: {
3766 descriptor: ToolDescriptor;
3867 includeSchemas: boolean;
@@ -45,8 +74,9 @@ const projectDescriptor = (input: { · 6 unmodified lines
4574
4675 return {
4776 ...descriptor,
48− inputSchema: undefined,
49− outputSchema: undefined,
77+ ...(descriptorContract(descriptor, false)
78+ ? { contract: descriptorContract(descriptor, false) }
79+ : {}),
5080 };
5181 };
5282
@@ -383,13 +413,8 @@ export function createDiscoveryPrimitivesFromToolCatalog(input: { · 330 unmodified lines
383413 score: hit.score,
384414 description: descriptor.description,
385415 interaction: descriptor.interaction ?? "auto",
386− inputTypePreview: descriptor.inputTypePreview,
387− outputTypePreview: descriptor.outputTypePreview,
388− ...(includeSchemas
389− ? {
390− inputSchema: descriptor.inputSchema,
391− outputSchema: descriptor.outputSchema,
392− }
416+ ...(descriptorContract(descriptor, includeSchemas)
417+ ? { contract: descriptorContract(descriptor, includeSchemas) }
393418 : {}),
394419 };
395420 })
@@ -451,7 +476,7 @@ export function buildDynamicExecuteDescription(input: { · 55 unmodified lines
451476 "Workflow:",
452477 '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });',
453478 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",
454− "3) Read details.inputSchema/details.outputSchema when you need the projected shape.",
479+ "3) Read details.contract?.inputSchema/details.contract?.outputSchema when you need the projected shape.",
455480 "4) Call selected tools.<path>(input).",
456481 "Do not use fetch; use tools.* only.",
457482 ].join("\n");
packages/kernel/core/src/index.test.ts+37−13
@@ -289,16 +289,20 @@ describe("codemode-core", () => { · 288 unmodified lines
289289 path: asToolPath("source.docs.search"),
290290 sourceKey: "source.docs",
291291 description: "Search docs",
292− inputTypePreview: "object",
293− outputTypePreview: "object",
292+ contract: {
293+ inputTypePreview: "object",
294+ outputTypePreview: "object",
295+ },
294296 },
295297 "source.issues.create": {
296298 path: asToolPath("source.issues.create"),
297299 sourceKey: "source.issues",
298300 description: "Create issue",
299301 interaction: "required",
300− inputTypePreview: "object",
301− outputTypePreview: "object",
302+ contract: {
303+ inputTypePreview: "object",
304+ outputTypePreview: "object",
305+ },
302306 },
303307 };
304308
@@ -327,8 +331,24 @@ describe("codemode-core", () => { · 22 unmodified lines
327331 .slice(0, limit)
328332 .map((descriptor) => ({
329333 ...descriptor,
330− inputSchema: includeSchemas ? descriptor.inputSchema : undefined,
331− outputSchema: includeSchemas ? descriptor.outputSchema : undefined,
334+ ...(descriptor.contract
335+ ? {
336+ contract: {
337+ ...(descriptor.contract.inputTypePreview !== undefined
338+ ? { inputTypePreview: descriptor.contract.inputTypePreview }
339+ : {}),
340+ ...(descriptor.contract.outputTypePreview !== undefined
341+ ? { outputTypePreview: descriptor.contract.outputTypePreview }
342+ : {}),
343+ ...(includeSchemas && descriptor.contract.inputSchema !== undefined
344+ ? { inputSchema: descriptor.contract.inputSchema }
345+ : {}),
346+ ...(includeSchemas && descriptor.contract.outputSchema !== undefined
347+ ? { outputSchema: descriptor.contract.outputSchema }
348+ : {}),
349+ },
350+ }
351+ : {}),
332352 })),
333353 ),
334354 getToolByPath: ({ path }) =>
@@ -367,9 +387,11 @@ describe("codemode-core", () => { · 32 unmodified lines
367387 path: asToolPath("github.repos.getRepo"),
368388 sourceKey: "source.github",
369389 description: "Get repository details",
370− inputTypePreview: "{ owner: string; repo: string }",
371− outputTypePreview: "Repository",
372− inputSchema: { type: "object" },
390+ contract: {
391+ inputTypePreview: "{ owner: string; repo: string }",
392+ outputTypePreview: "Repository",
393+ inputSchema: { type: "object" },
394+ },
373395 },
374396 },
375397 {
@@ -383,8 +405,10 @@ describe("codemode-core", () => { · 7 unmodified lines
383405 path: asToolPath("github.issues.list"),
384406 sourceKey: "source.github",
385407 description: "List repository issues",
386− inputTypePreview: "{ owner: string; repo: string }",
387− outputTypePreview: "Issue[]",
408+ contract: {
409+ inputTypePreview: "{ owner: string; repo: string }",
410+ outputTypePreview: "Issue[]",
411+ },
388412 },
389413 },
390414 ],
@@ -403,7 +427,7 @@ describe("codemode-core", () => { · 12 unmodified lines
403427 includeSchemas: false,
404428 });
405429 expect(listed[0]?.path).toBe("github.repos.getRepo");
406− expect(listed[0]?.inputSchema).toBeUndefined();
430+ expect(listed[0]?.contract?.inputSchema).toBeUndefined();
407431
408432 const discovered = yield* catalog.searchTools({
409433 query: "repository issues",
@@ -415,7 +439,7 @@ describe("codemode-core", () => { · 5 unmodified lines
415439 path: asToolPath("github.repos.getRepo"),
416440 includeSchemas: true,
417441 });
418− expect(described?.inputSchema).toEqual({ type: "object" });
442+ expect(described?.contract?.inputSchema).toEqual({ type: "object" });
419443 }),
420444 );
421445
packages/kernel/core/src/tool-map.ts+17−14
@@ -420,12 +420,26 @@ export function toolDescriptorsFromTools(input: { · 419 unmodified lines
420420 const metadata = entry.metadata;
421421 const definition = entry.tool;
422422 const inputSchema =
423− metadata?.inputSchema
423+ metadata?.contract?.inputSchema
424424 ?? deriveSchemaValue(definition.inputSchema)
425425 ?? deriveSchemaValue(definition.parameters);
426426 const outputSchema =
427− metadata?.outputSchema
427+ metadata?.contract?.outputSchema
428428 ?? deriveSchemaValue(definition.outputSchema);
429+ const contract = {
430+ inputTypePreview:
431+ metadata?.contract?.inputTypePreview ?? inferTypeFromSchemaValue(inputSchema, "unknown"),
432+ outputTypePreview:
433+ metadata?.contract?.outputTypePreview ?? inferTypeFromSchemaValue(outputSchema, "unknown"),
434+ ...(inputSchema !== undefined ? { inputSchema } : {}),
435+ ...(outputSchema !== undefined ? { outputSchema } : {}),
436+ ...(metadata?.contract?.exampleInput !== undefined
437+ ? { exampleInput: metadata.contract.exampleInput }
438+ : {}),
439+ ...(metadata?.contract?.exampleOutput !== undefined
440+ ? { exampleOutput: metadata.contract.exampleOutput }
441+ : {}),
442+ } satisfies NonNullable<ToolDescriptor["contract"]>;
429443
430444 return {
431445 path: entry.path,
@@ -433,18 +447,7 @@ export function toolDescriptorsFromTools(input: { · 1 unmodified line
433447 description: definition.description,
434448 interaction: metadata?.interaction,
435449 elicitation: metadata?.elicitation,
436− inputTypePreview:
437− metadata?.inputTypePreview ?? inferTypeFromSchemaValue(inputSchema, "unknown"),
438− outputTypePreview:
439− metadata?.outputTypePreview ?? inferTypeFromSchemaValue(outputSchema, "unknown"),
440− ...(inputSchema !== undefined ? { inputSchema } : {}),
441− ...(outputSchema !== undefined ? { outputSchema } : {}),
442− ...(metadata?.exampleInput !== undefined
443− ? { exampleInput: metadata.exampleInput }
444− : {}),
445− ...(metadata?.exampleOutput !== undefined
446− ? { exampleOutput: metadata.exampleOutput }
447− : {}),
450+ contract,
448451 ...(metadata?.providerKind ? { providerKind: metadata.providerKind } : {}),
449452 ...(metadata?.providerData !== undefined
450453 ? { providerData: metadata.providerData }
packages/kernel/core/src/types.ts+18−15
@@ -75,15 +75,28 @@ export type OnElicitation = ( · 74 unmodified lines
7575 input: ToolElicitationRequest,
7676 ) => Effect.Effect<ElicitationResponse, unknown>;
7777
78−export type ToolMetadata = {
79− interaction?: "auto" | "required";
80− elicitation?: ElicitationRequest;
78+export type ToolContract = {
8179 inputTypePreview?: string;
8280 outputTypePreview?: string;
8381 inputSchema?: unknown;
8482 outputSchema?: unknown;
8583 exampleInput?: unknown;
8684 exampleOutput?: unknown;
85+};
86+
87+export const ToolContractSchema = Schema.Struct({
88+ inputTypePreview: Schema.optional(Schema.String),
89+ outputTypePreview: Schema.optional(Schema.String),
90+ inputSchema: Schema.optional(Schema.Unknown),
91+ outputSchema: Schema.optional(Schema.Unknown),
92+ exampleInput: Schema.optional(Schema.Unknown),
93+ exampleOutput: Schema.optional(Schema.Unknown),
94+});
95+
96+export type ToolMetadata = {
97+ interaction?: "auto" | "required";
98+ elicitation?: ElicitationRequest;
99+ contract?: ToolContract;
87100 sourceKey?: string;
88101 providerKind?: string;
89102 providerData?: unknown;
@@ -131,12 +144,7 @@ export type ToolDescriptor = { · 41 unmodified lines
131144 description?: string;
132145 interaction?: "auto" | "required";
133146 elicitation?: ElicitationRequest;
134− inputTypePreview?: string;
135− outputTypePreview?: string;
136− inputSchema?: unknown;
137− outputSchema?: unknown;
138− exampleInput?: unknown;
139− exampleOutput?: unknown;
147+ contract?: ToolContract;
140148 providerKind?: string;
141149 providerData?: unknown;
142150 };
@@ -149,12 +157,7 @@ export const ToolDescriptorSchema = Schema.Struct({ · 6 unmodified lines
149157 Schema.Union(Schema.Literal("auto"), Schema.Literal("required")),
150158 ),
151159 elicitation: Schema.optional(Schema.Unknown),
152− inputTypePreview: Schema.optional(Schema.String),
153− outputTypePreview: Schema.optional(Schema.String),
154− inputSchema: Schema.optional(Schema.Unknown),
155− outputSchema: Schema.optional(Schema.Unknown),
156− exampleInput: Schema.optional(Schema.Unknown),
157− exampleOutput: Schema.optional(Schema.Unknown),
160+ contract: Schema.optional(ToolContractSchema),
158161 providerKind: Schema.optional(Schema.String),
159162 providerData: Schema.optional(Schema.Unknown),
160163 });
packages/platform/control-plane/src/runtime/catalog/catalog-typescript.ts+165−22
@@ -2,7 +2,12 @@ import { createHash } from "node:crypto"; · 1 unmodified line
22
33 import type { ProjectedCatalog } from "@executor/ir/catalog";
44 import type { ShapeSymbolId } from "@executor/ir/ids";
5−import type { CatalogV1, ShapeNode, ShapeSymbol } from "@executor/ir/model";
5+import type {
6+ CatalogV1,
7+ DocumentationBlock,
8+ ShapeNode,
9+ ShapeSymbol,
10+} from "@executor/ir/model";
611
712 export type CatalogTypeRoot = {
813 readonly shapeId: ShapeSymbolId;
@@ -128,7 +133,9 @@ const objectTypeLiteral = ( · 119 unmodified lines
128133
129134 return [
130135 "{",
131− ...lines.map((line) => `${indent}${line}`),
136+ ...lines.flatMap((line) =>
137+ line.split("\n").map((segment) => `${indent}${segment}`)
138+ ),
132139 `${indent.slice(0, -2)}}`,
133140 ].join("\n");
134141 };
@@ -153,6 +160,73 @@ const isSyntheticShapeLabel = (value: string): boolean => · 18 unmodified lines
153160
154161 const looksLikeHumanPhrase = (value: string): boolean => /\s/.test(value.trim());
155162
163+const cleanDocText = (value: string | undefined): string | null => {
164+ const trimmed = value?.trim();
165+ return trimmed && trimmed.length > 0 ? trimmed : null;
166+};
167+
168+const escapeJsDocText = (value: string): string => value.replace(/\*\//g, "*\\/");
169+
170+const pushParagraphLines = (
171+ lines: Array<string>,
172+ value: string | null,
173+): void => {
174+ if (!value) {
175+ return;
176+ }
177+
178+ if (lines.length > 0) {
179+ lines.push("");
180+ }
181+
182+ for (const line of value.split(/\r?\n/)) {
183+ lines.push(escapeJsDocText(line.trimEnd()));
184+ }
185+};
186+
187+export const documentationComment = (input: {
188+ title?: string;
189+ docs?: DocumentationBlock;
190+ deprecated?: boolean;
191+ includeTitle?: boolean;
192+}): string | null => {
193+ const lines: Array<string> = [];
194+ const title = input.includeTitle && input.title && looksLikeHumanPhrase(input.title)
195+ ? cleanDocText(input.title)
196+ : null;
197+ const summary = cleanDocText(input.docs?.summary);
198+ const description = cleanDocText(input.docs?.description);
199+ const externalDocsUrl = cleanDocText(input.docs?.externalDocsUrl);
200+
201+ pushParagraphLines(lines, title && title !== summary ? title : null);
202+ pushParagraphLines(lines, summary);
203+ pushParagraphLines(lines, description && description !== summary ? description : null);
204+
205+ if (externalDocsUrl) {
206+ if (lines.length > 0) {
207+ lines.push("");
208+ }
209+ lines.push(`@see ${escapeJsDocText(externalDocsUrl)}`);
210+ }
211+
212+ if (input.deprecated) {
213+ if (lines.length > 0) {
214+ lines.push("");
215+ }
216+ lines.push("@deprecated");
217+ }
218+
219+ if (lines.length === 0) {
220+ return null;
221+ }
222+
223+ return [
224+ "/**",
225+ ...lines.map((line) => line.length > 0 ? ` * ${line}` : " *"),
226+ " */",
227+ ].join("\n");
228+};
229+
156230 const compactAliasHint = (value: string): string => {
157231 const segments = typeNameWords(value);
158232 if (segments.length <= 5) {
@@ -254,16 +328,27 @@ const renderObjectNode = ( · 95 unmodified lines
254328 stack: readonly ShapeSymbolId[],
255329 aliasHint: string | undefined,
256330 renderShape: RenderShape,
331+ includeDocs: boolean,
257332 ): string => {
258333 const required = new Set(node.required);
259334 const lines = Object.keys(node.fields)
260335 .sort((left, right) => left.localeCompare(right))
261− .map((key) =>
262− `${formatPropertyKey(key)}${required.has(key) ? "" : "?"}: ${renderShape(node.fields[key]!.shapeId, {
263− stack,
264− aliasHint: aliasHint ? joinTypeNameSegments(aliasHint, key) : key,
265− })};`
266− );
336+ .map((key) => {
337+ const field = node.fields[key]!;
338+ const propertyLine =
339+ `${formatPropertyKey(key)}${required.has(key) ? "" : "?"}: ${renderShape(field.shapeId, {
340+ stack,
341+ aliasHint: aliasHint ? joinTypeNameSegments(aliasHint, key) : key,
342+ })};`;
343+ const comment = includeDocs
344+ ? documentationComment({
345+ docs: field.docs,
346+ deprecated: field.deprecated,
347+ })
348+ : null;
349+
350+ return comment ? `${comment}\n${propertyLine}` : propertyLine;
351+ });
267352
268353 const patternShapeIds = Object.values(node.patternProperties ?? {});
269354 const hasUnknownIndex = node.additionalProperties === true;
@@ -290,6 +375,7 @@ const renderObjectFields = ( · 20 unmodified lines
290375 stack: readonly ShapeSymbolId[],
291376 aliasHint: string | undefined,
292377 renderShape: RenderShape,
378+ includeDocs: boolean,
293379 ): string =>
294380 renderObjectNode(
295381 {
@@ -301,6 +387,7 @@ const renderObjectFields = ( · 5 unmodified lines
301387 stack,
302388 aliasHint,
303389 renderShape,
390+ includeDocs,
304391 );
305392
306393 const renderShapeBody = (
@@ -309,6 +396,7 @@ const renderShapeBody = ( · 2 unmodified lines
309396 stack: readonly ShapeSymbolId[],
310397 aliasHint: string | undefined,
311398 renderShape: RenderShape,
399+ includeDocs: boolean,
312400 ): string => {
313401 const shape = getShapeSymbol(catalog, shapeId);
314402 const node = shape.node;
@@ -321,7 +409,7 @@ const renderShapeBody = ( · 6 unmodified lines
321409 return renderInlineShapeNode(node);
322410 case "object":
323411 case "graphqlInterface":
324− return renderObjectFields(node, stack, aliasHint, renderShape);
412+ return renderObjectFields(node, stack, aliasHint, renderShape, includeDocs);
325413 case "array":
326414 return `Array<${wrapCompositeType(renderShape(node.itemShapeId, {
327415 stack,
@@ -714,6 +802,7 @@ export const createCatalogTypeProjector = (input: { · 386 unmodified lines
714802 stack: readonly ShapeSymbolId[],
715803 aliasHint: string | undefined,
716804 renderShape: RenderShape,
805+ includeDocs: boolean,
717806 ): string | null => {
718807 const variants = shapeIds.map((shapeId) => resolveUnionVariantObject(shapeId));
719808 if (variants.some((variant) => variant === null)) {
@@ -769,7 +858,7 @@ export const createCatalogTypeProjector = (input: { · 49 unmodified lines
769858 }
770859
771860 const baseText = baseHasSharedStructure
772− ? renderObjectNode(sharedNode, stack, aliasHint, renderShape)
861+ ? renderObjectNode(sharedNode, stack, aliasHint, renderShape, includeDocs)
773862 : null;
774863
775864 const variantTexts = objectVariants.map((variant, index) => {
@@ -786,6 +875,7 @@ export const createCatalogTypeProjector = (input: { · 10 unmodified lines
786875 stack,
787876 aliasHint ? joinTypeNameSegments(aliasHint, variantAliasLabel(discriminator, index)) : variantAliasLabel(discriminator, index),
788877 renderShape,
878+ includeDocs,
789879 );
790880 });
791881 const unionText = variantTexts.map((variantText) => wrapCompositeType(variantText)).join(" | ");
@@ -857,15 +947,21 @@ export const createCatalogTypeProjector = (input: { · 65 unmodified lines
857947 switch (shape.node.type) {
858948 case "anyOf":
859949 case "oneOf": {
860− const normalized = normalizedUnionRender(shape.node.items, stack, aliasHint, renderDeclarationShape);
861− return normalized ?? renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape);
950+ const normalized = normalizedUnionRender(
951+ shape.node.items,
952+ stack,
953+ aliasHint,
954+ renderDeclarationShape,
955+ true,
956+ );
957+ return normalized ?? renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape, true);
862958 }
863959 case "graphqlUnion": {
864− const normalized = normalizedUnionRender(shape.node.memberTypeIds, stack, aliasHint, renderDeclarationShape);
865− return normalized ?? renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape);
960+ const normalized = normalizedUnionRender(shape.node.memberTypeIds, stack, aliasHint, renderDeclarationShape, true);
961+ return normalized ?? renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape, true);
866962 }
867963 default:
868− return renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape);
964+ return renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape, true);
869965 }
870966 };
871967
@@ -921,16 +1017,49 @@ export const createCatalogTypeProjector = (input: { · 49 unmodified lines
9211017 switch (shape.node.type) {
9221018 case "anyOf":
9231019 case "oneOf": {
924− const normalized = normalizedUnionRender(shape.node.items, [...stack, shapeId], options.aliasHint, renderSelfContainedShape);
925− return normalized ?? renderShapeBody(catalog, shapeId, [...stack, shapeId], options.aliasHint, renderSelfContainedShape);
1020+ const normalized = normalizedUnionRender(
1021+ shape.node.items,
1022+ [...stack, shapeId],
1023+ options.aliasHint,
1024+ renderSelfContainedShape,
1025+ false,
1026+ );
1027+ return normalized ?? renderShapeBody(
1028+ catalog,
1029+ shapeId,
1030+ [...stack, shapeId],
1031+ options.aliasHint,
1032+ renderSelfContainedShape,
1033+ false,
1034+ );
9261035 }
9271036 case "graphqlUnion": {
928− const normalized = normalizedUnionRender(shape.node.memberTypeIds, [...stack, shapeId], options.aliasHint, renderSelfContainedShape);
929− return normalized ?? renderShapeBody(catalog, shapeId, [...stack, shapeId], options.aliasHint, renderSelfContainedShape);
1037+ const normalized = normalizedUnionRender(
1038+ shape.node.memberTypeIds,
1039+ [...stack, shapeId],
1040+ options.aliasHint,
1041+ renderSelfContainedShape,
1042+ false,
1043+ );
1044+ return normalized ?? renderShapeBody(
1045+ catalog,
1046+ shapeId,
1047+ [...stack, shapeId],
1048+ options.aliasHint,
1049+ renderSelfContainedShape,
1050+ false,
1051+ );
9301052 }
9311053 }
9321054
933− return renderShapeBody(catalog, shapeId, [...stack, shapeId], options.aliasHint, renderSelfContainedShape);
1055+ return renderShapeBody(
1056+ catalog,
1057+ shapeId,
1058+ [...stack, shapeId],
1059+ options.aliasHint,
1060+ renderSelfContainedShape,
1061+ false,
1062+ );
9341063 };
9351064
9361065 const supportingDeclarations = (): readonly string[] => {
@@ -960,11 +1089,25 @@ export const createCatalogTypeProjector = (input: { · 23 unmodified lines
9601089 const canonicalType = canonicalTypeForSignature(signature);
9611090 const representativeShapeId = representativeShapeIdForSignature(signature);
9621091 const aliasName = aliasNameForSignature(signature);
1092+ const representativeShape = getShapeSymbol(catalog, representativeShapeId);
1093+ const comment = documentationComment({
1094+ title: representativeShape.title,
1095+ docs: representativeShape.docs,
1096+ deprecated: representativeShape.deprecated,
1097+ includeTitle: true,
1098+ });
9631099 if (canonicalType !== aliasName) {
964− return `type ${aliasName} = ${canonicalType};`;
1100+ return comment
1101+ ? `${comment}\ntype ${aliasName} = ${canonicalType};`
1102+ : `type ${aliasName} = ${canonicalType};`;
9651103 }
9661104 const body = renderDeclarationShapeBody(representativeShapeId, [representativeShapeId], aliasName);
967− return body === aliasName ? "" : `type ${aliasName} = ${body};`;
1105+ if (body === aliasName) {
1106+ return "";
1107+ }
1108+
1109+ const declaration = `type ${aliasName} = ${body};`;
1110+ return comment ? `${comment}\n${declaration}` : declaration;
9681111 })
9691112 .filter((declaration) => declaration.length > 0);
9701113 };
packages/platform/control-plane/src/runtime/catalog/prettier-format.ts+2−2
@@ -3,7 +3,7 @@ import parserBabel from "prettier/plugins/babel"; · 2 unmodified lines
33 import parserEstree from "prettier/plugins/estree";
44 import parserTypescript from "prettier/plugins/typescript";
55
6−export type PrettierParser = "json" | "typescript";
6+export type PrettierParser = "json" | "typescript" | "typescript-module";
77
88 const plugins = [parserBabel, parserEstree, parserTypescript];
99
@@ -31,7 +31,7 @@ export async function formatWithPrettier( · 21 unmodified lines
3131 }
3232
3333 const result = await format(input, {
34− parser,
34+ parser: parser === "typescript-module" ? "typescript" : parser,
3535 plugins,
3636 printWidth: 60,
3737 tabWidth: 2,
packages/platform/control-plane/src/runtime/catalog/source/runtime.test.ts+16−16
@@ -353,7 +353,7 @@ describe("source-catalog-runtime", () => { · 352 unmodified lines
353353 });
354354
355355 expect(tool).toBeDefined();
356− expect(tool?.descriptor.inputSchema).toMatchObject({
356+ expect(tool?.descriptor.contract?.inputSchema).toMatchObject({
357357 type: "object",
358358 properties: {
359359 filter: {
@@ -368,11 +368,11 @@ describe("source-catalog-runtime", () => { · 8 unmodified lines
368368 },
369369 });
370370
371− const serializedInput = JSON.stringify(tool?.descriptor.inputSchema);
371+ const serializedInput = JSON.stringify(tool?.descriptor.contract?.inputSchema);
372372 expect(serializedInput).not.toContain("\"$ref\":\"#/$defs/shape_");
373373 expect(serializedInput).not.toContain("\"shape_");
374− expect(tool?.descriptor.inputTypePreview).toContain("filter?: {");
375− expect(tool?.descriptor.outputTypePreview).toContain("nodes?: {");
374+ expect(tool?.descriptor.contract?.inputTypePreview).toContain("filter?: {");
375+ expect(tool?.descriptor.contract?.outputTypePreview).toContain("nodes?: {");
376376 }));
377377
378378 it.effect("can skip type previews when only list metadata is needed", () =>
@@ -384,10 +384,10 @@ describe("source-catalog-runtime", () => { · 5 unmodified lines
384384 });
385385
386386 expect(tool).toBeDefined();
387− expect(tool?.descriptor.inputSchema).toBeUndefined();
388− expect(tool?.descriptor.outputSchema).toBeUndefined();
389− expect(tool?.descriptor.inputTypePreview).toBeUndefined();
390− expect(tool?.descriptor.outputTypePreview).toBeUndefined();
387+ expect(tool?.descriptor.contract?.inputSchema).toBeUndefined();
388+ expect(tool?.descriptor.contract?.outputSchema).toBeUndefined();
389+ expect(tool?.descriptor.contract?.inputTypePreview).toBeUndefined();
390+ expect(tool?.descriptor.contract?.outputTypePreview).toBeUndefined();
391391 }));
392392
393393 it.effect("projects a single tool by path without expanding the whole catalog", () =>
@@ -400,7 +400,7 @@ describe("source-catalog-runtime", () => { · 6 unmodified lines
400400
401401 expect(tool).not.toBeNull();
402402 expect(tool?.path).toBe("linear.teams");
403− expect(tool?.descriptor.inputSchema).toMatchObject({
403+ expect(tool?.descriptor.contract?.inputSchema).toMatchObject({
404404 type: "object",
405405 properties: {
406406 filter: {
@@ -569,12 +569,12 @@ describe("source-catalog-runtime", () => { · 162 unmodified lines
569569 });
570570
571571 expect(tool).not.toBeNull();
572− expect(tool?.descriptor.outputTypePreview).toContain("actor?: string;");
573− expect(tool?.descriptor.outputTypePreview).toContain('action: "blocked";');
574− expect(tool?.descriptor.outputTypePreview).toContain('action: "unblocked";');
575− expect(tool?.descriptor.outputTypePreview).toContain('action: "route-blocked";');
576− expect(tool?.descriptor.outputTypePreview).toContain("& (");
577− expect(tool?.descriptor.outputTypePreview).not.toContain("Member2");
572+ expect(tool?.descriptor.contract?.outputTypePreview).toContain("actor?: string;");
573+ expect(tool?.descriptor.contract?.outputTypePreview).toContain('action: "blocked";');
574+ expect(tool?.descriptor.contract?.outputTypePreview).toContain('action: "unblocked";');
575+ expect(tool?.descriptor.contract?.outputTypePreview).toContain('action: "route-blocked";');
576+ expect(tool?.descriptor.contract?.outputTypePreview).toContain("& (");
577+ expect(tool?.descriptor.contract?.outputTypePreview).not.toContain("Member2");
578578 }));
579579
580580 it.effect("uses the shared TS projector for unsupported preview shapes", () =>
@@ -622,6 +622,6 @@ describe("source-catalog-runtime", () => { · 41 unmodified lines
622622 });
623623
624624 expect(tool).not.toBeNull();
625− expect(tool?.descriptor.inputTypePreview).toContain("unsupported?: unknown;");
625+ expect(tool?.descriptor.contract?.inputTypePreview).toContain("unsupported?: unknown;");
626626 }));
627627 });
packages/platform/control-plane/src/runtime/catalog/source/runtime.ts+231−4
@@ -19,6 +19,7 @@ import { · 18 unmodified lines
1919 projectCatalogForAgentSdk,
2020 type ProjectedCatalog,
2121 } from "@executor/ir/catalog";
22+import type { ShapeSymbolId } from "@executor/ir/ids";
2223 import type {
2324 Capability,
2425 CatalogSnapshotV1,
@@ -29,10 +30,13 @@ import type { · 4 unmodified lines
2930 import { LocalSourceArtifactMissingError } from "../../local/errors";
3031 import {
3132 createCatalogTypeProjector,
33+ documentationComment,
3234 joinTypeNameSegments,
3335 projectedCatalogTypeRoots,
36+ shapeAllowsOmittedArgs,
3437 type CatalogTypeProjector,
3538 } from "../catalog-typescript";
39+import { formatWithPrettier } from "../prettier-format";
3640 import {
3741 RuntimeLocalWorkspaceService,
3842 type RuntimeLocalWorkspaceState,
@@ -85,6 +89,24 @@ export type LoadedSourceCatalogToolIndexEntry = Omit< · 46 unmodified lines
8589 "revision" | "projectedDescriptor" | "typeProjector"
8690 >;
8791
92+export type LoadedSourceCatalogToolContractSide = {
93+ shapeId: string | null;
94+ typePreview: string | null;
95+ typeDeclaration: string | null;
96+ schemaJson: string | null;
97+ exampleJson: string | null;
98+};
99+
100+export type LoadedSourceCatalogToolContract = {
101+ callSignature: string;
102+ callDeclaration: string;
103+ callShapeId: string;
104+ resultShapeId: string | null;
105+ responseSetId: string;
106+ input: LoadedSourceCatalogToolContractSide;
107+ output: LoadedSourceCatalogToolContractSide;
108+};
109+
88110 export const catalogToolCatalogEntry = (input: {
89111 tool: LoadedSourceCatalogToolIndexEntry;
90112 score: (queryTokens: readonly string[]) => number;
@@ -102,6 +124,14 @@ const catalogNamespaceFromPath = (path: string): string => { · 11 unmodified lines
102124
103125 const descriptorPath = (descriptor: CatalogToolDescriptor): string => descriptor.path;
104126
127+const optionalJsonString = (value: unknown): string | null => {
128+ if (value === undefined || value === null) {
129+ return null;
130+ }
131+
132+ return JSON.stringify(value, null, 2);
133+};
134+
105135 const projectedToolPath = (projected: ProjectedCatalog, capability: Capability): string =>
106136 projected.toolDescriptors[capability.id]?.toolPath.join(".") ?? "";
107137
@@ -535,10 +565,12 @@ const codemodeDescriptorFromCapability = (input: { · 427 unmodified lines
535565 sourceKey: input.source.id,
536566 description: input.capability.surface.summary ?? input.capability.surface.description,
537567 interaction,
538− ...(inputSchema !== undefined ? { inputSchema } : {}),
539− ...(outputSchema !== undefined ? { outputSchema } : {}),
540− inputTypePreview,
541− ...(outputTypePreview !== undefined ? { outputTypePreview } : {}),
568+ contract: {
569+ inputTypePreview,
570+ ...(outputTypePreview !== undefined ? { outputTypePreview } : {}),
571+ ...(inputSchema !== undefined ? { inputSchema } : {}),
572+ ...(outputSchema !== undefined ? { outputSchema } : {}),
573+ },
542574 providerKind: input.executable.adapterKey,
543575 providerData: {
544576 capabilityId: input.capability.id,
@@ -576,6 +608,8 @@ const loadedCatalogToolFromCapability = (input: { · 31 unmodified lines
576608 input.capability.surface.title,
577609 input.capability.surface.summary,
578610 input.capability.surface.description,
611+ descriptor.contract?.inputTypePreview,
612+ descriptor.contract?.outputTypePreview,
579613 ...(searchDoc?.tags ?? []),
580614 ...(searchDoc?.protocolHints ?? []),
581615 ...(searchDoc?.authHints ?? []),
@@ -853,6 +887,199 @@ export const expandCatalogTools = (input: { · 271 unmodified lines
853887 ),
854888 );
855889
890+const declarationBlockForShape = (input: {
891+ catalog: CatalogV1;
892+ shapeId: ShapeSymbolId;
893+ aliasHint: string;
894+}): Effect.Effect<string, Error, never> =>
895+ Effect.tryPromise({
896+ try: async () => {
897+ const projector = createCatalogTypeProjector({
898+ catalog: input.catalog,
899+ roots: [{
900+ shapeId: input.shapeId,
901+ aliasHint: input.aliasHint,
902+ }],
903+ });
904+ const rootType = projector.renderDeclarationShape(input.shapeId, {
905+ aliasHint: input.aliasHint,
906+ });
907+ const supportingDeclarations = projector.supportingDeclarations();
908+ const rootDeclarationPrefix = `type ${input.aliasHint} =`;
909+ const declarationText = supportingDeclarations.some((declaration) =>
910+ declaration.includes(rootDeclarationPrefix)
911+ )
912+ ? supportingDeclarations.join("\n\n")
913+ : [
914+ ...supportingDeclarations,
915+ typeAliasDeclaration({
916+ catalog: input.catalog,
917+ shapeId: input.shapeId,
918+ aliasHint: input.aliasHint,
919+ body: rootType,
920+ }),
921+ ].join("\n\n");
922+
923+ return formatWithPrettier(declarationText, "typescript-module");
924+ },
925+ catch: (cause) =>
926+ cause instanceof Error ? cause : new Error(String(cause)),
927+ });
928+
929+const formattedOptionalTypeExpression = (
930+ value: string | undefined,
931+): Effect.Effect<string | null, Error, never> =>
932+ value === undefined
933+ ? Effect.succeed(null)
934+ : Effect.tryPromise({
935+ try: () => formatWithPrettier(value, "typescript"),
936+ catch: (cause) =>
937+ cause instanceof Error ? cause : new Error(String(cause)),
938+ });
939+
940+const formattedOptionalJson = (
941+ value: unknown,
942+): Effect.Effect<string | null, Error, never> => {
943+ const serialized = optionalJsonString(value);
944+ return serialized === null
945+ ? Effect.succeed(null)
946+ : Effect.tryPromise({
947+ try: () => formatWithPrettier(serialized, "json"),
948+ catch: (cause) =>
949+ cause instanceof Error ? cause : new Error(String(cause)),
950+ });
951+};
952+
953+const lowerCamelCase = (value: string): string =>
954+ value.length === 0 ? "tool" : `${value.slice(0, 1).toLowerCase()}${value.slice(1)}`;
955+
956+const typeAliasDeclaration = (input: {
957+ catalog: CatalogV1;
958+ shapeId: ShapeSymbolId;
959+ aliasHint: string;
960+ body: string;
961+}): string => {
962+ const shape = input.catalog.symbols[input.shapeId];
963+ const comment = shape?.kind === "shape"
964+ ? documentationComment({
965+ title: shape.title,
966+ docs: shape.docs,
967+ deprecated: shape.deprecated,
968+ includeTitle: true,
969+ })
970+ : null;
971+ const declaration = `type ${input.aliasHint} = ${input.body};`;
972+ return comment ? `${comment}\n${declaration}` : declaration;
973+};
974+
975+export const buildLoadedSourceCatalogToolContract = (
976+ tool: LoadedSourceCatalogTool,
977+): Effect.Effect<LoadedSourceCatalogToolContract, Error, never> => {
978+ const inputAlias = joinTypeNameSegments(...tool.projectedDescriptor.toolPath, "call");
979+ const outputAlias = joinTypeNameSegments(...tool.projectedDescriptor.toolPath, "result");
980+ const inputShapeId = tool.projectedDescriptor.callShapeId;
981+ const outputShapeId = tool.projectedDescriptor.resultShapeId ?? null;
982+ const argsOptional = shapeAllowsOmittedArgs(tool.projectedCatalog, inputShapeId);
983+ const outputTypeName = outputShapeId ? outputAlias : "unknown";
984+ const callFunctionName = lowerCamelCase(
985+ joinTypeNameSegments(...tool.projectedDescriptor.toolPath),
986+ );
987+ const callComment = documentationComment({
988+ title: tool.capability.surface.title,
989+ docs: {
990+ ...(tool.capability.surface.summary
991+ ? { summary: tool.capability.surface.summary }
992+ : {}),
993+ ...(tool.capability.surface.description
994+ ? { description: tool.capability.surface.description }
995+ : {}),
996+ },
997+ includeTitle: true,
998+ });
999+
1000+ return Effect.gen(function* () {
1001+ const [
1002+ inputTypePreview,
1003+ outputTypePreview,
1004+ inputTypeDeclaration,
1005+ outputTypeDeclaration,
1006+ inputSchemaJson,
1007+ outputSchemaJson,
1008+ callSignature,
1009+ callDeclaration,
1010+ ] =
1011+ yield* Effect.all([
1012+ formattedOptionalTypeExpression(tool.descriptor.contract?.inputTypePreview),
1013+ formattedOptionalTypeExpression(tool.descriptor.contract?.outputTypePreview),
1014+ declarationBlockForShape({
1015+ catalog: tool.projectedCatalog,
1016+ shapeId: inputShapeId,
1017+ aliasHint: inputAlias,
1018+ }),
1019+ outputShapeId
1020+ ? declarationBlockForShape({
1021+ catalog: tool.projectedCatalog,
1022+ shapeId: outputShapeId,
1023+ aliasHint: outputAlias,
1024+ })
1025+ : Effect.succeed<string | null>(null),
1026+ formattedOptionalJson(
1027+ tool.descriptor.contract?.inputSchema
1028+ ?? shapeToJsonSchema(tool.projectedCatalog, inputShapeId),
1029+ ),
1030+ outputShapeId
1031+ ? formattedOptionalJson(
1032+ tool.descriptor.contract?.outputSchema
1033+ ?? shapeToJsonSchema(tool.projectedCatalog, outputShapeId),
1034+ )
1035+ : Effect.succeed<string | null>(null),
1036+ Effect.tryPromise({
1037+ try: () =>
1038+ formatWithPrettier(
1039+ `(${argsOptional ? "args?" : "args"}: ${inputAlias}) => Promise<${outputTypeName}>`,
1040+ "typescript",
1041+ ),
1042+ catch: (cause) =>
1043+ cause instanceof Error ? cause : new Error(String(cause)),
1044+ }),
1045+ Effect.tryPromise({
1046+ try: () =>
1047+ formatWithPrettier(
1048+ [
1049+ ...(callComment ? [callComment] : []),
1050+ `declare function ${callFunctionName}(${argsOptional ? "args?" : "args"}: ${inputAlias}): Promise<${outputTypeName}>;`,
1051+ ].join("\n"),
1052+ "typescript-module",
1053+ ),
1054+ catch: (cause) =>
1055+ cause instanceof Error ? cause : new Error(String(cause)),
1056+ }),
1057+ ]);
1058+
1059+ return {
1060+ callSignature,
1061+ callDeclaration,
1062+ callShapeId: inputShapeId,
1063+ resultShapeId: outputShapeId,
1064+ responseSetId: tool.projectedDescriptor.responseSetId,
1065+ input: {
1066+ shapeId: inputShapeId,
1067+ typePreview: inputTypePreview,
1068+ typeDeclaration: inputTypeDeclaration,
1069+ schemaJson: inputSchemaJson,
1070+ exampleJson: null,
1071+ },
1072+ output: {
1073+ shapeId: outputShapeId,
1074+ typePreview: outputTypePreview,
1075+ typeDeclaration: outputTypeDeclaration,
1076+ schemaJson: outputSchemaJson,
1077+ exampleJson: null,
1078+ },
1079+ } satisfies LoadedSourceCatalogToolContract;
1080+ });
1081+};
1082+
8561083 export const expandCatalogToolByPath = (input: {
8571084 catalogs: readonly LoadedSourceCatalog[];
8581085 path: string;
packages/platform/control-plane/src/runtime/sources/executor-tools.ts+13−4
@@ -186,6 +186,13 @@ const prepareExecutorAddSourceInput = (input: { · 185 unmodified lines
186186 endpoint: input.args.endpoint,
187187 name: input.args.name ?? null,
188188 namespace: input.args.namespace ?? null,
189+ transport: input.args.transport ?? null,
190+ queryParams: input.args.queryParams ?? null,
191+ headers: input.args.headers ?? null,
192+ command: input.args.command ?? null,
193+ args: input.args.args ?? null,
194+ env: input.args.env ?? null,
195+ cwd: input.args.cwd ?? null,
189196 workspaceId: input.workspaceId,
190197 actorAccountId: input.accountId,
191198 executionId: input.executionId,
@@ -468,10 +475,12 @@ export const createExecutorToolMap = (input: { · 276 unmodified lines
468475 },
469476 },
470477 metadata: {
471− inputTypePreview: EXECUTOR_SOURCES_ADD_INPUT_HINT,
472− outputTypePreview: EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE,
473− inputSchema: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA,
474− outputSchema: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA,
478+ contract: {
479+ inputTypePreview: EXECUTOR_SOURCES_ADD_INPUT_HINT,
480+ outputTypePreview: EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE,
481+ inputSchema: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA,
482+ outputSchema: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA,
483+ },
475484 sourceKey: "executor",
476485 interaction: "auto",
477486 },
packages/platform/control-plane/src/runtime/sources/source-adapter-fixture-matrix.test.ts+19−19
@@ -362,10 +362,10 @@ describe("source adapter fixture matrix", () => { · 361 unmodified lines
362362 Object.keys(snapshot.catalog.capabilities).length,
363363 ).toBeGreaterThan(250);
364364 expect(tool).toBeDefined();
365− expect(tool?.descriptor.inputTypePreview).not.toContain("unknown");
366− expect(tool?.descriptor.outputTypePreview).toContain("data:");
367− expect(tool?.descriptor.outputTypePreview).toContain("status:");
368− expect(tool?.descriptor.inputSchema).toMatchObject({
365+ expect(tool?.descriptor.contract?.inputTypePreview).not.toContain("unknown");
366+ expect(tool?.descriptor.contract?.outputTypePreview).toContain("data:");
367+ expect(tool?.descriptor.contract?.outputTypePreview).toContain("status:");
368+ expect(tool?.descriptor.contract?.inputSchema).toMatchObject({
369369 type: "object",
370370 properties: {
371371 idOrName: {
@@ -428,8 +428,8 @@ describe("source adapter fixture matrix", () => { · 56 unmodified lines
428428
429429 expect(manifest.tools.length).toBeGreaterThan(50);
430430 expect(tool).toBeDefined();
431− expect(tool?.descriptor.inputTypePreview).toContain("key_name");
432− expect(tool?.descriptor.inputSchema).toMatchObject({
431+ expect(tool?.descriptor.contract?.inputTypePreview).toContain("key_name");
432+ expect(tool?.descriptor.contract?.inputSchema).toMatchObject({
433433 type: "object",
434434 properties: {
435435 body: {
@@ -444,7 +444,7 @@ describe("source adapter fixture matrix", () => { · 8 unmodified lines
444444 },
445445 required: ["body"],
446446 });
447− expect(tool?.descriptor.outputSchema).toMatchObject({
447+ expect(tool?.descriptor.contract?.outputSchema).toMatchObject({
448448 type: "object",
449449 properties: {
450450 data: {
@@ -452,7 +452,7 @@ describe("source adapter fixture matrix", () => { · 1 unmodified line
452452 },
453453 },
454454 });
455− const outputSchema = tool?.descriptor.outputSchema;
455+ const outputSchema = tool?.descriptor.contract?.outputSchema;
456456 const dataVariants =
457457 outputSchema && typeof outputSchema === "object"
458458 ? (
@@ -979,10 +979,10 @@ describe("source adapter fixture matrix", () => { · 520 unmodified lines
979979 Object.keys(snapshot.catalog.capabilities).length,
980980 ).toBeGreaterThan(10);
981981 expect(tool).toBeDefined();
982− expect(tool?.descriptor.inputTypePreview).not.toContain("unknown");
983− expect(tool?.descriptor.outputTypePreview).toContain("data:");
984− expect(tool?.descriptor.outputTypePreview).toContain("status:");
985− expect(tool?.descriptor.inputSchema).toMatchObject({
982+ expect(tool?.descriptor.contract?.inputTypePreview).not.toContain("unknown");
983+ expect(tool?.descriptor.contract?.outputTypePreview).toContain("data:");
984+ expect(tool?.descriptor.contract?.outputTypePreview).toContain("status:");
985+ expect(tool?.descriptor.contract?.inputSchema).toMatchObject({
986986 type: "object",
987987 properties: {
988988 spreadsheetId: {
@@ -1001,7 +1001,7 @@ describe("source adapter fixture matrix", () => { · 12 unmodified lines
10011001 },
10021002 },
10031003 });
1004− expect(JSON.stringify(tool?.descriptor.outputSchema)).toContain(
1004+ expect(JSON.stringify(tool?.descriptor.contract?.outputSchema)).toContain(
10051005 '"gridProperties"',
10061006 );
10071007 expect(
@@ -1111,11 +1111,11 @@ describe("source adapter fixture matrix", () => { · 103 unmodified lines
11111111 ),
11121112 ).toEqual([]);
11131113 expect(tool).toBeDefined();
1114− expect(tool?.descriptor.inputTypePreview).toContain("{");
1115− expect(tool?.descriptor.inputTypePreview).toContain("input: {");
1116− expect(tool?.descriptor.outputTypePreview).toContain("data:");
1117− expect(tool?.descriptor.outputTypePreview).not.toContain("unknown[]");
1118− expect(tool?.descriptor.inputSchema).toMatchObject({
1114+ expect(tool?.descriptor.contract?.inputTypePreview).toContain("{");
1115+ expect(tool?.descriptor.contract?.inputTypePreview).toContain("input: {");
1116+ expect(tool?.descriptor.contract?.outputTypePreview).toContain("data:");
1117+ expect(tool?.descriptor.contract?.outputTypePreview).not.toContain("unknown[]");
1118+ expect(tool?.descriptor.contract?.inputSchema).toMatchObject({
11191119 type: "object",
11201120 required: ["input"],
11211121 properties: {
@@ -1303,7 +1303,7 @@ describe("source adapter fixture matrix", () => { · 181 unmodified lines
13031303 );
13041304 }
13051305
1306− expect(tool.descriptor.outputSchema).toMatchObject({
1306+ expect(tool.descriptor.contract?.outputSchema).toMatchObject({
13071307 type: "object",
13081308 properties: {
13091309 data: {
packages/platform/control-plane/src/runtime/sources/source-inspection.test.ts+218−20
@@ -1,11 +1,13 @@
11 import { describe, expect, it } from "@effect/vitest";
2+import * as Effect from "effect/Effect";
23
34 import type { LoadedSourceCatalogTool } from "../catalog/source/runtime";
45 import { inspectionToolDetailFromTool } from "./source-inspection";
56
67 describe("source inspection", () => {
7− it("keeps tool detail lightweight by default", () => {
8− const detail = inspectionToolDetailFromTool({
8+ it.effect("keeps the shared TypeScript contract first-class on tool detail", () =>
9+ Effect.gen(function* () {
10+ const detail = yield* inspectionToolDetailFromTool({
911 path: "linear.administrableTeams",
1012 source: {
1113 id: "linear",
@@ -30,7 +32,9 @@ describe("source inspection", () => { · 18 unmodified lines
3032 },
3133 projection: {
3234 callShapeId: "shape_call",
33− resultShapeId: "shape_result",
35+ resultDataShapeId: "shape_result_data",
36+ resultErrorShapeId: "shape_result_error",
37+ responseSetId: "response_set_1",
3438 },
3539 display: {
3640 protocol: "graphql",
@@ -47,26 +51,220 @@ describe("source inspection", () => { · 10 unmodified lines
4751 toolPath: ["linear", "administrableTeams"],
4852 callShapeId: "shape_call",
4953 resultShapeId: "shape_result",
54+ responseSetId: "response_set_1",
5055 },
51− descriptor: {},
56+ descriptor: {
57+ inputTypePreview: "{ input: string }",
58+ outputTypePreview: "{ data: string | null; error: unknown | null; headers: Record<string, string>; status: number | null }",
59+ inputSchema: {
60+ type: "object",
61+ properties: {
62+ input: { type: "string" },
63+ },
64+ required: ["input"],
65+ },
66+ outputSchema: {
67+ type: "object",
68+ properties: {
69+ data: { type: ["string", "null"] },
70+ },
71+ required: ["data"],
72+ },
73+ },
74+ projectedCatalog: {
75+ version: "ir.v1",
76+ documents: {},
77+ resources: {},
78+ scopes: {},
79+ capabilities: {},
80+ executables: {},
81+ responseSets: {},
82+ diagnostics: {},
83+ symbols: {
84+ shape_call: {
85+ id: "shape_call",
86+ kind: "shape",
87+ resourceId: "res_1",
88+ title: "AdministrableTeamsCall",
89+ node: {
90+ type: "object",
91+ fields: {
92+ input: {
93+ shapeId: "shape_call_input",
94+ docs: {
95+ description: "The exact input string to filter by.",
96+ },
97+ },
98+ },
99+ required: ["input"],
100+ additionalProperties: false,
101+ },
102+ synthetic: false,
103+ provenance: [],
104+ },
105+ shape_call_input: {
106+ id: "shape_call_input",
107+ kind: "shape",
108+ resourceId: "res_1",
109+ title: "Input",
110+ node: {
111+ type: "scalar",
112+ scalar: "string",
113+ },
114+ synthetic: false,
115+ provenance: [],
116+ },
117+ shape_result: {
118+ id: "shape_result",
119+ kind: "shape",
120+ resourceId: "res_1",
121+ title: "AdministrableTeamsResult",
122+ node: {
123+ type: "object",
124+ fields: {
125+ data: {
126+ shapeId: "shape_result_data_nullable",
127+ docs: {
128+ description: "Matching team data when present.",
129+ },
130+ },
131+ error: {
132+ shapeId: "shape_result_error_nullable",
133+ },
134+ headers: {
135+ shapeId: "shape_result_headers",
136+ },
137+ status: {
138+ shapeId: "shape_result_status_nullable",
139+ },
140+ },
141+ required: ["data", "error", "headers", "status"],
142+ additionalProperties: false,
143+ },
144+ synthetic: true,
145+ provenance: [],
146+ },
147+ shape_result_data_nullable: {
148+ id: "shape_result_data_nullable",
149+ kind: "shape",
150+ resourceId: "res_1",
151+ node: {
152+ type: "nullable",
153+ itemShapeId: "shape_result_data",
154+ },
155+ synthetic: true,
156+ provenance: [],
157+ },
158+ shape_result_data: {
159+ id: "shape_result_data",
160+ kind: "shape",
161+ resourceId: "res_1",
162+ node: {
163+ type: "scalar",
164+ scalar: "string",
165+ },
166+ synthetic: false,
167+ provenance: [],
168+ },
169+ shape_result_error_nullable: {
170+ id: "shape_result_error_nullable",
171+ kind: "shape",
172+ resourceId: "res_1",
173+ node: {
174+ type: "nullable",
175+ itemShapeId: "shape_result_error",
176+ },
177+ synthetic: true,
178+ provenance: [],
179+ },
180+ shape_result_error: {
181+ id: "shape_result_error",
182+ kind: "shape",
183+ resourceId: "res_1",
184+ node: {
185+ type: "unknown",
186+ },
187+ synthetic: false,
188+ provenance: [],
189+ },
190+ shape_result_headers: {
191+ id: "shape_result_headers",
192+ kind: "shape",
193+ resourceId: "res_1",
194+ node: {
195+ type: "object",
196+ fields: {},
197+ additionalProperties: "shape_result_header_value",
198+ },
199+ synthetic: true,
200+ provenance: [],
201+ },
202+ shape_result_header_value: {
203+ id: "shape_result_header_value",
204+ kind: "shape",
205+ resourceId: "res_1",
206+ node: {
207+ type: "scalar",
208+ scalar: "string",
209+ },
210+ synthetic: true,
211+ provenance: [],
212+ },
213+ shape_result_status_nullable: {
214+ id: "shape_result_status_nullable",
215+ kind: "shape",
216+ resourceId: "res_1",
217+ node: {
218+ type: "nullable",
219+ itemShapeId: "shape_result_status",
220+ },
221+ synthetic: true,
222+ provenance: [],
223+ },
224+ shape_result_status: {
225+ id: "shape_result_status",
226+ kind: "shape",
227+ resourceId: "res_1",
228+ node: {
229+ type: "scalar",
230+ scalar: "integer",
231+ },
232+ synthetic: true,
233+ provenance: [],
234+ },
235+ },
236+ },
237+ typeProjector: {} as LoadedSourceCatalogTool["typeProjector"],
52238 } as LoadedSourceCatalogTool);
53239
54− expect(detail.summary.path).toBe("linear.administrableTeams");
55− expect(detail.summary.method).toBe("query");
240+ expect(detail.summary.path).toBe("linear.administrableTeams");
241+ expect(detail.summary.method).toBe("query");
242+
243+ expect(detail.contract.callSignature).toContain("Promise<");
244+ expect(detail.contract.callDeclaration).toContain("/**");
245+ expect(detail.contract.callDeclaration).toContain("Administrable Teams");
246+ expect(detail.contract.callDeclaration).toContain("declare function linearAdministrableTeams");
247+ expect(detail.contract.callShapeId).toBe("shape_call");
248+ expect(detail.contract.resultShapeId).toBe("shape_result");
249+ expect(detail.contract.input.typeDeclaration).toContain("type LinearAdministrableTeamsCall");
250+ expect(detail.contract.output.typeDeclaration).toContain("type LinearAdministrableTeamsResult");
251+ expect(detail.contract.input.typeDeclaration).toContain("The exact input string to filter by.");
252+ expect(detail.contract.output.typeDeclaration).toContain("Matching team data when present.");
253+ expect(detail.contract.input.schemaJson).toContain("\"input\"");
254+ expect(detail.contract.output.schemaJson).toContain("\"data\"");
56255
57− const sectionTitles = detail.sections.map((section) => section.title);
58− expect(sectionTitles).not.toContain("Input Type");
59− expect(sectionTitles).not.toContain("Output Type");
60− expect(sectionTitles).not.toContain("Input Schema");
61− expect(sectionTitles).not.toContain("Output Schema");
256+ const sectionTitles = detail.sections.map((section) => section.title);
257+ expect(sectionTitles).not.toContain("Input Schema");
258+ expect(sectionTitles).not.toContain("Output Schema");
62259
63− const overview = detail.sections.find((section) => section.kind === "facts");
64− expect(overview).toBeDefined();
65− expect(overview?.items).toEqual(
66− expect.arrayContaining([
67− expect.objectContaining({ label: "Call shape", value: "shape_call" }),
68− expect.objectContaining({ label: "Result shape", value: "shape_result" }),
69− ]),
70− );
71− });
260+ const overview = detail.sections.find((section) => section.kind === "facts");
261+ expect(overview).toBeDefined();
262+ expect(overview?.items).toEqual(
263+ expect.arrayContaining([
264+ expect.objectContaining({ label: "Signature" }),
265+ expect.objectContaining({ label: "Call shape", value: "shape_call" }),
266+ expect.objectContaining({ label: "Result shape", value: "shape_result" }),
267+ ]),
268+ );
269+ }));
72270 });
packages/platform/control-plane/src/runtime/sources/source-inspection.ts+94−81
@@ -19,6 +19,7 @@ import * as Effect from "effect/Effect"; · 18 unmodified lines
1919 import { LocalSourceArtifactMissingError } from "../local/errors";
2020 import { operationErrors } from "../policy/operation-errors";
2121 import {
22+ buildLoadedSourceCatalogToolContract,
2223 expandCatalogToolByPath,
2324 expandCatalogTools,
2425 loadSourceWithCatalog,
@@ -107,6 +108,12 @@ const executableDetails = (tool: LoadedSourceCatalogTool) => { · 82 unmodified lines
107108 const inspectionToolListItemFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolListItem => ({
108109 path: tool.path,
109110 method: executableDetails(tool).method,
111+ ...(tool.descriptor.contract?.inputTypePreview
112+ ? { inputTypePreview: tool.descriptor.contract.inputTypePreview }
113+ : {}),
114+ ...(tool.descriptor.contract?.outputTypePreview
115+ ? { outputTypePreview: tool.descriptor.contract.outputTypePreview }
116+ : {}),
110117 });
111118
112119 const persistedToolSummaryFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolSummary => {
@@ -128,11 +135,11 @@ const persistedToolSummaryFromTool = (tool: LoadedSourceCatalogTool): SourceInsp · 15 unmodified lines
128135 tags: [...details.tags],
129136 method: details.method,
130137 pathTemplate: details.pathTemplate,
131− ...(tool.descriptor.inputTypePreview
132− ? { inputTypePreview: tool.descriptor.inputTypePreview }
138+ ...(tool.descriptor.contract?.inputTypePreview
139+ ? { inputTypePreview: tool.descriptor.contract.inputTypePreview }
133140 : {}),
134− ...(tool.descriptor.outputTypePreview
135− ? { outputTypePreview: tool.descriptor.outputTypePreview }
141+ ...(tool.descriptor.contract?.outputTypePreview
142+ ? { outputTypePreview: tool.descriptor.contract.outputTypePreview }
136143 : {}),
137144 };
138145 };
@@ -157,85 +164,89 @@ const jsonSection = ( · 18 unmodified lines
157164
158165 export const inspectionToolDetailFromTool = (
159166 tool: LoadedSourceCatalogTool,
160−): SourceInspectionToolDetail => {
161− const summary = persistedToolSummaryFromTool(tool);
162− const details = executableDetails(tool);
163− const overviewItems = [
164− { label: "Protocol", value: details.protocol },
165− ...(details.method ? [{ label: "Method", value: details.method, mono: true }] : []),
166− ...(details.pathTemplate ? [{ label: "Target", value: details.pathTemplate, mono: true }] : []),
167− ...(details.operationId ? [{ label: "Operation", value: details.operationId, mono: true }] : []),
168− ...(details.group ? [{ label: "Group", value: details.group, mono: true }] : []),
169− ...(details.leaf ? [{ label: "Leaf", value: details.leaf, mono: true }] : []),
170− ...(details.rawToolId ? [{ label: "Raw tool", value: details.rawToolId, mono: true }] : []),
171− { label: "Call shape", value: tool.projectedDescriptor.callShapeId, mono: true },
172− ...(tool.projectedDescriptor.resultShapeId
173− ? [{ label: "Result shape", value: tool.projectedDescriptor.resultShapeId, mono: true }]
174− : []),
175− ];
176− const nativeSections = [
177− ...(tool.capability.native ?? []).map((blob, index) => ({
178− kind: "code" as const,
179− title: `Capability native ${String(index + 1)}: ${blob.kind}`,
180− language: nativeEncodingLanguage(blob.encoding),
181− body:
182− typeof blob.value === "string"
183− ? blob.value
184− : JSON.stringify(blob.value ?? null, null, 2),
185− })),
186− ...(tool.executable.native ?? []).map((blob, index) => ({
187− kind: "code" as const,
188− title: `Executable native ${String(index + 1)}: ${blob.kind}`,
189− language: nativeEncodingLanguage(blob.encoding),
190− body:
191− typeof blob.value === "string"
192− ? blob.value
193− : JSON.stringify(blob.value ?? null, null, 2),
194− })),
195− ];
196− const sections = [
197− {
198− kind: "facts" as const,
199− title: "Overview",
200− items: overviewItems,
201− },
202− ...(summary.description
203− ? [{
204− kind: "markdown" as const,
205− title: "Description",
206− body: summary.description,
207− }]
208− : []),
209− ...([
210− jsonSection("Input Schema", tool.descriptor.inputSchema),
211− jsonSection("Output Schema", tool.descriptor.outputSchema),
212− jsonSection("Capability", tool.capability),
213− jsonSection("Executable", {
214− id: tool.executable.id,
215− adapterKey: tool.executable.adapterKey,
216− bindingVersion: tool.executable.bindingVersion,
217− binding: tool.executable.binding,
218− projection: tool.executable.projection,
219− display: tool.executable.display ?? null,
220− }),
221− jsonSection("Documentation", {
222− summary: tool.capability.surface.summary,
223− description: tool.capability.surface.description,
224− }),
225− ].filter((section) => section !== null) as Array<SourceInspectionToolDetail["sections"][number]>),
226− ...nativeSections,
227− ];
228−
229− return ({
230− summary,
231− sections,
167+): Effect.Effect<SourceInspectionToolDetail, Error, never> =>
168+ Effect.gen(function* () {
169+ const summary = persistedToolSummaryFromTool(tool);
170+ const details = executableDetails(tool);
171+ const contract = yield* buildLoadedSourceCatalogToolContract(tool);
172+ const overviewItems = [
173+ { label: "Protocol", value: details.protocol },
174+ ...(details.method ? [{ label: "Method", value: details.method, mono: true }] : []),
175+ ...(details.pathTemplate ? [{ label: "Target", value: details.pathTemplate, mono: true }] : []),
176+ ...(details.operationId ? [{ label: "Operation", value: details.operationId, mono: true }] : []),
177+ ...(details.group ? [{ label: "Group", value: details.group, mono: true }] : []),
178+ ...(details.leaf ? [{ label: "Leaf", value: details.leaf, mono: true }] : []),
179+ ...(details.rawToolId ? [{ label: "Raw tool", value: details.rawToolId, mono: true }] : []),
180+ { label: "Signature", value: contract.callSignature, mono: true },
181+ { label: "Call shape", value: contract.callShapeId, mono: true },
182+ ...(contract.resultShapeId
183+ ? [{ label: "Result shape", value: contract.resultShapeId, mono: true }]
184+ : []),
185+ { label: "Response set", value: contract.responseSetId, mono: true },
186+ ];
187+ const nativeSections = [
188+ ...(tool.capability.native ?? []).map((blob, index) => ({
189+ kind: "code" as const,
190+ title: `Capability native ${String(index + 1)}: ${blob.kind}`,
191+ language: nativeEncodingLanguage(blob.encoding),
192+ body:
193+ typeof blob.value === "string"
194+ ? blob.value
195+ : JSON.stringify(blob.value ?? null, null, 2),
196+ })),
197+ ...(tool.executable.native ?? []).map((blob, index) => ({
198+ kind: "code" as const,
199+ title: `Executable native ${String(index + 1)}: ${blob.kind}`,
200+ language: nativeEncodingLanguage(blob.encoding),
201+ body:
202+ typeof blob.value === "string"
203+ ? blob.value
204+ : JSON.stringify(blob.value ?? null, null, 2),
205+ })),
206+ ];
207+ const sections = [
208+ {
209+ kind: "facts" as const,
210+ title: "Overview",
211+ items: overviewItems,
212+ },
213+ ...(summary.description
214+ ? [{
215+ kind: "markdown" as const,
216+ title: "Description",
217+ body: summary.description,
218+ }]
219+ : []),
220+ ...([
221+ jsonSection("Capability", tool.capability),
222+ jsonSection("Executable", {
223+ id: tool.executable.id,
224+ adapterKey: tool.executable.adapterKey,
225+ bindingVersion: tool.executable.bindingVersion,
226+ binding: tool.executable.binding,
227+ projection: tool.executable.projection,
228+ display: tool.executable.display ?? null,
229+ }),
230+ jsonSection("Documentation", {
231+ summary: tool.capability.surface.summary,
232+ description: tool.capability.surface.description,
233+ }),
234+ ].filter((section) => section !== null) as Array<SourceInspectionToolDetail["sections"][number]>),
235+ ...nativeSections,
236+ ];
237+
238+ return {
239+ summary,
240+ contract,
241+ sections,
242+ } satisfies SourceInspectionToolDetail;
232243 });
233−};
234244
235245 const resolveSourceInspection = (input: {
236246 workspaceId: WorkspaceId;
237247 sourceId: SourceId;
238248 includeSchemas: boolean;
249+ includeTypePreviews: boolean;
239250 }) =>
240251 Effect.gen(function* () {
241252 const loaded = yield* loadSourceCatalogOrEmpty({
@@ -255,7 +266,7 @@ const resolveSourceInspection = (input: { · 13 unmodified lines
255266 const tools = yield* expandCatalogTools({
256267 catalogs: [loaded.catalogEntry],
257268 includeSchemas: input.includeSchemas,
258− includeTypePreviews: input.includeSchemas,
269+ includeTypePreviews: input.includeTypePreviews,
259270 });
260271
261272 return {
@@ -289,8 +300,8 @@ const resolveSourceInspectionTool = (input: { · 27 unmodified lines
289300 const tool = yield* expandCatalogToolByPath({
290301 catalogs: [loaded.catalogEntry],
291302 path: input.toolPath,
292− includeSchemas: false,
293− includeTypePreviews: false,
303+ includeSchemas: true,
304+ includeTypePreviews: true,
294305 });
295306
296307 return {
@@ -382,6 +393,7 @@ export const getSourceInspection = (input: { · 85 unmodified lines
382393 const inspection = yield* resolveSourceInspection({
383394 ...input,
384395 includeSchemas: false,
396+ includeTypePreviews: true,
385397 });
386398
387399 return {
@@ -420,7 +432,7 @@ export const getSourceInspectionToolDetail = (input: { · 32 unmodified lines
420432 );
421433 }
422434
423− return inspectionToolDetailFromTool(tool);
435+ return yield* inspectionToolDetailFromTool(tool);
424436 }).pipe(
425437 Effect.mapError((cause) =>
426438 mapInspectionError(
@@ -440,6 +452,7 @@ export const discoverSourceInspectionTools = (input: { · 13 unmodified lines
440452 workspaceId: input.workspaceId,
441453 sourceId: input.sourceId,
442454 includeSchemas: false,
455+ includeTypePreviews: true,
443456 });
444457 const queryTokens = tokenize(input.payload.query);
445458 const results = inspection.tools
packages/platform/control-plane/src/schema/models/source-inspection.ts+25−0
@@ -56,6 +56,26 @@ export const SourceInspectionSectionSchema = Schema.Union( · 55 unmodified lines
5656 export const SourceInspectionToolListItemSchema = Schema.Struct({
5757 path: Schema.String,
5858 method: Schema.NullOr(Schema.String),
59+ inputTypePreview: Schema.optional(Schema.String),
60+ outputTypePreview: Schema.optional(Schema.String),
61+});
62+
63+export const SourceInspectionToolContractSideSchema = Schema.Struct({
64+ shapeId: Schema.NullOr(Schema.String),
65+ typePreview: Schema.NullOr(Schema.String),
66+ typeDeclaration: Schema.NullOr(Schema.String),
67+ schemaJson: Schema.NullOr(Schema.String),
68+ exampleJson: Schema.NullOr(Schema.String),
69+});
70+
71+export const SourceInspectionToolContractSchema = Schema.Struct({
72+ callSignature: Schema.String,
73+ callDeclaration: Schema.String,
74+ callShapeId: Schema.String,
75+ resultShapeId: Schema.NullOr(Schema.String),
76+ responseSetId: Schema.String,
77+ input: SourceInspectionToolContractSideSchema,
78+ output: SourceInspectionToolContractSideSchema,
5979 });
6080
6181 export const SourceInspectionSchema = Schema.Struct({
@@ -68,6 +88,7 @@ export const SourceInspectionSchema = Schema.Struct({ · 6 unmodified lines
6888
6989 export const SourceInspectionToolDetailSchema = Schema.Struct({
7090 summary: SourceInspectionToolSummarySchema,
91+ contract: SourceInspectionToolContractSchema,
7192 sections: Schema.Array(SourceInspectionSectionSchema),
7293 });
7394
@@ -109,6 +130,10 @@ export type SourceInspectionSection = · 35 unmodified lines
109130 typeof SourceInspectionSectionSchema.Type;
110131 export type SourceInspectionToolListItem =
111132 typeof SourceInspectionToolListItemSchema.Type;
133+export type SourceInspectionToolContractSide =
134+ typeof SourceInspectionToolContractSideSchema.Type;
135+export type SourceInspectionToolContract =
136+ typeof SourceInspectionToolContractSchema.Type;
112137 export type SourceInspection = typeof SourceInspectionSchema.Type;
113138 export type SourceInspectionToolDetail =
114139 typeof SourceInspectionToolDetailSchema.Type;
packages/platform/control-plane/src/schema/schema.test.ts+21−0
@@ -25,6 +25,27 @@ describe("control-plane-schema", () => { · 24 unmodified lines
2525 method: "GET",
2626 pathTemplate: "/issues",
2727 },
28+ contract: {
29+ callSignature: "(args: GithubIssuesListCall) => Promise<GithubIssuesListResult>",
30+ callDeclaration: "declare function githubIssuesList(args: GithubIssuesListCall): Promise<GithubIssuesListResult>;",
31+ callShapeId: "shape_call",
32+ resultShapeId: "shape_result",
33+ responseSetId: "response_set_1",
34+ input: {
35+ shapeId: "shape_call",
36+ typePreview: "{ owner: string; repo: string }",
37+ typeDeclaration: "type GithubIssuesListCall = { owner: string; repo: string };",
38+ schemaJson: "{}",
39+ exampleJson: null,
40+ },
41+ output: {
42+ shapeId: "shape_result",
43+ typePreview: "{ data: unknown; error: unknown; headers: Record<string, string>; status: number | null }",
44+ typeDeclaration: "type GithubIssuesListResult = { data: unknown; error: unknown; headers: Record<string, string>; status: number | null };",
45+ schemaJson: "{}",
46+ exampleJson: null,
47+ },
48+ },
2849 sections: [{
2950 kind: "facts",
3051 title: "Overview",
packages/sources/google-discovery/src/tools.ts+10−6
@@ -456,12 +456,16 @@ export const createGoogleDiscoveryToolFromDefinition = ( · 455 unmodified lines
456456 interaction: input.definition.method === "get" || input.definition.method === "head"
457457 ? "auto"
458458 : "required",
459− ...(presentation.inputSchema !== undefined
460− ? { inputSchema: presentation.inputSchema }
461− : {}),
462− ...(presentation.outputSchema !== undefined
463− ? { outputSchema: presentation.outputSchema }
464− : {}),
459+ contract: {
460+ inputTypePreview: presentation.inputTypePreview,
461+ outputTypePreview: presentation.outputTypePreview,
462+ ...(presentation.inputSchema !== undefined
463+ ? { inputSchema: presentation.inputSchema }
464+ : {}),
465+ ...(presentation.outputSchema !== undefined
466+ ? { outputSchema: presentation.outputSchema }
467+ : {}),
468+ },
465469 sourceKey: input.sourceKey,
466470 providerKind: "google_discovery",
467471 providerData,
packages/sources/graphql/src/graphql-tools.ts+34−30
@@ -1437,25 +1437,27 @@ export const createGraphqlToolFromPersistedOperation = (input: { · 1436 unmodified lines
14371437 : providerData.operationType === "query"
14381438 ? "auto"
14391439 : "required",
1440− inputTypePreview: typeSignatureFromSchema(
1441− input.inputSchema,
1442− "unknown",
1443− GRAPHQL_PRESENTATION_TYPE_MAX_LENGTH,
1444− ),
1445− outputTypePreview: typeSignatureFromSchema(
1446− input.outputSchema,
1447− "unknown",
1448− GRAPHQL_PRESENTATION_TYPE_MAX_LENGTH,
1449− ),
1450− ...(input.inputSchema !== undefined
1451− ? { inputSchema: input.inputSchema }
1452− : {}),
1453− ...(input.outputSchema !== undefined
1454− ? { outputSchema: input.outputSchema }
1455− : {}),
1456− ...(input.exampleInput !== undefined
1457− ? { exampleInput: input.exampleInput }
1458− : {}),
1440+ contract: {
1441+ inputTypePreview: typeSignatureFromSchema(
1442+ input.inputSchema,
1443+ "unknown",
1444+ GRAPHQL_PRESENTATION_TYPE_MAX_LENGTH,
1445+ ),
1446+ outputTypePreview: typeSignatureFromSchema(
1447+ input.outputSchema,
1448+ "unknown",
1449+ GRAPHQL_PRESENTATION_TYPE_MAX_LENGTH,
1450+ ),
1451+ ...(input.inputSchema !== undefined
1452+ ? { inputSchema: input.inputSchema }
1453+ : {}),
1454+ ...(input.outputSchema !== undefined
1455+ ? { outputSchema: input.outputSchema }
1456+ : {}),
1457+ ...(input.exampleInput !== undefined
1458+ ? { exampleInput: input.exampleInput }
1459+ : {}),
1460+ },
14591461 sourceKey: input.sourceKey,
14601462 providerKind: "graphql",
14611463 providerData,
@@ -1524,17 +1526,19 @@ export const graphqlToolDescriptorFromDefinition = (input: { · 62 unmodified lines
15241526 description: input.definition.description,
15251527 interaction:
15261528 input.definition.operationType === "query" ? "auto" : "required",
1527− inputTypePreview: presentation.inputTypePreview,
1528− outputTypePreview: presentation.outputTypePreview,
1529− ...(input.includeSchemas && presentation.inputSchema !== undefined
1530− ? { inputSchema: presentation.inputSchema }
1531− : {}),
1532− ...(input.includeSchemas && presentation.outputSchema !== undefined
1533− ? { outputSchema: presentation.outputSchema }
1534− : {}),
1535− ...(presentation.exampleInput !== undefined
1536− ? { exampleInput: presentation.exampleInput }
1537− : {}),
1529+ contract: {
1530+ inputTypePreview: presentation.inputTypePreview,
1531+ outputTypePreview: presentation.outputTypePreview,
1532+ ...(input.includeSchemas && presentation.inputSchema !== undefined
1533+ ? { inputSchema: presentation.inputSchema }
1534+ : {}),
1535+ ...(input.includeSchemas && presentation.outputSchema !== undefined
1536+ ? { outputSchema: presentation.outputSchema }
1537+ : {}),
1538+ ...(presentation.exampleInput !== undefined
1539+ ? { exampleInput: presentation.exampleInput }
1540+ : {}),
1541+ },
15381542 providerKind: "graphql",
15391543 providerData: presentation.providerData,
15401544 };
packages/sources/mcp/src/adapter.ts+165−38
@@ -4,7 +4,8 @@ import { · 3 unmodified lines
44 type ToolInput,
55 type ToolPath,
66 } from "@executor/codemode-core";
7−import { createSdkMcpConnector } from "./connection";
7+import { createPooledMcpConnector } from "./connection-pool";
8+import { createSdkMcpConnector, isMcpStdioTransport } from "./connection";
89 import {
910 createMcpToolsFromManifest,
1011 discoverMcpToolsFromConnector,
@@ -26,8 +27,8 @@ import { · 15 unmodified lines
2627 encodeBindingConfig,
2728 McpConnectFieldsSchema,
2829 OptionalNullableStringSchema,
29− SourceConnectCommonFieldsSchema,
3030 SourceTransportSchema,
31+ StringArraySchema,
3132 StringMapSchema,
3233 createCatalogImportMetadata,
3334 EXECUTABLE_BINDING_VERSION,
@@ -35,6 +36,7 @@ import { · 1 unmodified line
3536 type Source,
3637 type SourceAdapter,
3738 type SourceCatalogSyncResult,
39+ type SourceTransport,
3840 } from "@executor/source-core";
3941 import * as Effect from "effect/Effect";
4042 import * as Schema from "effect/Schema";
@@ -69,27 +71,38 @@ const namespaceFromSourceName = (name: string): string => { · 28 unmodified lines
6971 return normalized.length > 0 ? normalized : "source";
7072 };
7173
74+const OptionalNullableStringArraySchema = Schema.optional(
75+ Schema.NullOr(StringArraySchema),
76+);
77+
7278 const McpConnectPayloadSchema = Schema.extend(
73− SourceConnectCommonFieldsSchema,
74− Schema.extend(
75− McpConnectFieldsSchema,
76− Schema.Struct({
77− kind: Schema.Literal("mcp"),
78− }),
79− ),
79+ McpConnectFieldsSchema,
80+ Schema.Struct({
81+ kind: Schema.Literal("mcp"),
82+ endpoint: OptionalNullableStringSchema,
83+ name: OptionalNullableStringSchema,
84+ namespace: OptionalNullableStringSchema,
85+ }),
8086 );
8187
82−const McpExecutorAddInputSchema = Schema.Struct({
83− kind: Schema.optional(Schema.Literal("mcp")),
84− endpoint: Schema.String,
85− name: OptionalNullableStringSchema,
86− namespace: OptionalNullableStringSchema,
87−});
88+const McpExecutorAddInputSchema = Schema.extend(
89+ McpConnectFieldsSchema,
90+ Schema.Struct({
91+ kind: Schema.optional(Schema.Literal("mcp")),
92+ endpoint: OptionalNullableStringSchema,
93+ name: OptionalNullableStringSchema,
94+ namespace: OptionalNullableStringSchema,
95+ }),
96+);
8897
8998 const McpBindingConfigSchema = Schema.Struct({
9099 transport: Schema.NullOr(SourceTransportSchema),
91100 queryParams: Schema.NullOr(StringMapSchema),
92101 headers: Schema.NullOr(StringMapSchema),
102+ command: Schema.NullOr(Schema.String),
103+ args: Schema.NullOr(StringArraySchema),
104+ env: Schema.NullOr(StringMapSchema),
105+ cwd: Schema.NullOr(Schema.String),
93106 });
94107
95108 type McpBindingConfig = typeof McpBindingConfigSchema.Type;
@@ -98,6 +111,10 @@ const McpSourceBindingPayloadSchema = Schema.Struct({ · 2 unmodified lines
98111 transport: Schema.optional(Schema.NullOr(SourceTransportSchema)),
99112 queryParams: Schema.optional(Schema.NullOr(StringMapSchema)),
100113 headers: Schema.optional(Schema.NullOr(StringMapSchema)),
114+ command: Schema.optional(Schema.NullOr(Schema.String)),
115+ args: OptionalNullableStringArraySchema,
116+ env: Schema.optional(Schema.NullOr(StringMapSchema)),
117+ cwd: Schema.optional(Schema.NullOr(Schema.String)),
101118 });
102119
103120 const McpExecutableBindingSchema = Schema.Struct({
@@ -127,6 +144,86 @@ const bindingHasAnyField = ( · 23 unmodified lines
127144 !Array.isArray(value) &&
128145 fields.some((field) => Object.prototype.hasOwnProperty.call(value, field));
129146
147+const trimOrNull = (value: string | null | undefined): string | null => {
148+ if (value === null || value === undefined) {
149+ return null;
150+ }
151+
152+ const trimmed = value.trim();
153+ return trimmed.length > 0 ? trimmed : null;
154+};
155+
156+const normalizeStringArray = (
157+ value: ReadonlyArray<string> | null | undefined,
158+): string[] | null => {
159+ if (!value || value.length === 0) {
160+ return null;
161+ }
162+
163+ const normalized = value
164+ .map((entry) => entry.trim())
165+ .filter((entry) => entry.length > 0);
166+
167+ return normalized.length > 0 ? normalized : null;
168+};
169+
170+const normalizeMcpBindingConfig = (
171+ bindingConfig: {
172+ transport?: SourceTransport | null | undefined;
173+ queryParams?: Record<string, string> | null | undefined;
174+ headers?: Record<string, string> | null | undefined;
175+ command?: string | null | undefined;
176+ args?: ReadonlyArray<string> | null | undefined;
177+ env?: Record<string, string> | null | undefined;
178+ cwd?: string | null | undefined;
179+ },
180+): Effect.Effect<McpBindingConfig, Error, never> =>
181+ Effect.gen(function* () {
182+ const command = trimOrNull(bindingConfig.command);
183+ const isStdio = isMcpStdioTransport({
184+ transport: bindingConfig.transport ?? undefined,
185+ command: command ?? undefined,
186+ });
187+
188+ if (isStdio) {
189+ if (command === null) {
190+ return yield* sourceCoreEffectError("mcp/adapter", "MCP stdio transport requires a command");
191+ }
192+
193+ if (bindingConfig.queryParams && Object.keys(bindingConfig.queryParams).length > 0) {
194+ return yield* sourceCoreEffectError("mcp/adapter", "MCP stdio transport does not support query params");
195+ }
196+
197+ if (bindingConfig.headers && Object.keys(bindingConfig.headers).length > 0) {
198+ return yield* sourceCoreEffectError("mcp/adapter", "MCP stdio transport does not support request headers");
199+ }
200+
201+ return {
202+ transport: "stdio",
203+ queryParams: null,
204+ headers: null,
205+ command,
206+ args: normalizeStringArray(bindingConfig.args),
207+ env: bindingConfig.env ?? null,
208+ cwd: trimOrNull(bindingConfig.cwd),
209+ } satisfies McpBindingConfig;
210+ }
211+
212+ if (command !== null || bindingConfig.args || bindingConfig.env || trimOrNull(bindingConfig.cwd) !== null) {
213+ return yield* sourceCoreEffectError("mcp/adapter", "MCP process settings require transport: \"stdio\"");
214+ }
215+
216+ return {
217+ transport: bindingConfig.transport ?? null,
218+ queryParams: bindingConfig.queryParams ?? null,
219+ headers: bindingConfig.headers ?? null,
220+ command: null,
221+ args: null,
222+ env: null,
223+ cwd: null,
224+ } satisfies McpBindingConfig;
225+ });
226+
130227 const mcpBindingConfigFromSource = (
131228 source: Pick<Source, "id" | "bindingVersion" | "binding">,
132229 ): Effect.Effect<McpBindingConfig, Error, never> =>
@@ -145,14 +242,10 @@ const mcpBindingConfigFromSource = ( · 12 unmodified lines
145242 expectedVersion: MCP_BINDING_CONFIG_VERSION,
146243 schema: McpSourceBindingPayloadSchema,
147244 value: source.binding,
148− allowedKeys: ["transport", "queryParams", "headers"],
245+ allowedKeys: ["transport", "queryParams", "headers", "command", "args", "env", "cwd"],
149246 });
150247
151− return {
152− transport: bindingConfig.transport ?? null,
153− queryParams: bindingConfig.queryParams ?? null,
154− headers: bindingConfig.headers ?? null,
155− } satisfies McpBindingConfig;
248+ return yield* normalizeMcpBindingConfig(bindingConfig);
156249 });
157250
158251 const effectFromMcpManifestEntry = (
@@ -233,7 +326,8 @@ export const mcpSourceAdapter: SourceAdapter = { · 74 unmodified lines
233326 connectPayloadSchema: McpConnectPayloadSchema,
234327 executorAddInputSchema: McpExecutorAddInputSchema,
235328 executorAddHelpText: [
236− 'Omit kind or set kind: "mcp". endpoint is the MCP server URL.',
329+ 'Omit kind or set kind: "mcp". For remote servers, provide endpoint plus optional transport/queryParams/headers.',
330+ 'For local servers, set transport: "stdio" and provide command plus optional args/env/cwd.',
237331 ],
238332 executorAddInputSignatureWidth: 240,
239333 serializeBindingConfig: (source) =>
@@ -264,6 +358,10 @@ export const mcpSourceAdapter: SourceAdapter = { · 24 unmodified lines
264358 transport: bindingConfig.transport,
265359 queryParams: bindingConfig.queryParams,
266360 headers: bindingConfig.headers,
361+ command: bindingConfig.command,
362+ args: bindingConfig.args,
363+ env: bindingConfig.env,
364+ cwd: bindingConfig.cwd,
267365 })),
268366 sourceConfigFromSource: (source) =>
269367 Effect.runSync(
@@ -273,6 +371,10 @@ export const mcpSourceAdapter: SourceAdapter = { · 3 unmodified lines
273371 transport: bindingConfig.transport,
274372 queryParams: bindingConfig.queryParams,
275373 headers: bindingConfig.headers,
374+ command: bindingConfig.command,
375+ args: bindingConfig.args,
376+ env: bindingConfig.env,
377+ cwd: bindingConfig.cwd,
276378 })),
277379 ),
278380 validateSource: (source) =>
@@ -286,6 +388,10 @@ export const mcpSourceAdapter: SourceAdapter = { · 7 unmodified lines
286388 transport: bindingConfig.transport,
287389 queryParams: bindingConfig.queryParams,
288390 headers: bindingConfig.headers,
391+ command: bindingConfig.command,
392+ args: bindingConfig.args,
393+ env: bindingConfig.env,
394+ cwd: bindingConfig.cwd,
289395 },
290396 };
291397 }),
@@ -309,6 +415,10 @@ export const mcpSourceAdapter: SourceAdapter = { · 17 unmodified lines
309415 authCookies: auth.cookies,
310416 }),
311417 authProvider: auth.authProvider,
418+ command: bindingConfig.command ?? undefined,
419+ args: bindingConfig.args ?? undefined,
420+ env: bindingConfig.env ?? undefined,
421+ cwd: bindingConfig.cwd ?? undefined,
312422 }),
313423 catch: (cause) =>
314424 cause instanceof Error ? cause : new Error(String(cause)),
@@ -353,19 +463,30 @@ export const mcpSourceAdapter: SourceAdapter = { · 38 unmodified lines
353463 const bindingConfig = Effect.runSync(
354464 mcpBindingConfigFromSource(input.source),
355465 );
356− const connector = createSdkMcpConnector({
357− endpoint: input.source.endpoint,
358− transport: bindingConfig.transport ?? undefined,
359− queryParams: {
360− ...bindingConfig.queryParams,
361− ...input.auth.queryParams,
362− },
363− headers: headersWithAuthCookies({
364− headers: bindingConfig.headers ?? {},
365− authHeaders: input.auth.headers,
366− authCookies: input.auth.cookies,
466+ const connector = createPooledMcpConnector({
467+ connect: createSdkMcpConnector({
468+ endpoint: input.source.endpoint,
469+ transport: bindingConfig.transport ?? undefined,
470+ queryParams: {
471+ ...bindingConfig.queryParams,
472+ ...input.auth.queryParams,
473+ },
474+ headers: headersWithAuthCookies({
475+ headers: bindingConfig.headers ?? {},
476+ authHeaders: input.auth.headers,
477+ authCookies: input.auth.cookies,
478+ }),
479+ authProvider: input.auth.authProvider,
480+ command: bindingConfig.command ?? undefined,
481+ args: bindingConfig.args ?? undefined,
482+ env: bindingConfig.env ?? undefined,
483+ cwd: bindingConfig.cwd ?? undefined,
367484 }),
368− authProvider: input.auth.authProvider,
485+ runId:
486+ typeof input.context?.runId === "string" && input.context.runId.length > 0
487+ ? input.context.runId
488+ : undefined,
489+ sourceKey: input.source.id,
369490 });
370491 const tools = createMcpToolsFromManifest({
371492 manifest: {
@@ -392,8 +513,8 @@ export const mcpSourceAdapter: SourceAdapter = { · 20 unmodified lines
392513 icons: null,
393514 meta: null,
394515 rawTool: null,
395− inputSchema: input.descriptor.inputSchema,
396− outputSchema: input.descriptor.outputSchema,
516+ inputSchema: input.descriptor.contract?.inputSchema,
517+ outputSchema: input.descriptor.contract?.outputSchema,
397518 },
398519 ],
399520 },
@@ -430,8 +551,14 @@ export const mcpSourceAdapter: SourceAdapter = { · 30 unmodified lines
430551 metadata: {
431552 sourceKey: input.source.id,
432553 interaction: input.descriptor.interaction,
433− inputSchema: input.descriptor.inputSchema,
434− outputSchema: input.descriptor.outputSchema,
554+ contract: {
555+ ...(input.descriptor.contract?.inputSchema !== undefined
556+ ? { inputSchema: input.descriptor.contract.inputSchema }
557+ : {}),
558+ ...(input.descriptor.contract?.outputSchema !== undefined
559+ ? { outputSchema: input.descriptor.contract.outputSchema }
560+ : {}),
561+ },
435562 providerKind: input.descriptor.providerKind,
436563 providerData: input.descriptor.providerData,
437564 },
packages/sources/mcp/src/tools.test.ts+1−1
@@ -377,7 +377,7 @@ describe("codemode-mcp", () => { · 376 unmodified lines
377377
378378 const toolDefinition = resolveToolDefinition(discovered.tools["source.mcp.echo"]!);
379379 expect(toolDefinition.metadata?.sourceKey).toBe("mcp.demo");
380− expect(JSON.stringify(toolDefinition.metadata?.inputSchema)).toContain("value");
380+ expect(JSON.stringify(toolDefinition.metadata?.contract?.inputSchema)).toContain("value");
381381
382382 const invoke = resolveToolExecutor(discovered.tools, "source.mcp.echo");
383383 const invocationResult = yield* Effect.promise(() => invoke({ value: "hello" }));
packages/sources/mcp/src/tools.ts+8−6
@@ -503,12 +503,14 @@ export const createMcpToolsFromManifest = (input: { · 502 unmodified lines
503503 },
504504 metadata: {
505505 sourceKey,
506− ...(entry.inputSchema !== undefined
507− ? { inputSchema: entry.inputSchema }
508− : {}),
509− ...(entry.outputSchema !== undefined
510− ? { outputSchema: entry.outputSchema }
511− : {}),
506+ contract: {
507+ ...(entry.inputSchema !== undefined
508+ ? { inputSchema: entry.inputSchema }
509+ : {}),
510+ ...(entry.outputSchema !== undefined
511+ ? { outputSchema: entry.outputSchema }
512+ : {}),
513+ },
512514 },
513515 }),
514516 ] as const;
packages/sources/openapi/src/extraction.real-specs.test.ts+1−1
@@ -111,7 +111,7 @@ describe("openapi-extraction real specs", () => { · 110 unmodified lines
111111
112112 for (const expectedTool of expectedTools) {
113113 const resolved = resolveToolDefinition(tools[expectedTool.expectedPath]!);
114− const inputSchema = parseInputSchema(resolved.metadata?.inputSchema);
114+ const inputSchema = parseInputSchema(resolved.metadata?.contract?.inputSchema);
115115
116116 expect(inputSchema?.type).toBe("object");
117117 expect(Object.keys(inputSchema?.properties ?? {})).toEqual(
packages/sources/openapi/src/tools.test.ts+1−1
@@ -354,7 +354,7 @@ describe("openapi-tools", () => { · 353 unmodified lines
354354 expect(resolvedGet.metadata?.sourceKey).toBe("api.demo");
355355 expect(resolvedGet.metadata?.providerKind).toBe("openapi");
356356 expect(resolvedGet.metadata?.providerData).toMatchObject({ group: "repos" });
357− expect(resolvedGet.metadata?.inputSchema).toBeDefined();
357+ expect(resolvedGet.metadata?.contract?.inputSchema).toBeDefined();
358358 }),
359359 );
360360
packages/sources/openapi/src/tools.ts+16−14
@@ -541,20 +541,22 @@ export const createOpenApiToolFromDefinition = ( · 540 unmodified lines
541541 },
542542 metadata: {
543543 sourceKey: input.sourceKey,
544− inputTypePreview: presentation.inputTypePreview,
545− outputTypePreview: presentation.outputTypePreview,
546− ...((presentation.inputSchema ?? input.definition.typing?.inputSchema) !== undefined
547− ? { inputSchema: presentation.inputSchema ?? input.definition.typing?.inputSchema }
548− : {}),
549− ...((presentation.outputSchema ?? input.definition.typing?.outputSchema) !== undefined
550− ? { outputSchema: presentation.outputSchema ?? input.definition.typing?.outputSchema }
551− : {}),
552− ...(presentation.exampleInput !== undefined
553− ? { exampleInput: presentation.exampleInput }
554− : {}),
555− ...(presentation.exampleOutput !== undefined
556− ? { exampleOutput: presentation.exampleOutput }
557− : {}),
544+ contract: {
545+ inputTypePreview: presentation.inputTypePreview,
546+ outputTypePreview: presentation.outputTypePreview,
547+ ...((presentation.inputSchema ?? input.definition.typing?.inputSchema) !== undefined
548+ ? { inputSchema: presentation.inputSchema ?? input.definition.typing?.inputSchema }
549+ : {}),
550+ ...((presentation.outputSchema ?? input.definition.typing?.outputSchema) !== undefined
551+ ? { outputSchema: presentation.outputSchema ?? input.definition.typing?.outputSchema }
552+ : {}),
553+ ...(presentation.exampleInput !== undefined
554+ ? { exampleInput: presentation.exampleInput }
555+ : {}),
556+ ...(presentation.exampleOutput !== undefined
557+ ? { exampleOutput: presentation.exampleOutput }
558+ : {}),
559+ },
558560 providerKind: "openapi",
559561 providerData: presentation.providerData,
560562 },