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
81 : undefined;81 : undefined;
82};82};
83 83
84const readBindingTransport = (source: Source): "auto" | "streamable-http" | "sse" | undefined => {84const readBindingTransport = (source: Source): "auto" | "streamable-http" | "sse" | "stdio" | undefined => {
85 const candidate = source.binding.transport;85 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"
87 ? candidate87 ? candidate
88 : undefined;88 : undefined;
89};89};
@@ -106,6 +106,10 @@ const refreshPayloadFromSource = (source: Source): ConnectSourcePayload | null = · 16 unmodified lines
106 ...(readBindingTransport(source) ? { transport: readBindingTransport(source) } : {}),106 ...(readBindingTransport(source) ? { transport: readBindingTransport(source) } : {}),
107 queryParams: readBindingStringMap(source, "queryParams"),107 queryParams: readBindingStringMap(source, "queryParams"),
108 headers: readBindingStringMap(source, "headers"),108 headers: readBindingStringMap(source, "headers"),
109 command: readBindingString(source, "command"),
110 args: readBindingStringArray(source, "args"),
111 env: readBindingStringMap(source, "env"),
112 cwd: readBindingString(source, "cwd"),
109 };113 };
110 case "openapi": {114 case "openapi": {
111 const specUrl = readBindingString(source, "specUrl");115 const specUrl = readBindingString(source, "specUrl");
@@ -364,7 +368,12 @@ function ModelView(props: { · 252 unmodified lines
364 368
365 const filteredTools = props.bundle.tools.filter((tool) => {369 const filteredTools = props.bundle.tools.filter((tool) => {
366 if (terms.length === 0) return true;370 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 ]
368 .join(" ")377 .join(" ")
369 .toLowerCase();378 .toLowerCase();
370 return terms.every((t) => corpus.includes(t));379 return terms.every((t) => corpus.includes(t));
@@ -733,6 +742,14 @@ function ToolDetailPanel(props: { detail: SourceInspectionToolDetail }) { · 362 unmodified lines
733 </span>742 </span>
734 )}743 )}
735 </div>744 </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>
736 </div>753 </div>
737 </div>754 </div>
738 </div>755 </div>
@@ -740,6 +757,66 @@ function ToolDetailPanel(props: { detail: SourceInspectionToolDetail }) { · 1 unmodified line
740 {/* Body */}757 {/* Body */}
741 <div className="flex-1 overflow-y-auto">758 <div className="flex-1 overflow-y-auto">
742 <div className="px-5 py-4 space-y-4">759 <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
743 {detail.sections.map((section, index) => {820 {detail.sections.map((section, index) => {
744 if (section.kind === "facts") {821 if (section.kind === "facts") {
745 return (822 return (
@@ -885,6 +962,30 @@ function DiscoveryView(props: { · 139 unmodified lines
885 {item.description}962 {item.description}
886 </p>963 </p>
887 )}964 )}
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 )}
888 </button>989 </button>
889 ))}990 ))}
890 </div>991 </div>
packages/kernel/core/src/discovery.ts+37−12
@@ -27,12 +27,41 @@ const searchableTextForTool = (tool: ToolDescriptor): string => · 26 unmodified lines
27 tool.path,27 tool.path,
28 tool.sourceKey,28 tool.sourceKey,
29 tool.description ?? "",29 tool.description ?? "",
30 tool.inputTypePreview ?? "",30 tool.contract?.inputTypePreview ?? "",
31 tool.outputTypePreview ?? "",31 tool.contract?.outputTypePreview ?? "",
32 ]32 ]
33 .join(" ")33 .join(" ")
34 .toLowerCase();34 .toLowerCase();
35 35
36const 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
36const projectDescriptor = (input: {65const projectDescriptor = (input: {
37 descriptor: ToolDescriptor;66 descriptor: ToolDescriptor;
38 includeSchemas: boolean;67 includeSchemas: boolean;
@@ -45,8 +74,9 @@ const projectDescriptor = (input: { · 6 unmodified lines
45 74
46 return {75 return {
47 ...descriptor,76 ...descriptor,
48 inputSchema: undefined,77 ...(descriptorContract(descriptor, false)
49 outputSchema: undefined,78 ? { contract: descriptorContract(descriptor, false) }
79 : {}),
50 };80 };
51};81};
52 82
@@ -383,13 +413,8 @@ export function createDiscoveryPrimitivesFromToolCatalog(input: { · 330 unmodified lines
383 score: hit.score,413 score: hit.score,
384 description: descriptor.description,414 description: descriptor.description,
385 interaction: descriptor.interaction ?? "auto",415 interaction: descriptor.interaction ?? "auto",
386 inputTypePreview: descriptor.inputTypePreview,416 ...(descriptorContract(descriptor, includeSchemas)
387 outputTypePreview: descriptor.outputTypePreview,417 ? { contract: descriptorContract(descriptor, includeSchemas) }
388 ...(includeSchemas
389 ? {
390 inputSchema: descriptor.inputSchema,
391 outputSchema: descriptor.outputSchema,
392 }
393 : {}),418 : {}),
394 };419 };
395 })420 })
@@ -451,7 +476,7 @@ export function buildDynamicExecuteDescription(input: { · 55 unmodified lines
451 "Workflow:",476 "Workflow:",
452 '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });',477 '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });',
453 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",478 "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.",
455 "4) Call selected tools.<path>(input).",480 "4) Call selected tools.<path>(input).",
456 "Do not use fetch; use tools.* only.",481 "Do not use fetch; use tools.* only.",
457 ].join("\n");482 ].join("\n");
packages/kernel/core/src/index.test.ts+37−13
@@ -289,16 +289,20 @@ describe("codemode-core", () => { · 288 unmodified lines
289 path: asToolPath("source.docs.search"),289 path: asToolPath("source.docs.search"),
290 sourceKey: "source.docs",290 sourceKey: "source.docs",
291 description: "Search docs",291 description: "Search docs",
292 inputTypePreview: "object",292 contract: {
293 outputTypePreview: "object",293 inputTypePreview: "object",
294 outputTypePreview: "object",
295 },
294 },296 },
295 "source.issues.create": {297 "source.issues.create": {
296 path: asToolPath("source.issues.create"),298 path: asToolPath("source.issues.create"),
297 sourceKey: "source.issues",299 sourceKey: "source.issues",
298 description: "Create issue",300 description: "Create issue",
299 interaction: "required",301 interaction: "required",
300 inputTypePreview: "object",302 contract: {
301 outputTypePreview: "object",303 inputTypePreview: "object",
304 outputTypePreview: "object",
305 },
302 },306 },
303 };307 };
304 308
@@ -327,8 +331,24 @@ describe("codemode-core", () => { · 22 unmodified lines
327 .slice(0, limit)331 .slice(0, limit)
328 .map((descriptor) => ({332 .map((descriptor) => ({
329 ...descriptor,333 ...descriptor,
330 inputSchema: includeSchemas ? descriptor.inputSchema : undefined,334 ...(descriptor.contract
331 outputSchema: includeSchemas ? descriptor.outputSchema : undefined,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 : {}),
332 })),352 })),
333 ),353 ),
334 getToolByPath: ({ path }) =>354 getToolByPath: ({ path }) =>
@@ -367,9 +387,11 @@ describe("codemode-core", () => { · 32 unmodified lines
367 path: asToolPath("github.repos.getRepo"),387 path: asToolPath("github.repos.getRepo"),
368 sourceKey: "source.github",388 sourceKey: "source.github",
369 description: "Get repository details",389 description: "Get repository details",
370 inputTypePreview: "{ owner: string; repo: string }",390 contract: {
371 outputTypePreview: "Repository",391 inputTypePreview: "{ owner: string; repo: string }",
372 inputSchema: { type: "object" },392 outputTypePreview: "Repository",
393 inputSchema: { type: "object" },
394 },
373 },395 },
374 },396 },
375 {397 {
@@ -383,8 +405,10 @@ describe("codemode-core", () => { · 7 unmodified lines
383 path: asToolPath("github.issues.list"),405 path: asToolPath("github.issues.list"),
384 sourceKey: "source.github",406 sourceKey: "source.github",
385 description: "List repository issues",407 description: "List repository issues",
386 inputTypePreview: "{ owner: string; repo: string }",408 contract: {
387 outputTypePreview: "Issue[]",409 inputTypePreview: "{ owner: string; repo: string }",
410 outputTypePreview: "Issue[]",
411 },
388 },412 },
389 },413 },
390 ],414 ],
@@ -403,7 +427,7 @@ describe("codemode-core", () => { · 12 unmodified lines
403 includeSchemas: false,427 includeSchemas: false,
404 });428 });
405 expect(listed[0]?.path).toBe("github.repos.getRepo");429 expect(listed[0]?.path).toBe("github.repos.getRepo");
406 expect(listed[0]?.inputSchema).toBeUndefined();430 expect(listed[0]?.contract?.inputSchema).toBeUndefined();
407 431
408 const discovered = yield* catalog.searchTools({432 const discovered = yield* catalog.searchTools({
409 query: "repository issues",433 query: "repository issues",
@@ -415,7 +439,7 @@ describe("codemode-core", () => { · 5 unmodified lines
415 path: asToolPath("github.repos.getRepo"),439 path: asToolPath("github.repos.getRepo"),
416 includeSchemas: true,440 includeSchemas: true,
417 });441 });
418 expect(described?.inputSchema).toEqual({ type: "object" });442 expect(described?.contract?.inputSchema).toEqual({ type: "object" });
419 }),443 }),
420 );444 );
421 445
packages/kernel/core/src/tool-map.ts+17−14
@@ -420,12 +420,26 @@ export function toolDescriptorsFromTools(input: { · 419 unmodified lines
420 const metadata = entry.metadata;420 const metadata = entry.metadata;
421 const definition = entry.tool;421 const definition = entry.tool;
422 const inputSchema =422 const inputSchema =
423 metadata?.inputSchema423 metadata?.contract?.inputSchema
424 ?? deriveSchemaValue(definition.inputSchema)424 ?? deriveSchemaValue(definition.inputSchema)
425 ?? deriveSchemaValue(definition.parameters);425 ?? deriveSchemaValue(definition.parameters);
426 const outputSchema =426 const outputSchema =
427 metadata?.outputSchema427 metadata?.contract?.outputSchema
428 ?? deriveSchemaValue(definition.outputSchema);428 ?? 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"]>;
429 443
430 return {444 return {
431 path: entry.path,445 path: entry.path,
@@ -433,18 +447,7 @@ export function toolDescriptorsFromTools(input: { · 1 unmodified line
433 description: definition.description,447 description: definition.description,
434 interaction: metadata?.interaction,448 interaction: metadata?.interaction,
435 elicitation: metadata?.elicitation,449 elicitation: metadata?.elicitation,
436 inputTypePreview:450 contract,
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 : {}),
448 ...(metadata?.providerKind ? { providerKind: metadata.providerKind } : {}),451 ...(metadata?.providerKind ? { providerKind: metadata.providerKind } : {}),
449 ...(metadata?.providerData !== undefined452 ...(metadata?.providerData !== undefined
450 ? { providerData: metadata.providerData }453 ? { providerData: metadata.providerData }
packages/kernel/core/src/types.ts+18−15
@@ -75,15 +75,28 @@ export type OnElicitation = ( · 74 unmodified lines
75 input: ToolElicitationRequest,75 input: ToolElicitationRequest,
76) => Effect.Effect<ElicitationResponse, unknown>;76) => Effect.Effect<ElicitationResponse, unknown>;
77 77
78export type ToolMetadata = {78export type ToolContract = {
79 interaction?: "auto" | "required";
80 elicitation?: ElicitationRequest;
81 inputTypePreview?: string;79 inputTypePreview?: string;
82 outputTypePreview?: string;80 outputTypePreview?: string;
83 inputSchema?: unknown;81 inputSchema?: unknown;
84 outputSchema?: unknown;82 outputSchema?: unknown;
85 exampleInput?: unknown;83 exampleInput?: unknown;
86 exampleOutput?: unknown;84 exampleOutput?: unknown;
85};
86
87export 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
96export type ToolMetadata = {
97 interaction?: "auto" | "required";
98 elicitation?: ElicitationRequest;
99 contract?: ToolContract;
87 sourceKey?: string;100 sourceKey?: string;
88 providerKind?: string;101 providerKind?: string;
89 providerData?: unknown;102 providerData?: unknown;
@@ -131,12 +144,7 @@ export type ToolDescriptor = { · 41 unmodified lines
131 description?: string;144 description?: string;
132 interaction?: "auto" | "required";145 interaction?: "auto" | "required";
133 elicitation?: ElicitationRequest;146 elicitation?: ElicitationRequest;
134 inputTypePreview?: string;147 contract?: ToolContract;
135 outputTypePreview?: string;
136 inputSchema?: unknown;
137 outputSchema?: unknown;
138 exampleInput?: unknown;
139 exampleOutput?: unknown;
140 providerKind?: string;148 providerKind?: string;
141 providerData?: unknown;149 providerData?: unknown;
142};150};
@@ -149,12 +157,7 @@ export const ToolDescriptorSchema = Schema.Struct({ · 6 unmodified lines
149 Schema.Union(Schema.Literal("auto"), Schema.Literal("required")),157 Schema.Union(Schema.Literal("auto"), Schema.Literal("required")),
150 ),158 ),
151 elicitation: Schema.optional(Schema.Unknown),159 elicitation: Schema.optional(Schema.Unknown),
152 inputTypePreview: Schema.optional(Schema.String),160 contract: Schema.optional(ToolContractSchema),
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),
158 providerKind: Schema.optional(Schema.String),161 providerKind: Schema.optional(Schema.String),
159 providerData: Schema.optional(Schema.Unknown),162 providerData: Schema.optional(Schema.Unknown),
160});163});
packages/platform/control-plane/src/runtime/catalog/catalog-typescript.ts+165−22
@@ -2,7 +2,12 @@ import { createHash } from "node:crypto"; · 1 unmodified line
2 2
3import type { ProjectedCatalog } from "@executor/ir/catalog";3import type { ProjectedCatalog } from "@executor/ir/catalog";
4import type { ShapeSymbolId } from "@executor/ir/ids";4import type { ShapeSymbolId } from "@executor/ir/ids";
5import type { CatalogV1, ShapeNode, ShapeSymbol } from "@executor/ir/model";5import type {
6 CatalogV1,
7 DocumentationBlock,
8 ShapeNode,
9 ShapeSymbol,
10} from "@executor/ir/model";
6 11
7export type CatalogTypeRoot = {12export type CatalogTypeRoot = {
8 readonly shapeId: ShapeSymbolId;13 readonly shapeId: ShapeSymbolId;
@@ -128,7 +133,9 @@ const objectTypeLiteral = ( · 119 unmodified lines
128 133
129 return [134 return [
130 "{",135 "{",
131 ...lines.map((line) => `${indent}${line}`),136 ...lines.flatMap((line) =>
137 line.split("\n").map((segment) => `${indent}${segment}`)
138 ),
132 `${indent.slice(0, -2)}}`,139 `${indent.slice(0, -2)}}`,
133 ].join("\n");140 ].join("\n");
134};141};
@@ -153,6 +160,73 @@ const isSyntheticShapeLabel = (value: string): boolean => · 18 unmodified lines
153 160
154const looksLikeHumanPhrase = (value: string): boolean => /\s/.test(value.trim());161const looksLikeHumanPhrase = (value: string): boolean => /\s/.test(value.trim());
155 162
163const cleanDocText = (value: string | undefined): string | null => {
164 const trimmed = value?.trim();
165 return trimmed && trimmed.length > 0 ? trimmed : null;
166};
167
168const escapeJsDocText = (value: string): string => value.replace(/\*\//g, "*\\/");
169
170const 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
187export 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
156const compactAliasHint = (value: string): string => {230const compactAliasHint = (value: string): string => {
157 const segments = typeNameWords(value);231 const segments = typeNameWords(value);
158 if (segments.length <= 5) {232 if (segments.length <= 5) {
@@ -254,16 +328,27 @@ const renderObjectNode = ( · 95 unmodified lines
254 stack: readonly ShapeSymbolId[],328 stack: readonly ShapeSymbolId[],
255 aliasHint: string | undefined,329 aliasHint: string | undefined,
256 renderShape: RenderShape,330 renderShape: RenderShape,
331 includeDocs: boolean,
257): string => {332): string => {
258 const required = new Set(node.required);333 const required = new Set(node.required);
259 const lines = Object.keys(node.fields)334 const lines = Object.keys(node.fields)
260 .sort((left, right) => left.localeCompare(right))335 .sort((left, right) => left.localeCompare(right))
261 .map((key) =>336 .map((key) => {
262 `${formatPropertyKey(key)}${required.has(key) ? "" : "?"}: ${renderShape(node.fields[key]!.shapeId, {337 const field = node.fields[key]!;
263 stack,338 const propertyLine =
264 aliasHint: aliasHint ? joinTypeNameSegments(aliasHint, key) : key,339 `${formatPropertyKey(key)}${required.has(key) ? "" : "?"}: ${renderShape(field.shapeId, {
265 })};`340 stack,
266 );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 });
267 352
268 const patternShapeIds = Object.values(node.patternProperties ?? {});353 const patternShapeIds = Object.values(node.patternProperties ?? {});
269 const hasUnknownIndex = node.additionalProperties === true;354 const hasUnknownIndex = node.additionalProperties === true;
@@ -290,6 +375,7 @@ const renderObjectFields = ( · 20 unmodified lines
290 stack: readonly ShapeSymbolId[],375 stack: readonly ShapeSymbolId[],
291 aliasHint: string | undefined,376 aliasHint: string | undefined,
292 renderShape: RenderShape,377 renderShape: RenderShape,
378 includeDocs: boolean,
293): string =>379): string =>
294 renderObjectNode(380 renderObjectNode(
295 {381 {
@@ -301,6 +387,7 @@ const renderObjectFields = ( · 5 unmodified lines
301 stack,387 stack,
302 aliasHint,388 aliasHint,
303 renderShape,389 renderShape,
390 includeDocs,
304 );391 );
305 392
306const renderShapeBody = (393const renderShapeBody = (
@@ -309,6 +396,7 @@ const renderShapeBody = ( · 2 unmodified lines
309 stack: readonly ShapeSymbolId[],396 stack: readonly ShapeSymbolId[],
310 aliasHint: string | undefined,397 aliasHint: string | undefined,
311 renderShape: RenderShape,398 renderShape: RenderShape,
399 includeDocs: boolean,
312): string => {400): string => {
313 const shape = getShapeSymbol(catalog, shapeId);401 const shape = getShapeSymbol(catalog, shapeId);
314 const node = shape.node;402 const node = shape.node;
@@ -321,7 +409,7 @@ const renderShapeBody = ( · 6 unmodified lines
321 return renderInlineShapeNode(node);409 return renderInlineShapeNode(node);
322 case "object":410 case "object":
323 case "graphqlInterface":411 case "graphqlInterface":
324 return renderObjectFields(node, stack, aliasHint, renderShape);412 return renderObjectFields(node, stack, aliasHint, renderShape, includeDocs);
325 case "array":413 case "array":
326 return `Array<${wrapCompositeType(renderShape(node.itemShapeId, {414 return `Array<${wrapCompositeType(renderShape(node.itemShapeId, {
327 stack,415 stack,
@@ -714,6 +802,7 @@ export const createCatalogTypeProjector = (input: { · 386 unmodified lines
714 stack: readonly ShapeSymbolId[],802 stack: readonly ShapeSymbolId[],
715 aliasHint: string | undefined,803 aliasHint: string | undefined,
716 renderShape: RenderShape,804 renderShape: RenderShape,
805 includeDocs: boolean,
717 ): string | null => {806 ): string | null => {
718 const variants = shapeIds.map((shapeId) => resolveUnionVariantObject(shapeId));807 const variants = shapeIds.map((shapeId) => resolveUnionVariantObject(shapeId));
719 if (variants.some((variant) => variant === null)) {808 if (variants.some((variant) => variant === null)) {
@@ -769,7 +858,7 @@ export const createCatalogTypeProjector = (input: { · 49 unmodified lines
769 }858 }
770 859
771 const baseText = baseHasSharedStructure860 const baseText = baseHasSharedStructure
772 ? renderObjectNode(sharedNode, stack, aliasHint, renderShape)861 ? renderObjectNode(sharedNode, stack, aliasHint, renderShape, includeDocs)
773 : null;862 : null;
774 863
775 const variantTexts = objectVariants.map((variant, index) => {864 const variantTexts = objectVariants.map((variant, index) => {
@@ -786,6 +875,7 @@ export const createCatalogTypeProjector = (input: { · 10 unmodified lines
786 stack,875 stack,
787 aliasHint ? joinTypeNameSegments(aliasHint, variantAliasLabel(discriminator, index)) : variantAliasLabel(discriminator, index),876 aliasHint ? joinTypeNameSegments(aliasHint, variantAliasLabel(discriminator, index)) : variantAliasLabel(discriminator, index),
788 renderShape,877 renderShape,
878 includeDocs,
789 );879 );
790 });880 });
791 const unionText = variantTexts.map((variantText) => wrapCompositeType(variantText)).join(" | ");881 const unionText = variantTexts.map((variantText) => wrapCompositeType(variantText)).join(" | ");
@@ -857,15 +947,21 @@ export const createCatalogTypeProjector = (input: { · 65 unmodified lines
857 switch (shape.node.type) {947 switch (shape.node.type) {
858 case "anyOf":948 case "anyOf":
859 case "oneOf": {949 case "oneOf": {
860 const normalized = normalizedUnionRender(shape.node.items, stack, aliasHint, renderDeclarationShape);950 const normalized = normalizedUnionRender(
861 return normalized ?? renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape);951 shape.node.items,
952 stack,
953 aliasHint,
954 renderDeclarationShape,
955 true,
956 );
957 return normalized ?? renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape, true);
862 }958 }
863 case "graphqlUnion": {959 case "graphqlUnion": {
864 const normalized = normalizedUnionRender(shape.node.memberTypeIds, stack, aliasHint, renderDeclarationShape);960 const normalized = normalizedUnionRender(shape.node.memberTypeIds, stack, aliasHint, renderDeclarationShape, true);
865 return normalized ?? renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape);961 return normalized ?? renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape, true);
866 }962 }
867 default:963 default:
868 return renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape);964 return renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape, true);
869 }965 }
870 };966 };
871 967
@@ -921,16 +1017,49 @@ export const createCatalogTypeProjector = (input: { · 49 unmodified lines
921 switch (shape.node.type) {1017 switch (shape.node.type) {
922 case "anyOf":1018 case "anyOf":
923 case "oneOf": {1019 case "oneOf": {
924 const normalized = normalizedUnionRender(shape.node.items, [...stack, shapeId], options.aliasHint, renderSelfContainedShape);1020 const normalized = normalizedUnionRender(
925 return normalized ?? renderShapeBody(catalog, shapeId, [...stack, shapeId], options.aliasHint, renderSelfContainedShape);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 );
926 }1035 }
927 case "graphqlUnion": {1036 case "graphqlUnion": {
928 const normalized = normalizedUnionRender(shape.node.memberTypeIds, [...stack, shapeId], options.aliasHint, renderSelfContainedShape);1037 const normalized = normalizedUnionRender(
929 return normalized ?? renderShapeBody(catalog, shapeId, [...stack, shapeId], options.aliasHint, renderSelfContainedShape);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 );
930 }1052 }
931 }1053 }
932 1054
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 );
934 };1063 };
935 1064
936 const supportingDeclarations = (): readonly string[] => {1065 const supportingDeclarations = (): readonly string[] => {
@@ -960,11 +1089,25 @@ export const createCatalogTypeProjector = (input: { · 23 unmodified lines
960 const canonicalType = canonicalTypeForSignature(signature);1089 const canonicalType = canonicalTypeForSignature(signature);
961 const representativeShapeId = representativeShapeIdForSignature(signature);1090 const representativeShapeId = representativeShapeIdForSignature(signature);
962 const aliasName = aliasNameForSignature(signature);1091 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 });
963 if (canonicalType !== aliasName) {1099 if (canonicalType !== aliasName) {
964 return `type ${aliasName} = ${canonicalType};`;1100 return comment
1101 ? `${comment}\ntype ${aliasName} = ${canonicalType};`
1102 : `type ${aliasName} = ${canonicalType};`;
965 }1103 }
966 const body = renderDeclarationShapeBody(representativeShapeId, [representativeShapeId], aliasName);1104 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;
968 })1111 })
969 .filter((declaration) => declaration.length > 0);1112 .filter((declaration) => declaration.length > 0);
970 };1113 };
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
3import parserEstree from "prettier/plugins/estree";3import parserEstree from "prettier/plugins/estree";
4import parserTypescript from "prettier/plugins/typescript";4import parserTypescript from "prettier/plugins/typescript";
5 5
6export type PrettierParser = "json" | "typescript";6export type PrettierParser = "json" | "typescript" | "typescript-module";
7 7
8const plugins = [parserBabel, parserEstree, parserTypescript];8const plugins = [parserBabel, parserEstree, parserTypescript];
9 9
@@ -31,7 +31,7 @@ export async function formatWithPrettier( · 21 unmodified lines
31 }31 }
32 32
33 const result = await format(input, {33 const result = await format(input, {
34 parser,34 parser: parser === "typescript-module" ? "typescript" : parser,
35 plugins,35 plugins,
36 printWidth: 60,36 printWidth: 60,
37 tabWidth: 2,37 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
353 });353 });
354 354
355 expect(tool).toBeDefined();355 expect(tool).toBeDefined();
356 expect(tool?.descriptor.inputSchema).toMatchObject({356 expect(tool?.descriptor.contract?.inputSchema).toMatchObject({
357 type: "object",357 type: "object",
358 properties: {358 properties: {
359 filter: {359 filter: {
@@ -368,11 +368,11 @@ describe("source-catalog-runtime", () => { · 8 unmodified lines
368 },368 },
369 });369 });
370 370
371 const serializedInput = JSON.stringify(tool?.descriptor.inputSchema);371 const serializedInput = JSON.stringify(tool?.descriptor.contract?.inputSchema);
372 expect(serializedInput).not.toContain("\"$ref\":\"#/$defs/shape_");372 expect(serializedInput).not.toContain("\"$ref\":\"#/$defs/shape_");
373 expect(serializedInput).not.toContain("\"shape_");373 expect(serializedInput).not.toContain("\"shape_");
374 expect(tool?.descriptor.inputTypePreview).toContain("filter?: {");374 expect(tool?.descriptor.contract?.inputTypePreview).toContain("filter?: {");
375 expect(tool?.descriptor.outputTypePreview).toContain("nodes?: {");375 expect(tool?.descriptor.contract?.outputTypePreview).toContain("nodes?: {");
376 }));376 }));
377 377
378 it.effect("can skip type previews when only list metadata is needed", () =>378 it.effect("can skip type previews when only list metadata is needed", () =>
@@ -384,10 +384,10 @@ describe("source-catalog-runtime", () => { · 5 unmodified lines
384 });384 });
385 385
386 expect(tool).toBeDefined();386 expect(tool).toBeDefined();
387 expect(tool?.descriptor.inputSchema).toBeUndefined();387 expect(tool?.descriptor.contract?.inputSchema).toBeUndefined();
388 expect(tool?.descriptor.outputSchema).toBeUndefined();388 expect(tool?.descriptor.contract?.outputSchema).toBeUndefined();
389 expect(tool?.descriptor.inputTypePreview).toBeUndefined();389 expect(tool?.descriptor.contract?.inputTypePreview).toBeUndefined();
390 expect(tool?.descriptor.outputTypePreview).toBeUndefined();390 expect(tool?.descriptor.contract?.outputTypePreview).toBeUndefined();
391 }));391 }));
392 392
393 it.effect("projects a single tool by path without expanding the whole catalog", () =>393 it.effect("projects a single tool by path without expanding the whole catalog", () =>
@@ -400,7 +400,7 @@ describe("source-catalog-runtime", () => { · 6 unmodified lines
400 400
401 expect(tool).not.toBeNull();401 expect(tool).not.toBeNull();
402 expect(tool?.path).toBe("linear.teams");402 expect(tool?.path).toBe("linear.teams");
403 expect(tool?.descriptor.inputSchema).toMatchObject({403 expect(tool?.descriptor.contract?.inputSchema).toMatchObject({
404 type: "object",404 type: "object",
405 properties: {405 properties: {
406 filter: {406 filter: {
@@ -569,12 +569,12 @@ describe("source-catalog-runtime", () => { · 162 unmodified lines
569 });569 });
570 570
571 expect(tool).not.toBeNull();571 expect(tool).not.toBeNull();
572 expect(tool?.descriptor.outputTypePreview).toContain("actor?: string;");572 expect(tool?.descriptor.contract?.outputTypePreview).toContain("actor?: string;");
573 expect(tool?.descriptor.outputTypePreview).toContain('action: "blocked";');573 expect(tool?.descriptor.contract?.outputTypePreview).toContain('action: "blocked";');
574 expect(tool?.descriptor.outputTypePreview).toContain('action: "unblocked";');574 expect(tool?.descriptor.contract?.outputTypePreview).toContain('action: "unblocked";');
575 expect(tool?.descriptor.outputTypePreview).toContain('action: "route-blocked";');575 expect(tool?.descriptor.contract?.outputTypePreview).toContain('action: "route-blocked";');
576 expect(tool?.descriptor.outputTypePreview).toContain("& (");576 expect(tool?.descriptor.contract?.outputTypePreview).toContain("& (");
577 expect(tool?.descriptor.outputTypePreview).not.toContain("Member2");577 expect(tool?.descriptor.contract?.outputTypePreview).not.toContain("Member2");
578 }));578 }));
579 579
580 it.effect("uses the shared TS projector for unsupported preview shapes", () =>580 it.effect("uses the shared TS projector for unsupported preview shapes", () =>
@@ -622,6 +622,6 @@ describe("source-catalog-runtime", () => { · 41 unmodified lines
622 });622 });
623 623
624 expect(tool).not.toBeNull();624 expect(tool).not.toBeNull();
625 expect(tool?.descriptor.inputTypePreview).toContain("unsupported?: unknown;");625 expect(tool?.descriptor.contract?.inputTypePreview).toContain("unsupported?: unknown;");
626 }));626 }));
627});627});
packages/platform/control-plane/src/runtime/catalog/source/runtime.ts+231−4
@@ -19,6 +19,7 @@ import { · 18 unmodified lines
19 projectCatalogForAgentSdk,19 projectCatalogForAgentSdk,
20 type ProjectedCatalog,20 type ProjectedCatalog,
21} from "@executor/ir/catalog";21} from "@executor/ir/catalog";
22import type { ShapeSymbolId } from "@executor/ir/ids";
22import type {23import type {
23 Capability,24 Capability,
24 CatalogSnapshotV1,25 CatalogSnapshotV1,
@@ -29,10 +30,13 @@ import type { · 4 unmodified lines
29import { LocalSourceArtifactMissingError } from "../../local/errors";30import { LocalSourceArtifactMissingError } from "../../local/errors";
30import {31import {
31 createCatalogTypeProjector,32 createCatalogTypeProjector,
33 documentationComment,
32 joinTypeNameSegments,34 joinTypeNameSegments,
33 projectedCatalogTypeRoots,35 projectedCatalogTypeRoots,
36 shapeAllowsOmittedArgs,
34 type CatalogTypeProjector,37 type CatalogTypeProjector,
35} from "../catalog-typescript";38} from "../catalog-typescript";
39import { formatWithPrettier } from "../prettier-format";
36import {40import {
37 RuntimeLocalWorkspaceService,41 RuntimeLocalWorkspaceService,
38 type RuntimeLocalWorkspaceState,42 type RuntimeLocalWorkspaceState,
@@ -85,6 +89,24 @@ export type LoadedSourceCatalogToolIndexEntry = Omit< · 46 unmodified lines
85 "revision" | "projectedDescriptor" | "typeProjector"89 "revision" | "projectedDescriptor" | "typeProjector"
86>;90>;
87 91
92export type LoadedSourceCatalogToolContractSide = {
93 shapeId: string | null;
94 typePreview: string | null;
95 typeDeclaration: string | null;
96 schemaJson: string | null;
97 exampleJson: string | null;
98};
99
100export 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
88export const catalogToolCatalogEntry = (input: {110export const catalogToolCatalogEntry = (input: {
89 tool: LoadedSourceCatalogToolIndexEntry;111 tool: LoadedSourceCatalogToolIndexEntry;
90 score: (queryTokens: readonly string[]) => number;112 score: (queryTokens: readonly string[]) => number;
@@ -102,6 +124,14 @@ const catalogNamespaceFromPath = (path: string): string => { · 11 unmodified lines
102 124
103const descriptorPath = (descriptor: CatalogToolDescriptor): string => descriptor.path;125const descriptorPath = (descriptor: CatalogToolDescriptor): string => descriptor.path;
104 126
127const 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
105const projectedToolPath = (projected: ProjectedCatalog, capability: Capability): string =>135const projectedToolPath = (projected: ProjectedCatalog, capability: Capability): string =>
106 projected.toolDescriptors[capability.id]?.toolPath.join(".") ?? "";136 projected.toolDescriptors[capability.id]?.toolPath.join(".") ?? "";
107 137
@@ -535,10 +565,12 @@ const codemodeDescriptorFromCapability = (input: { · 427 unmodified lines
535 sourceKey: input.source.id,565 sourceKey: input.source.id,
536 description: input.capability.surface.summary ?? input.capability.surface.description,566 description: input.capability.surface.summary ?? input.capability.surface.description,
537 interaction,567 interaction,
538 ...(inputSchema !== undefined ? { inputSchema } : {}),568 contract: {
539 ...(outputSchema !== undefined ? { outputSchema } : {}),569 inputTypePreview,
540 inputTypePreview,570 ...(outputTypePreview !== undefined ? { outputTypePreview } : {}),
541 ...(outputTypePreview !== undefined ? { outputTypePreview } : {}),571 ...(inputSchema !== undefined ? { inputSchema } : {}),
572 ...(outputSchema !== undefined ? { outputSchema } : {}),
573 },
542 providerKind: input.executable.adapterKey,574 providerKind: input.executable.adapterKey,
543 providerData: {575 providerData: {
544 capabilityId: input.capability.id,576 capabilityId: input.capability.id,
@@ -576,6 +608,8 @@ const loadedCatalogToolFromCapability = (input: { · 31 unmodified lines
576 input.capability.surface.title,608 input.capability.surface.title,
577 input.capability.surface.summary,609 input.capability.surface.summary,
578 input.capability.surface.description,610 input.capability.surface.description,
611 descriptor.contract?.inputTypePreview,
612 descriptor.contract?.outputTypePreview,
579 ...(searchDoc?.tags ?? []),613 ...(searchDoc?.tags ?? []),
580 ...(searchDoc?.protocolHints ?? []),614 ...(searchDoc?.protocolHints ?? []),
581 ...(searchDoc?.authHints ?? []),615 ...(searchDoc?.authHints ?? []),
@@ -853,6 +887,199 @@ export const expandCatalogTools = (input: { · 271 unmodified lines
853 ),887 ),
854 );888 );
855 889
890const 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
929const 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
940const 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
953const lowerCamelCase = (value: string): string =>
954 value.length === 0 ? "tool" : `${value.slice(0, 1).toLowerCase()}${value.slice(1)}`;
955
956const 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
975export 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
856export const expandCatalogToolByPath = (input: {1083export const expandCatalogToolByPath = (input: {
857 catalogs: readonly LoadedSourceCatalog[];1084 catalogs: readonly LoadedSourceCatalog[];
858 path: string;1085 path: string;
packages/platform/control-plane/src/runtime/sources/executor-tools.ts+13−4
@@ -186,6 +186,13 @@ const prepareExecutorAddSourceInput = (input: { · 185 unmodified lines
186 endpoint: input.args.endpoint,186 endpoint: input.args.endpoint,
187 name: input.args.name ?? null,187 name: input.args.name ?? null,
188 namespace: input.args.namespace ?? null,188 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,
189 workspaceId: input.workspaceId,196 workspaceId: input.workspaceId,
190 actorAccountId: input.accountId,197 actorAccountId: input.accountId,
191 executionId: input.executionId,198 executionId: input.executionId,
@@ -468,10 +475,12 @@ export const createExecutorToolMap = (input: { · 276 unmodified lines
468 },475 },
469 },476 },
470 metadata: {477 metadata: {
471 inputTypePreview: EXECUTOR_SOURCES_ADD_INPUT_HINT,478 contract: {
472 outputTypePreview: EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE,479 inputTypePreview: EXECUTOR_SOURCES_ADD_INPUT_HINT,
473 inputSchema: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA,480 outputTypePreview: EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE,
474 outputSchema: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA,481 inputSchema: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA,
482 outputSchema: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA,
483 },
475 sourceKey: "executor",484 sourceKey: "executor",
476 interaction: "auto",485 interaction: "auto",
477 },486 },
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
362 Object.keys(snapshot.catalog.capabilities).length,362 Object.keys(snapshot.catalog.capabilities).length,
363 ).toBeGreaterThan(250);363 ).toBeGreaterThan(250);
364 expect(tool).toBeDefined();364 expect(tool).toBeDefined();
365 expect(tool?.descriptor.inputTypePreview).not.toContain("unknown");365 expect(tool?.descriptor.contract?.inputTypePreview).not.toContain("unknown");
366 expect(tool?.descriptor.outputTypePreview).toContain("data:");366 expect(tool?.descriptor.contract?.outputTypePreview).toContain("data:");
367 expect(tool?.descriptor.outputTypePreview).toContain("status:");367 expect(tool?.descriptor.contract?.outputTypePreview).toContain("status:");
368 expect(tool?.descriptor.inputSchema).toMatchObject({368 expect(tool?.descriptor.contract?.inputSchema).toMatchObject({
369 type: "object",369 type: "object",
370 properties: {370 properties: {
371 idOrName: {371 idOrName: {
@@ -428,8 +428,8 @@ describe("source adapter fixture matrix", () => { · 56 unmodified lines
428 428
429 expect(manifest.tools.length).toBeGreaterThan(50);429 expect(manifest.tools.length).toBeGreaterThan(50);
430 expect(tool).toBeDefined();430 expect(tool).toBeDefined();
431 expect(tool?.descriptor.inputTypePreview).toContain("key_name");431 expect(tool?.descriptor.contract?.inputTypePreview).toContain("key_name");
432 expect(tool?.descriptor.inputSchema).toMatchObject({432 expect(tool?.descriptor.contract?.inputSchema).toMatchObject({
433 type: "object",433 type: "object",
434 properties: {434 properties: {
435 body: {435 body: {
@@ -444,7 +444,7 @@ describe("source adapter fixture matrix", () => { · 8 unmodified lines
444 },444 },
445 required: ["body"],445 required: ["body"],
446 });446 });
447 expect(tool?.descriptor.outputSchema).toMatchObject({447 expect(tool?.descriptor.contract?.outputSchema).toMatchObject({
448 type: "object",448 type: "object",
449 properties: {449 properties: {
450 data: {450 data: {
@@ -452,7 +452,7 @@ describe("source adapter fixture matrix", () => { · 1 unmodified line
452 },452 },
453 },453 },
454 });454 });
455 const outputSchema = tool?.descriptor.outputSchema;455 const outputSchema = tool?.descriptor.contract?.outputSchema;
456 const dataVariants =456 const dataVariants =
457 outputSchema && typeof outputSchema === "object"457 outputSchema && typeof outputSchema === "object"
458 ? (458 ? (
@@ -979,10 +979,10 @@ describe("source adapter fixture matrix", () => { · 520 unmodified lines
979 Object.keys(snapshot.catalog.capabilities).length,979 Object.keys(snapshot.catalog.capabilities).length,
980 ).toBeGreaterThan(10);980 ).toBeGreaterThan(10);
981 expect(tool).toBeDefined();981 expect(tool).toBeDefined();
982 expect(tool?.descriptor.inputTypePreview).not.toContain("unknown");982 expect(tool?.descriptor.contract?.inputTypePreview).not.toContain("unknown");
983 expect(tool?.descriptor.outputTypePreview).toContain("data:");983 expect(tool?.descriptor.contract?.outputTypePreview).toContain("data:");
984 expect(tool?.descriptor.outputTypePreview).toContain("status:");984 expect(tool?.descriptor.contract?.outputTypePreview).toContain("status:");
985 expect(tool?.descriptor.inputSchema).toMatchObject({985 expect(tool?.descriptor.contract?.inputSchema).toMatchObject({
986 type: "object",986 type: "object",
987 properties: {987 properties: {
988 spreadsheetId: {988 spreadsheetId: {
@@ -1001,7 +1001,7 @@ describe("source adapter fixture matrix", () => { · 12 unmodified lines
1001 },1001 },
1002 },1002 },
1003 });1003 });
1004 expect(JSON.stringify(tool?.descriptor.outputSchema)).toContain(1004 expect(JSON.stringify(tool?.descriptor.contract?.outputSchema)).toContain(
1005 '"gridProperties"',1005 '"gridProperties"',
1006 );1006 );
1007 expect(1007 expect(
@@ -1111,11 +1111,11 @@ describe("source adapter fixture matrix", () => { · 103 unmodified lines
1111 ),1111 ),
1112 ).toEqual([]);1112 ).toEqual([]);
1113 expect(tool).toBeDefined();1113 expect(tool).toBeDefined();
1114 expect(tool?.descriptor.inputTypePreview).toContain("{");1114 expect(tool?.descriptor.contract?.inputTypePreview).toContain("{");
1115 expect(tool?.descriptor.inputTypePreview).toContain("input: {");1115 expect(tool?.descriptor.contract?.inputTypePreview).toContain("input: {");
1116 expect(tool?.descriptor.outputTypePreview).toContain("data:");1116 expect(tool?.descriptor.contract?.outputTypePreview).toContain("data:");
1117 expect(tool?.descriptor.outputTypePreview).not.toContain("unknown[]");1117 expect(tool?.descriptor.contract?.outputTypePreview).not.toContain("unknown[]");
1118 expect(tool?.descriptor.inputSchema).toMatchObject({1118 expect(tool?.descriptor.contract?.inputSchema).toMatchObject({
1119 type: "object",1119 type: "object",
1120 required: ["input"],1120 required: ["input"],
1121 properties: {1121 properties: {
@@ -1303,7 +1303,7 @@ describe("source adapter fixture matrix", () => { · 181 unmodified lines
1303 );1303 );
1304 }1304 }
1305 1305
1306 expect(tool.descriptor.outputSchema).toMatchObject({1306 expect(tool.descriptor.contract?.outputSchema).toMatchObject({
1307 type: "object",1307 type: "object",
1308 properties: {1308 properties: {
1309 data: {1309 data: {
packages/platform/control-plane/src/runtime/sources/source-inspection.test.ts+218−20
@@ -1,11 +1,13 @@
1import { describe, expect, it } from "@effect/vitest";1import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
2 3
3import type { LoadedSourceCatalogTool } from "../catalog/source/runtime";4import type { LoadedSourceCatalogTool } from "../catalog/source/runtime";
4import { inspectionToolDetailFromTool } from "./source-inspection";5import { inspectionToolDetailFromTool } from "./source-inspection";
5 6
6describe("source inspection", () => {7describe("source inspection", () => {
7 it("keeps tool detail lightweight by default", () => {8 it.effect("keeps the shared TypeScript contract first-class on tool detail", () =>
8 const detail = inspectionToolDetailFromTool({9 Effect.gen(function* () {
10 const detail = yield* inspectionToolDetailFromTool({
9 path: "linear.administrableTeams",11 path: "linear.administrableTeams",
10 source: {12 source: {
11 id: "linear",13 id: "linear",
@@ -30,7 +32,9 @@ describe("source inspection", () => { · 18 unmodified lines
30 },32 },
31 projection: {33 projection: {
32 callShapeId: "shape_call",34 callShapeId: "shape_call",
33 resultShapeId: "shape_result",35 resultDataShapeId: "shape_result_data",
36 resultErrorShapeId: "shape_result_error",
37 responseSetId: "response_set_1",
34 },38 },
35 display: {39 display: {
36 protocol: "graphql",40 protocol: "graphql",
@@ -47,26 +51,220 @@ describe("source inspection", () => { · 10 unmodified lines
47 toolPath: ["linear", "administrableTeams"],51 toolPath: ["linear", "administrableTeams"],
48 callShapeId: "shape_call",52 callShapeId: "shape_call",
49 resultShapeId: "shape_result",53 resultShapeId: "shape_result",
54 responseSetId: "response_set_1",
50 },55 },
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"],
52 } as LoadedSourceCatalogTool);238 } as LoadedSourceCatalogTool);
53 239
54 expect(detail.summary.path).toBe("linear.administrableTeams");240 expect(detail.summary.path).toBe("linear.administrableTeams");
55 expect(detail.summary.method).toBe("query");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\"");
56 255
57 const sectionTitles = detail.sections.map((section) => section.title);256 const sectionTitles = detail.sections.map((section) => section.title);
58 expect(sectionTitles).not.toContain("Input Type");257 expect(sectionTitles).not.toContain("Input Schema");
59 expect(sectionTitles).not.toContain("Output Type");258 expect(sectionTitles).not.toContain("Output Schema");
60 expect(sectionTitles).not.toContain("Input Schema");
61 expect(sectionTitles).not.toContain("Output Schema");
62 259
63 const overview = detail.sections.find((section) => section.kind === "facts");260 const overview = detail.sections.find((section) => section.kind === "facts");
64 expect(overview).toBeDefined();261 expect(overview).toBeDefined();
65 expect(overview?.items).toEqual(262 expect(overview?.items).toEqual(
66 expect.arrayContaining([263 expect.arrayContaining([
67 expect.objectContaining({ label: "Call shape", value: "shape_call" }),264 expect.objectContaining({ label: "Signature" }),
68 expect.objectContaining({ label: "Result shape", value: "shape_result" }),265 expect.objectContaining({ label: "Call shape", value: "shape_call" }),
69 ]),266 expect.objectContaining({ label: "Result shape", value: "shape_result" }),
70 );267 ]),
71 });268 );
269 }));
72});270});
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
19import { LocalSourceArtifactMissingError } from "../local/errors";19import { LocalSourceArtifactMissingError } from "../local/errors";
20import { operationErrors } from "../policy/operation-errors";20import { operationErrors } from "../policy/operation-errors";
21import {21import {
22 buildLoadedSourceCatalogToolContract,
22 expandCatalogToolByPath,23 expandCatalogToolByPath,
23 expandCatalogTools,24 expandCatalogTools,
24 loadSourceWithCatalog,25 loadSourceWithCatalog,
@@ -107,6 +108,12 @@ const executableDetails = (tool: LoadedSourceCatalogTool) => { · 82 unmodified lines
107const inspectionToolListItemFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolListItem => ({108const inspectionToolListItemFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolListItem => ({
108 path: tool.path,109 path: tool.path,
109 method: executableDetails(tool).method,110 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 : {}),
110});117});
111 118
112const persistedToolSummaryFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolSummary => {119const persistedToolSummaryFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolSummary => {
@@ -128,11 +135,11 @@ const persistedToolSummaryFromTool = (tool: LoadedSourceCatalogTool): SourceInsp · 15 unmodified lines
128 tags: [...details.tags],135 tags: [...details.tags],
129 method: details.method,136 method: details.method,
130 pathTemplate: details.pathTemplate,137 pathTemplate: details.pathTemplate,
131 ...(tool.descriptor.inputTypePreview138 ...(tool.descriptor.contract?.inputTypePreview
132 ? { inputTypePreview: tool.descriptor.inputTypePreview }139 ? { inputTypePreview: tool.descriptor.contract.inputTypePreview }
133 : {}),140 : {}),
134 ...(tool.descriptor.outputTypePreview141 ...(tool.descriptor.contract?.outputTypePreview
135 ? { outputTypePreview: tool.descriptor.outputTypePreview }142 ? { outputTypePreview: tool.descriptor.contract.outputTypePreview }
136 : {}),143 : {}),
137 };144 };
138};145};
@@ -157,85 +164,89 @@ const jsonSection = ( · 18 unmodified lines
157 164
158export const inspectionToolDetailFromTool = (165export const inspectionToolDetailFromTool = (
159 tool: LoadedSourceCatalogTool,166 tool: LoadedSourceCatalogTool,
160): SourceInspectionToolDetail => {167): Effect.Effect<SourceInspectionToolDetail, Error, never> =>
161 const summary = persistedToolSummaryFromTool(tool);168 Effect.gen(function* () {
162 const details = executableDetails(tool);169 const summary = persistedToolSummaryFromTool(tool);
163 const overviewItems = [170 const details = executableDetails(tool);
164 { label: "Protocol", value: details.protocol },171 const contract = yield* buildLoadedSourceCatalogToolContract(tool);
165 ...(details.method ? [{ label: "Method", value: details.method, mono: true }] : []),172 const overviewItems = [
166 ...(details.pathTemplate ? [{ label: "Target", value: details.pathTemplate, mono: true }] : []),173 { label: "Protocol", value: details.protocol },
167 ...(details.operationId ? [{ label: "Operation", value: details.operationId, mono: true }] : []),174 ...(details.method ? [{ label: "Method", value: details.method, mono: true }] : []),
168 ...(details.group ? [{ label: "Group", value: details.group, mono: true }] : []),175 ...(details.pathTemplate ? [{ label: "Target", value: details.pathTemplate, mono: true }] : []),
169 ...(details.leaf ? [{ label: "Leaf", value: details.leaf, mono: true }] : []),176 ...(details.operationId ? [{ label: "Operation", value: details.operationId, mono: true }] : []),
170 ...(details.rawToolId ? [{ label: "Raw tool", value: details.rawToolId, mono: true }] : []),177 ...(details.group ? [{ label: "Group", value: details.group, mono: true }] : []),
171 { label: "Call shape", value: tool.projectedDescriptor.callShapeId, mono: true },178 ...(details.leaf ? [{ label: "Leaf", value: details.leaf, mono: true }] : []),
172 ...(tool.projectedDescriptor.resultShapeId179 ...(details.rawToolId ? [{ label: "Raw tool", value: details.rawToolId, mono: true }] : []),
173 ? [{ label: "Result shape", value: tool.projectedDescriptor.resultShapeId, mono: true }]180 { label: "Signature", value: contract.callSignature, mono: true },
174 : []),181 { label: "Call shape", value: contract.callShapeId, mono: true },
175 ];182 ...(contract.resultShapeId
176 const nativeSections = [183 ? [{ label: "Result shape", value: contract.resultShapeId, mono: true }]
177 ...(tool.capability.native ?? []).map((blob, index) => ({184 : []),
178 kind: "code" as const,185 { label: "Response set", value: contract.responseSetId, mono: true },
179 title: `Capability native ${String(index + 1)}: ${blob.kind}`,186 ];
180 language: nativeEncodingLanguage(blob.encoding),187 const nativeSections = [
181 body:188 ...(tool.capability.native ?? []).map((blob, index) => ({
182 typeof blob.value === "string"189 kind: "code" as const,
183 ? blob.value190 title: `Capability native ${String(index + 1)}: ${blob.kind}`,
184 : JSON.stringify(blob.value ?? null, null, 2),191 language: nativeEncodingLanguage(blob.encoding),
185 })),192 body:
186 ...(tool.executable.native ?? []).map((blob, index) => ({193 typeof blob.value === "string"
187 kind: "code" as const,194 ? blob.value
188 title: `Executable native ${String(index + 1)}: ${blob.kind}`,195 : JSON.stringify(blob.value ?? null, null, 2),
189 language: nativeEncodingLanguage(blob.encoding),196 })),
190 body:197 ...(tool.executable.native ?? []).map((blob, index) => ({
191 typeof blob.value === "string"198 kind: "code" as const,
192 ? blob.value199 title: `Executable native ${String(index + 1)}: ${blob.kind}`,
193 : JSON.stringify(blob.value ?? null, null, 2),200 language: nativeEncodingLanguage(blob.encoding),
194 })),201 body:
195 ];202 typeof blob.value === "string"
196 const sections = [203 ? blob.value
197 {204 : JSON.stringify(blob.value ?? null, null, 2),
198 kind: "facts" as const,205 })),
199 title: "Overview",206 ];
200 items: overviewItems,207 const sections = [
201 },208 {
202 ...(summary.description209 kind: "facts" as const,
203 ? [{210 title: "Overview",
204 kind: "markdown" as const,211 items: overviewItems,
205 title: "Description",212 },
206 body: summary.description,213 ...(summary.description
207 }]214 ? [{
208 : []),215 kind: "markdown" as const,
209 ...([216 title: "Description",
210 jsonSection("Input Schema", tool.descriptor.inputSchema),217 body: summary.description,
211 jsonSection("Output Schema", tool.descriptor.outputSchema),218 }]
212 jsonSection("Capability", tool.capability),219 : []),
213 jsonSection("Executable", {220 ...([
214 id: tool.executable.id,221 jsonSection("Capability", tool.capability),
215 adapterKey: tool.executable.adapterKey,222 jsonSection("Executable", {
216 bindingVersion: tool.executable.bindingVersion,223 id: tool.executable.id,
217 binding: tool.executable.binding,224 adapterKey: tool.executable.adapterKey,
218 projection: tool.executable.projection,225 bindingVersion: tool.executable.bindingVersion,
219 display: tool.executable.display ?? null,226 binding: tool.executable.binding,
220 }),227 projection: tool.executable.projection,
221 jsonSection("Documentation", {228 display: tool.executable.display ?? null,
222 summary: tool.capability.surface.summary,229 }),
223 description: tool.capability.surface.description,230 jsonSection("Documentation", {
224 }),231 summary: tool.capability.surface.summary,
225 ].filter((section) => section !== null) as Array<SourceInspectionToolDetail["sections"][number]>),232 description: tool.capability.surface.description,
226 ...nativeSections,233 }),
227 ];234 ].filter((section) => section !== null) as Array<SourceInspectionToolDetail["sections"][number]>),
228 235 ...nativeSections,
229 return ({236 ];
230 summary,237
231 sections,238 return {
239 summary,
240 contract,
241 sections,
242 } satisfies SourceInspectionToolDetail;
232 });243 });
233};
234 244
235const resolveSourceInspection = (input: {245const resolveSourceInspection = (input: {
236 workspaceId: WorkspaceId;246 workspaceId: WorkspaceId;
237 sourceId: SourceId;247 sourceId: SourceId;
238 includeSchemas: boolean;248 includeSchemas: boolean;
249 includeTypePreviews: boolean;
239}) =>250}) =>
240 Effect.gen(function* () {251 Effect.gen(function* () {
241 const loaded = yield* loadSourceCatalogOrEmpty({252 const loaded = yield* loadSourceCatalogOrEmpty({
@@ -255,7 +266,7 @@ const resolveSourceInspection = (input: { · 13 unmodified lines
255 const tools = yield* expandCatalogTools({266 const tools = yield* expandCatalogTools({
256 catalogs: [loaded.catalogEntry],267 catalogs: [loaded.catalogEntry],
257 includeSchemas: input.includeSchemas,268 includeSchemas: input.includeSchemas,
258 includeTypePreviews: input.includeSchemas,269 includeTypePreviews: input.includeTypePreviews,
259 });270 });
260 271
261 return {272 return {
@@ -289,8 +300,8 @@ const resolveSourceInspectionTool = (input: { · 27 unmodified lines
289 const tool = yield* expandCatalogToolByPath({300 const tool = yield* expandCatalogToolByPath({
290 catalogs: [loaded.catalogEntry],301 catalogs: [loaded.catalogEntry],
291 path: input.toolPath,302 path: input.toolPath,
292 includeSchemas: false,303 includeSchemas: true,
293 includeTypePreviews: false,304 includeTypePreviews: true,
294 });305 });
295 306
296 return {307 return {
@@ -382,6 +393,7 @@ export const getSourceInspection = (input: { · 85 unmodified lines
382 const inspection = yield* resolveSourceInspection({393 const inspection = yield* resolveSourceInspection({
383 ...input,394 ...input,
384 includeSchemas: false,395 includeSchemas: false,
396 includeTypePreviews: true,
385 });397 });
386 398
387 return {399 return {
@@ -420,7 +432,7 @@ export const getSourceInspectionToolDetail = (input: { · 32 unmodified lines
420 );432 );
421 }433 }
422 434
423 return inspectionToolDetailFromTool(tool);435 return yield* inspectionToolDetailFromTool(tool);
424 }).pipe(436 }).pipe(
425 Effect.mapError((cause) =>437 Effect.mapError((cause) =>
426 mapInspectionError(438 mapInspectionError(
@@ -440,6 +452,7 @@ export const discoverSourceInspectionTools = (input: { · 13 unmodified lines
440 workspaceId: input.workspaceId,452 workspaceId: input.workspaceId,
441 sourceId: input.sourceId,453 sourceId: input.sourceId,
442 includeSchemas: false,454 includeSchemas: false,
455 includeTypePreviews: true,
443 });456 });
444 const queryTokens = tokenize(input.payload.query);457 const queryTokens = tokenize(input.payload.query);
445 const results = inspection.tools458 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
56export const SourceInspectionToolListItemSchema = Schema.Struct({56export const SourceInspectionToolListItemSchema = Schema.Struct({
57 path: Schema.String,57 path: Schema.String,
58 method: Schema.NullOr(Schema.String),58 method: Schema.NullOr(Schema.String),
59 inputTypePreview: Schema.optional(Schema.String),
60 outputTypePreview: Schema.optional(Schema.String),
61});
62
63export 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
71export 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,
59});79});
60 80
61export const SourceInspectionSchema = Schema.Struct({81export const SourceInspectionSchema = Schema.Struct({
@@ -68,6 +88,7 @@ export const SourceInspectionSchema = Schema.Struct({ · 6 unmodified lines
68 88
69export const SourceInspectionToolDetailSchema = Schema.Struct({89export const SourceInspectionToolDetailSchema = Schema.Struct({
70 summary: SourceInspectionToolSummarySchema,90 summary: SourceInspectionToolSummarySchema,
91 contract: SourceInspectionToolContractSchema,
71 sections: Schema.Array(SourceInspectionSectionSchema),92 sections: Schema.Array(SourceInspectionSectionSchema),
72});93});
73 94
@@ -109,6 +130,10 @@ export type SourceInspectionSection = · 35 unmodified lines
109 typeof SourceInspectionSectionSchema.Type;130 typeof SourceInspectionSectionSchema.Type;
110export type SourceInspectionToolListItem =131export type SourceInspectionToolListItem =
111 typeof SourceInspectionToolListItemSchema.Type;132 typeof SourceInspectionToolListItemSchema.Type;
133export type SourceInspectionToolContractSide =
134 typeof SourceInspectionToolContractSideSchema.Type;
135export type SourceInspectionToolContract =
136 typeof SourceInspectionToolContractSchema.Type;
112export type SourceInspection = typeof SourceInspectionSchema.Type;137export type SourceInspection = typeof SourceInspectionSchema.Type;
113export type SourceInspectionToolDetail =138export type SourceInspectionToolDetail =
114 typeof SourceInspectionToolDetailSchema.Type;139 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
25 method: "GET",25 method: "GET",
26 pathTemplate: "/issues",26 pathTemplate: "/issues",
27 },27 },
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 },
28 sections: [{49 sections: [{
29 kind: "facts",50 kind: "facts",
30 title: "Overview",51 title: "Overview",
packages/sources/google-discovery/src/tools.ts+10−6
@@ -456,12 +456,16 @@ export const createGoogleDiscoveryToolFromDefinition = ( · 455 unmodified lines
456 interaction: input.definition.method === "get" || input.definition.method === "head"456 interaction: input.definition.method === "get" || input.definition.method === "head"
457 ? "auto"457 ? "auto"
458 : "required",458 : "required",
459 ...(presentation.inputSchema !== undefined459 contract: {
460 ? { inputSchema: presentation.inputSchema }460 inputTypePreview: presentation.inputTypePreview,
461 : {}),461 outputTypePreview: presentation.outputTypePreview,
462 ...(presentation.outputSchema !== undefined462 ...(presentation.inputSchema !== undefined
463 ? { outputSchema: presentation.outputSchema }463 ? { inputSchema: presentation.inputSchema }
464 : {}),464 : {}),
465 ...(presentation.outputSchema !== undefined
466 ? { outputSchema: presentation.outputSchema }
467 : {}),
468 },
465 sourceKey: input.sourceKey,469 sourceKey: input.sourceKey,
466 providerKind: "google_discovery",470 providerKind: "google_discovery",
467 providerData,471 providerData,
packages/sources/graphql/src/graphql-tools.ts+34−30
@@ -1437,25 +1437,27 @@ export const createGraphqlToolFromPersistedOperation = (input: { · 1436 unmodified lines
1437 : providerData.operationType === "query"1437 : providerData.operationType === "query"
1438 ? "auto"1438 ? "auto"
1439 : "required",1439 : "required",
1440 inputTypePreview: typeSignatureFromSchema(1440 contract: {
1441 input.inputSchema,1441 inputTypePreview: typeSignatureFromSchema(
1442 "unknown",1442 input.inputSchema,
1443 GRAPHQL_PRESENTATION_TYPE_MAX_LENGTH,1443 "unknown",
1444 ),1444 GRAPHQL_PRESENTATION_TYPE_MAX_LENGTH,
1445 outputTypePreview: typeSignatureFromSchema(1445 ),
1446 input.outputSchema,1446 outputTypePreview: typeSignatureFromSchema(
1447 "unknown",1447 input.outputSchema,
1448 GRAPHQL_PRESENTATION_TYPE_MAX_LENGTH,1448 "unknown",
1449 ),1449 GRAPHQL_PRESENTATION_TYPE_MAX_LENGTH,
1450 ...(input.inputSchema !== undefined1450 ),
1451 ? { inputSchema: input.inputSchema }1451 ...(input.inputSchema !== undefined
1452 : {}),1452 ? { inputSchema: input.inputSchema }
1453 ...(input.outputSchema !== undefined1453 : {}),
1454 ? { outputSchema: input.outputSchema }1454 ...(input.outputSchema !== undefined
1455 : {}),1455 ? { outputSchema: input.outputSchema }
1456 ...(input.exampleInput !== undefined1456 : {}),
1457 ? { exampleInput: input.exampleInput }1457 ...(input.exampleInput !== undefined
1458 : {}),1458 ? { exampleInput: input.exampleInput }
1459 : {}),
1460 },
1459 sourceKey: input.sourceKey,1461 sourceKey: input.sourceKey,
1460 providerKind: "graphql",1462 providerKind: "graphql",
1461 providerData,1463 providerData,
@@ -1524,17 +1526,19 @@ export const graphqlToolDescriptorFromDefinition = (input: { · 62 unmodified lines
1524 description: input.definition.description,1526 description: input.definition.description,
1525 interaction:1527 interaction:
1526 input.definition.operationType === "query" ? "auto" : "required",1528 input.definition.operationType === "query" ? "auto" : "required",
1527 inputTypePreview: presentation.inputTypePreview,1529 contract: {
1528 outputTypePreview: presentation.outputTypePreview,1530 inputTypePreview: presentation.inputTypePreview,
1529 ...(input.includeSchemas && presentation.inputSchema !== undefined1531 outputTypePreview: presentation.outputTypePreview,
1530 ? { inputSchema: presentation.inputSchema }1532 ...(input.includeSchemas && presentation.inputSchema !== undefined
1531 : {}),1533 ? { inputSchema: presentation.inputSchema }
1532 ...(input.includeSchemas && presentation.outputSchema !== undefined1534 : {}),
1533 ? { outputSchema: presentation.outputSchema }1535 ...(input.includeSchemas && presentation.outputSchema !== undefined
1534 : {}),1536 ? { outputSchema: presentation.outputSchema }
1535 ...(presentation.exampleInput !== undefined1537 : {}),
1536 ? { exampleInput: presentation.exampleInput }1538 ...(presentation.exampleInput !== undefined
1537 : {}),1539 ? { exampleInput: presentation.exampleInput }
1540 : {}),
1541 },
1538 providerKind: "graphql",1542 providerKind: "graphql",
1539 providerData: presentation.providerData,1543 providerData: presentation.providerData,
1540 };1544 };
packages/sources/mcp/src/adapter.ts+165−38
@@ -4,7 +4,8 @@ import { · 3 unmodified lines
4 type ToolInput,4 type ToolInput,
5 type ToolPath,5 type ToolPath,
6} from "@executor/codemode-core";6} from "@executor/codemode-core";
7import { createSdkMcpConnector } from "./connection";7import { createPooledMcpConnector } from "./connection-pool";
8import { createSdkMcpConnector, isMcpStdioTransport } from "./connection";
8import {9import {
9 createMcpToolsFromManifest,10 createMcpToolsFromManifest,
10 discoverMcpToolsFromConnector,11 discoverMcpToolsFromConnector,
@@ -26,8 +27,8 @@ import { · 15 unmodified lines
26 encodeBindingConfig,27 encodeBindingConfig,
27 McpConnectFieldsSchema,28 McpConnectFieldsSchema,
28 OptionalNullableStringSchema,29 OptionalNullableStringSchema,
29 SourceConnectCommonFieldsSchema,
30 SourceTransportSchema,30 SourceTransportSchema,
31 StringArraySchema,
31 StringMapSchema,32 StringMapSchema,
32 createCatalogImportMetadata,33 createCatalogImportMetadata,
33 EXECUTABLE_BINDING_VERSION,34 EXECUTABLE_BINDING_VERSION,
@@ -35,6 +36,7 @@ import { · 1 unmodified line
35 type Source,36 type Source,
36 type SourceAdapter,37 type SourceAdapter,
37 type SourceCatalogSyncResult,38 type SourceCatalogSyncResult,
39 type SourceTransport,
38} from "@executor/source-core";40} from "@executor/source-core";
39import * as Effect from "effect/Effect";41import * as Effect from "effect/Effect";
40import * as Schema from "effect/Schema";42import * as Schema from "effect/Schema";
@@ -69,27 +71,38 @@ const namespaceFromSourceName = (name: string): string => { · 28 unmodified lines
69 return normalized.length > 0 ? normalized : "source";71 return normalized.length > 0 ? normalized : "source";
70};72};
71 73
74const OptionalNullableStringArraySchema = Schema.optional(
75 Schema.NullOr(StringArraySchema),
76);
77
72const McpConnectPayloadSchema = Schema.extend(78const McpConnectPayloadSchema = Schema.extend(
73 SourceConnectCommonFieldsSchema,79 McpConnectFieldsSchema,
74 Schema.extend(80 Schema.Struct({
75 McpConnectFieldsSchema,81 kind: Schema.Literal("mcp"),
76 Schema.Struct({82 endpoint: OptionalNullableStringSchema,
77 kind: Schema.Literal("mcp"),83 name: OptionalNullableStringSchema,
78 }),84 namespace: OptionalNullableStringSchema,
79 ),85 }),
80);86);
81 87
82const McpExecutorAddInputSchema = Schema.Struct({88const McpExecutorAddInputSchema = Schema.extend(
83 kind: Schema.optional(Schema.Literal("mcp")),89 McpConnectFieldsSchema,
84 endpoint: Schema.String,90 Schema.Struct({
85 name: OptionalNullableStringSchema,91 kind: Schema.optional(Schema.Literal("mcp")),
86 namespace: OptionalNullableStringSchema,92 endpoint: OptionalNullableStringSchema,
87});93 name: OptionalNullableStringSchema,
94 namespace: OptionalNullableStringSchema,
95 }),
96);
88 97
89const McpBindingConfigSchema = Schema.Struct({98const McpBindingConfigSchema = Schema.Struct({
90 transport: Schema.NullOr(SourceTransportSchema),99 transport: Schema.NullOr(SourceTransportSchema),
91 queryParams: Schema.NullOr(StringMapSchema),100 queryParams: Schema.NullOr(StringMapSchema),
92 headers: Schema.NullOr(StringMapSchema),101 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),
93});106});
94 107
95type McpBindingConfig = typeof McpBindingConfigSchema.Type;108type McpBindingConfig = typeof McpBindingConfigSchema.Type;
@@ -98,6 +111,10 @@ const McpSourceBindingPayloadSchema = Schema.Struct({ · 2 unmodified lines
98 transport: Schema.optional(Schema.NullOr(SourceTransportSchema)),111 transport: Schema.optional(Schema.NullOr(SourceTransportSchema)),
99 queryParams: Schema.optional(Schema.NullOr(StringMapSchema)),112 queryParams: Schema.optional(Schema.NullOr(StringMapSchema)),
100 headers: Schema.optional(Schema.NullOr(StringMapSchema)),113 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)),
101});118});
102 119
103const McpExecutableBindingSchema = Schema.Struct({120const McpExecutableBindingSchema = Schema.Struct({
@@ -127,6 +144,86 @@ const bindingHasAnyField = ( · 23 unmodified lines
127 !Array.isArray(value) &&144 !Array.isArray(value) &&
128 fields.some((field) => Object.prototype.hasOwnProperty.call(value, field));145 fields.some((field) => Object.prototype.hasOwnProperty.call(value, field));
129 146
147const 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
156const 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
170const 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
130const mcpBindingConfigFromSource = (227const mcpBindingConfigFromSource = (
131 source: Pick<Source, "id" | "bindingVersion" | "binding">,228 source: Pick<Source, "id" | "bindingVersion" | "binding">,
132): Effect.Effect<McpBindingConfig, Error, never> =>229): Effect.Effect<McpBindingConfig, Error, never> =>
@@ -145,14 +242,10 @@ const mcpBindingConfigFromSource = ( · 12 unmodified lines
145 expectedVersion: MCP_BINDING_CONFIG_VERSION,242 expectedVersion: MCP_BINDING_CONFIG_VERSION,
146 schema: McpSourceBindingPayloadSchema,243 schema: McpSourceBindingPayloadSchema,
147 value: source.binding,244 value: source.binding,
148 allowedKeys: ["transport", "queryParams", "headers"],245 allowedKeys: ["transport", "queryParams", "headers", "command", "args", "env", "cwd"],
149 });246 });
150 247
151 return {248 return yield* normalizeMcpBindingConfig(bindingConfig);
152 transport: bindingConfig.transport ?? null,
153 queryParams: bindingConfig.queryParams ?? null,
154 headers: bindingConfig.headers ?? null,
155 } satisfies McpBindingConfig;
156 });249 });
157 250
158const effectFromMcpManifestEntry = (251const effectFromMcpManifestEntry = (
@@ -233,7 +326,8 @@ export const mcpSourceAdapter: SourceAdapter = { · 74 unmodified lines
233 connectPayloadSchema: McpConnectPayloadSchema,326 connectPayloadSchema: McpConnectPayloadSchema,
234 executorAddInputSchema: McpExecutorAddInputSchema,327 executorAddInputSchema: McpExecutorAddInputSchema,
235 executorAddHelpText: [328 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.',
237 ],331 ],
238 executorAddInputSignatureWidth: 240,332 executorAddInputSignatureWidth: 240,
239 serializeBindingConfig: (source) =>333 serializeBindingConfig: (source) =>
@@ -264,6 +358,10 @@ export const mcpSourceAdapter: SourceAdapter = { · 24 unmodified lines
264 transport: bindingConfig.transport,358 transport: bindingConfig.transport,
265 queryParams: bindingConfig.queryParams,359 queryParams: bindingConfig.queryParams,
266 headers: bindingConfig.headers,360 headers: bindingConfig.headers,
361 command: bindingConfig.command,
362 args: bindingConfig.args,
363 env: bindingConfig.env,
364 cwd: bindingConfig.cwd,
267 })),365 })),
268 sourceConfigFromSource: (source) =>366 sourceConfigFromSource: (source) =>
269 Effect.runSync(367 Effect.runSync(
@@ -273,6 +371,10 @@ export const mcpSourceAdapter: SourceAdapter = { · 3 unmodified lines
273 transport: bindingConfig.transport,371 transport: bindingConfig.transport,
274 queryParams: bindingConfig.queryParams,372 queryParams: bindingConfig.queryParams,
275 headers: bindingConfig.headers,373 headers: bindingConfig.headers,
374 command: bindingConfig.command,
375 args: bindingConfig.args,
376 env: bindingConfig.env,
377 cwd: bindingConfig.cwd,
276 })),378 })),
277 ),379 ),
278 validateSource: (source) =>380 validateSource: (source) =>
@@ -286,6 +388,10 @@ export const mcpSourceAdapter: SourceAdapter = { · 7 unmodified lines
286 transport: bindingConfig.transport,388 transport: bindingConfig.transport,
287 queryParams: bindingConfig.queryParams,389 queryParams: bindingConfig.queryParams,
288 headers: bindingConfig.headers,390 headers: bindingConfig.headers,
391 command: bindingConfig.command,
392 args: bindingConfig.args,
393 env: bindingConfig.env,
394 cwd: bindingConfig.cwd,
289 },395 },
290 };396 };
291 }),397 }),
@@ -309,6 +415,10 @@ export const mcpSourceAdapter: SourceAdapter = { · 17 unmodified lines
309 authCookies: auth.cookies,415 authCookies: auth.cookies,
310 }),416 }),
311 authProvider: auth.authProvider,417 authProvider: auth.authProvider,
418 command: bindingConfig.command ?? undefined,
419 args: bindingConfig.args ?? undefined,
420 env: bindingConfig.env ?? undefined,
421 cwd: bindingConfig.cwd ?? undefined,
312 }),422 }),
313 catch: (cause) =>423 catch: (cause) =>
314 cause instanceof Error ? cause : new Error(String(cause)),424 cause instanceof Error ? cause : new Error(String(cause)),
@@ -353,19 +463,30 @@ export const mcpSourceAdapter: SourceAdapter = { · 38 unmodified lines
353 const bindingConfig = Effect.runSync(463 const bindingConfig = Effect.runSync(
354 mcpBindingConfigFromSource(input.source),464 mcpBindingConfigFromSource(input.source),
355 );465 );
356 const connector = createSdkMcpConnector({466 const connector = createPooledMcpConnector({
357 endpoint: input.source.endpoint,467 connect: createSdkMcpConnector({
358 transport: bindingConfig.transport ?? undefined,468 endpoint: input.source.endpoint,
359 queryParams: {469 transport: bindingConfig.transport ?? undefined,
360 ...bindingConfig.queryParams,470 queryParams: {
361 ...input.auth.queryParams,471 ...bindingConfig.queryParams,
362 },472 ...input.auth.queryParams,
363 headers: headersWithAuthCookies({473 },
364 headers: bindingConfig.headers ?? {},474 headers: headersWithAuthCookies({
365 authHeaders: input.auth.headers,475 headers: bindingConfig.headers ?? {},
366 authCookies: input.auth.cookies,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,
367 }),484 }),
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,
369 });490 });
370 const tools = createMcpToolsFromManifest({491 const tools = createMcpToolsFromManifest({
371 manifest: {492 manifest: {
@@ -392,8 +513,8 @@ export const mcpSourceAdapter: SourceAdapter = { · 20 unmodified lines
392 icons: null,513 icons: null,
393 meta: null,514 meta: null,
394 rawTool: null,515 rawTool: null,
395 inputSchema: input.descriptor.inputSchema,516 inputSchema: input.descriptor.contract?.inputSchema,
396 outputSchema: input.descriptor.outputSchema,517 outputSchema: input.descriptor.contract?.outputSchema,
397 },518 },
398 ],519 ],
399 },520 },
@@ -430,8 +551,14 @@ export const mcpSourceAdapter: SourceAdapter = { · 30 unmodified lines
430 metadata: {551 metadata: {
431 sourceKey: input.source.id,552 sourceKey: input.source.id,
432 interaction: input.descriptor.interaction,553 interaction: input.descriptor.interaction,
433 inputSchema: input.descriptor.inputSchema,554 contract: {
434 outputSchema: input.descriptor.outputSchema,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 },
435 providerKind: input.descriptor.providerKind,562 providerKind: input.descriptor.providerKind,
436 providerData: input.descriptor.providerData,563 providerData: input.descriptor.providerData,
437 },564 },
packages/sources/mcp/src/tools.test.ts+1−1
@@ -377,7 +377,7 @@ describe("codemode-mcp", () => { · 376 unmodified lines
377 377
378 const toolDefinition = resolveToolDefinition(discovered.tools["source.mcp.echo"]!);378 const toolDefinition = resolveToolDefinition(discovered.tools["source.mcp.echo"]!);
379 expect(toolDefinition.metadata?.sourceKey).toBe("mcp.demo");379 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");
381 381
382 const invoke = resolveToolExecutor(discovered.tools, "source.mcp.echo");382 const invoke = resolveToolExecutor(discovered.tools, "source.mcp.echo");
383 const invocationResult = yield* Effect.promise(() => invoke({ value: "hello" }));383 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
503 },503 },
504 metadata: {504 metadata: {
505 sourceKey,505 sourceKey,
506 ...(entry.inputSchema !== undefined506 contract: {
507 ? { inputSchema: entry.inputSchema }507 ...(entry.inputSchema !== undefined
508 : {}),508 ? { inputSchema: entry.inputSchema }
509 ...(entry.outputSchema !== undefined509 : {}),
510 ? { outputSchema: entry.outputSchema }510 ...(entry.outputSchema !== undefined
511 : {}),511 ? { outputSchema: entry.outputSchema }
512 : {}),
513 },
512 },514 },
513 }),515 }),
514 ] as const;516 ] 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
111 111
112 for (const expectedTool of expectedTools) {112 for (const expectedTool of expectedTools) {
113 const resolved = resolveToolDefinition(tools[expectedTool.expectedPath]!);113 const resolved = resolveToolDefinition(tools[expectedTool.expectedPath]!);
114 const inputSchema = parseInputSchema(resolved.metadata?.inputSchema);114 const inputSchema = parseInputSchema(resolved.metadata?.contract?.inputSchema);
115 115
116 expect(inputSchema?.type).toBe("object");116 expect(inputSchema?.type).toBe("object");
117 expect(Object.keys(inputSchema?.properties ?? {})).toEqual(117 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
354 expect(resolvedGet.metadata?.sourceKey).toBe("api.demo");354 expect(resolvedGet.metadata?.sourceKey).toBe("api.demo");
355 expect(resolvedGet.metadata?.providerKind).toBe("openapi");355 expect(resolvedGet.metadata?.providerKind).toBe("openapi");
356 expect(resolvedGet.metadata?.providerData).toMatchObject({ group: "repos" });356 expect(resolvedGet.metadata?.providerData).toMatchObject({ group: "repos" });
357 expect(resolvedGet.metadata?.inputSchema).toBeDefined();357 expect(resolvedGet.metadata?.contract?.inputSchema).toBeDefined();
358 }),358 }),
359 );359 );
360 360
packages/sources/openapi/src/tools.ts+16−14
@@ -541,20 +541,22 @@ export const createOpenApiToolFromDefinition = ( · 540 unmodified lines
541 },541 },
542 metadata: {542 metadata: {
543 sourceKey: input.sourceKey,543 sourceKey: input.sourceKey,
544 inputTypePreview: presentation.inputTypePreview,544 contract: {
545 outputTypePreview: presentation.outputTypePreview,545 inputTypePreview: presentation.inputTypePreview,
546 ...((presentation.inputSchema ?? input.definition.typing?.inputSchema) !== undefined546 outputTypePreview: presentation.outputTypePreview,
547 ? { inputSchema: presentation.inputSchema ?? input.definition.typing?.inputSchema }547 ...((presentation.inputSchema ?? input.definition.typing?.inputSchema) !== undefined
548 : {}),548 ? { inputSchema: presentation.inputSchema ?? input.definition.typing?.inputSchema }
549 ...((presentation.outputSchema ?? input.definition.typing?.outputSchema) !== undefined549 : {}),
550 ? { outputSchema: presentation.outputSchema ?? input.definition.typing?.outputSchema }550 ...((presentation.outputSchema ?? input.definition.typing?.outputSchema) !== undefined
551 : {}),551 ? { outputSchema: presentation.outputSchema ?? input.definition.typing?.outputSchema }
552 ...(presentation.exampleInput !== undefined552 : {}),
553 ? { exampleInput: presentation.exampleInput }553 ...(presentation.exampleInput !== undefined
554 : {}),554 ? { exampleInput: presentation.exampleInput }
555 ...(presentation.exampleOutput !== undefined555 : {}),
556 ? { exampleOutput: presentation.exampleOutput }556 ...(presentation.exampleOutput !== undefined
557 : {}),557 ? { exampleOutput: presentation.exampleOutput }
558 : {}),
559 },
558 providerKind: "openapi",560 providerKind: "openapi",
559 providerData: presentation.providerData,561 providerData: presentation.providerData,
560 },562 },