History

Reduce source artifact bloat for imported tools

Rhys Sullivan committed 7 months ago

Commit fff219b1 parent 203125313 files · +618 −273Browse files

Unified · Split

13 files
  1. packages/platform/sdk/src/runtime/sources/openapi-type-contract.test.ts +123 −0
  2. packages/platform/sdk/src/runtime/sources/source-inspection.test.ts +244 −216
  3. packages/platform/sdk/src/sources/inspection.ts +57 −20
  4. plugins/mcp/sdk/catalog.test.ts +35 −0
  5. plugins/mcp/sdk/catalog.ts +2 −1
  6. plugins/mcp/sdk/executable-binding.test.ts +44 −0
  7. plugins/mcp/sdk/executable-binding.ts +16 −0
  8. plugins/mcp/sdk/index.ts +5 −25
  9. plugins/openapi/sdk/catalog.ts +15 −1
  10. plugins/openapi/sdk/definitions.ts +12 −0
  11. plugins/openapi/sdk/extraction.ts +42 −1
  12. plugins/openapi/sdk/index.ts +12 −9
  13. plugins/openapi/sdk/types.ts +11 −0
packages/platform/sdk/src/runtime/sources/openapi-type-contract.test.ts+123−0
@@ -95,6 +95,76 @@ const noContentSuccessOpenApiFixture = JSON.stringify({ · 94 unmodified lines
9595 },
9696 });
9797
98+const sharedRefOpenApiFixture = JSON.stringify({
99+ openapi: "3.1.0",
100+ info: {
101+ title: "Shared Ref API",
102+ version: "1.0.0",
103+ },
104+ servers: [
105+ {
106+ url: "https://api.example.test",
107+ },
108+ ],
109+ paths: {
110+ "/widgets/{widgetId}": {
111+ get: {
112+ operationId: "getWidget",
113+ parameters: [
114+ {
115+ name: "widgetId",
116+ in: "path",
117+ required: true,
118+ schema: {
119+ type: "string",
120+ },
121+ },
122+ ],
123+ responses: {
124+ "200": {
125+ description: "Widget response",
126+ content: {
127+ "application/json": {
128+ schema: {
129+ $ref: "#/components/schemas/Widget",
130+ },
131+ },
132+ },
133+ },
134+ },
135+ },
136+ },
137+ },
138+ components: {
139+ schemas: {
140+ Widget: {
141+ type: "object",
142+ required: ["id", "name", "owner"],
143+ properties: {
144+ id: {
145+ type: "string",
146+ },
147+ name: {
148+ type: "string",
149+ },
150+ owner: {
151+ $ref: "#/components/schemas/Owner",
152+ },
153+ },
154+ },
155+ Owner: {
156+ type: "object",
157+ required: ["email"],
158+ properties: {
159+ email: {
160+ type: "string",
161+ },
162+ },
163+ },
164+ },
165+ },
166+});
167+
98168 const makeSource = (input: {
99169 id?: string;
100170 name?: string;
@@ -290,4 +360,57 @@ describe("openapi type contracts", () => { · 189 unmodified lines
290360 expect(contract.output.typeDeclaration).not.toContain("data: unknown");
291361 expect(contract.output.typePreview).toContain("data: null");
292362 }).pipe(Effect.provide(NodeFileSystem.layer)));
363+
364+ it.effect("preserves local component refs in IR and keeps executable bindings runtime-minimal", () =>
365+ Effect.gen(function* () {
366+ const source = makeSource({
367+ id: "source_shared_ref_api",
368+ name: "Shared Ref API",
369+ namespace: "shared-ref-api",
370+ });
371+ const manifest = yield* extractOpenApiManifest(
372+ source.name,
373+ sharedRefOpenApiFixture,
374+ );
375+ const operations = compileOpenApiToolDefinitions(manifest).map(
376+ openApiCatalogOperationFromDefinition,
377+ );
378+ const fragment = createOpenApiCatalogFragment({
379+ source,
380+ documents: [
381+ {
382+ documentKind: "openapi",
383+ documentKey: "https://example.com/shared-ref-openapi.json",
384+ fetchedAt: 0,
385+ contentText: sharedRefOpenApiFixture,
386+ },
387+ ],
388+ operations,
389+ });
390+ const snapshot = snapshotFromSourceCatalogSyncResult(
391+ createSourceCatalogSyncResult({
392+ fragment,
393+ importMetadata: createCatalogImportMetadata({
394+ source,
395+ pluginKey: "openapi",
396+ }),
397+ sourceHash: manifest.sourceHash,
398+ }),
399+ );
400+
401+ const refShapes = Object.values(snapshot.catalog.symbols).filter(
402+ (symbol) => symbol.kind === "shape" && symbol.node.type === "ref",
403+ );
404+ expect(refShapes.length).toBeGreaterThan(0);
405+
406+ const executable = Object.values(snapshot.catalog.executables)[0];
407+ expect(executable).toBeDefined();
408+ expect(executable?.binding).toMatchObject({
409+ kind: "openapi",
410+ toolId: "widgets.getWidget",
411+ });
412+ expect((executable?.binding as Record<string, unknown>).responses).toBeUndefined();
413+ expect((executable?.binding as Record<string, unknown>).documentation).toBeUndefined();
414+ expect(JSON.stringify(executable?.binding).length).toBeLessThan(2_000);
415+ }));
293416 });
packages/platform/sdk/src/runtime/sources/source-inspection.test.ts+244−216
@@ -12,238 +12,242 @@ import { · 11 unmodified lines
1212 inspectionToolDetailFromTool,
1313 } from "../../sources/inspection";
1414
15−describe("source inspection", () => {
16− it.effect("keeps the shared TypeScript contract first-class on tool detail", () =>
17− Effect.gen(function* () {
18− const detail = yield* inspectionToolDetailFromTool({
19− path: "linear.administrableTeams",
20− source: {
21− id: "linear",
22− },
23− capability: {
24− surface: {
25− title: "Administrable Teams",
26− summary: "All teams the user can administrate.",
27− description: "All teams the user can administrate.",
28− tags: ["query"],
29− },
30− native: [],
31− },
32− executable: {
33− id: "exec_graphql_administrableTeams",
34− pluginKey: "graphql",
35− bindingVersion: 1,
36− binding: {
37− operationKind: "query",
38− rootTypeName: "Query",
39− fieldName: "administrableTeams",
40− },
41− projection: {
42− callShapeId: "shape_call",
43− resultDataShapeId: "shape_result_data",
44− resultErrorShapeId: "shape_result_error",
45− responseSetId: "response_set_1",
46− },
47− display: {
48− protocol: "graphql",
49− method: "query",
50− pathTemplate: "administrableTeams",
51− operationId: "administrableTeams",
52− group: "query",
53− leaf: "administrableTeams",
54− rawToolId: "administrableTeams",
55− },
56− native: [],
15+const baseLoadedSourceCatalogTool = {
16+ path: "linear.administrableTeams",
17+ source: {
18+ id: "linear",
19+ },
20+ capability: {
21+ surface: {
22+ title: "Administrable Teams",
23+ summary: "All teams the user can administrate.",
24+ description: "All teams the user can administrate.",
25+ tags: ["query"],
26+ },
27+ native: [],
28+ },
29+ executable: {
30+ id: "exec_graphql_administrableTeams",
31+ pluginKey: "graphql",
32+ bindingVersion: 1,
33+ binding: {
34+ operationKind: "query",
35+ rootTypeName: "Query",
36+ fieldName: "administrableTeams",
37+ },
38+ projection: {
39+ callShapeId: "shape_call",
40+ resultDataShapeId: "shape_result_data",
41+ resultErrorShapeId: "shape_result_error",
42+ responseSetId: "response_set_1",
43+ },
44+ display: {
45+ protocol: "graphql",
46+ method: "query",
47+ pathTemplate: "administrableTeams",
48+ operationId: "administrableTeams",
49+ group: "query",
50+ leaf: "administrableTeams",
51+ rawToolId: "administrableTeams",
52+ },
53+ native: [],
54+ },
55+ projectedDescriptor: {
56+ toolPath: ["linear", "administrableTeams"],
57+ callShapeId: "shape_call",
58+ resultShapeId: "shape_result",
59+ responseSetId: "response_set_1",
60+ },
61+ descriptor: {
62+ inputTypePreview: "{ input: string }",
63+ outputTypePreview: "{ data: string | null; error: unknown | null; headers: Record<string, string>; status: number | null }",
64+ inputSchema: {
65+ type: "object",
66+ properties: {
67+ input: { type: "string" },
5768 },
58− projectedDescriptor: {
59− toolPath: ["linear", "administrableTeams"],
60− callShapeId: "shape_call",
61− resultShapeId: "shape_result",
62− responseSetId: "response_set_1",
69+ required: ["input"],
70+ },
71+ outputSchema: {
72+ type: "object",
73+ properties: {
74+ data: { type: ["string", "null"] },
6375 },
64− descriptor: {
65− inputTypePreview: "{ input: string }",
66− outputTypePreview: "{ data: string | null; error: unknown | null; headers: Record<string, string>; status: number | null }",
67− inputSchema: {
76+ required: ["data"],
77+ },
78+ },
79+ projectedCatalog: {
80+ version: "ir.v1",
81+ documents: {},
82+ resources: {},
83+ scopes: {},
84+ capabilities: {},
85+ executables: {},
86+ responseSets: {},
87+ diagnostics: {},
88+ symbols: {
89+ shape_call: {
90+ id: "shape_call",
91+ kind: "shape",
92+ resourceId: "res_1",
93+ title: "AdministrableTeamsCall",
94+ node: {
6895 type: "object",
69− properties: {
70− input: { type: "string" },
96+ fields: {
97+ input: {
98+ shapeId: "shape_call_input",
99+ docs: {
100+ description: "The exact input string to filter by.",
101+ },
102+ },
71103 },
72104 required: ["input"],
105+ additionalProperties: false,
73106 },
74− outputSchema: {
75− type: "object",
76− properties: {
77− data: { type: ["string", "null"] },
78− },
79− required: ["data"],
107+ synthetic: false,
108+ provenance: [],
109+ },
110+ shape_call_input: {
111+ id: "shape_call_input",
112+ kind: "shape",
113+ resourceId: "res_1",
114+ title: "Input",
115+ node: {
116+ type: "scalar",
117+ scalar: "string",
80118 },
119+ synthetic: false,
120+ provenance: [],
81121 },
82− projectedCatalog: {
83− version: "ir.v1",
84− documents: {},
85− resources: {},
86− scopes: {},
87− capabilities: {},
88− executables: {},
89− responseSets: {},
90− diagnostics: {},
91− symbols: {
92− shape_call: {
93− id: "shape_call",
94− kind: "shape",
95− resourceId: "res_1",
96− title: "AdministrableTeamsCall",
97− node: {
98− type: "object",
99− fields: {
100− input: {
101− shapeId: "shape_call_input",
102− docs: {
103− description: "The exact input string to filter by.",
104− },
105− },
106− },
107− required: ["input"],
108− additionalProperties: false,
109− },
110− synthetic: false,
111− provenance: [],
112− },
113− shape_call_input: {
114− id: "shape_call_input",
115− kind: "shape",
116− resourceId: "res_1",
117− title: "Input",
118− node: {
119− type: "scalar",
120− scalar: "string",
121− },
122− synthetic: false,
123− provenance: [],
124− },
125− shape_result: {
126− id: "shape_result",
127− kind: "shape",
128− resourceId: "res_1",
129− title: "AdministrableTeamsResult",
130− node: {
131− type: "object",
132− fields: {
133− data: {
134− shapeId: "shape_result_data_nullable",
135− docs: {
136− description: "Matching team data when present.",
137− },
138− },
139− error: {
140− shapeId: "shape_result_error_nullable",
141− },
142− headers: {
143− shapeId: "shape_result_headers",
144− },
145− status: {
146− shapeId: "shape_result_status_nullable",
147− },
122+ shape_result: {
123+ id: "shape_result",
124+ kind: "shape",
125+ resourceId: "res_1",
126+ title: "AdministrableTeamsResult",
127+ node: {
128+ type: "object",
129+ fields: {
130+ data: {
131+ shapeId: "shape_result_data_nullable",
132+ docs: {
133+ description: "Matching team data when present.",
148134 },
149− required: ["data", "error", "headers", "status"],
150− additionalProperties: false,
151− },
152− synthetic: true,
153− provenance: [],
154− },
155− shape_result_data_nullable: {
156− id: "shape_result_data_nullable",
157− kind: "shape",
158− resourceId: "res_1",
159− node: {
160− type: "nullable",
161− itemShapeId: "shape_result_data",
162− },
163− synthetic: true,
164− provenance: [],
165− },
166− shape_result_data: {
167− id: "shape_result_data",
168− kind: "shape",
169− resourceId: "res_1",
170− node: {
171− type: "scalar",
172− scalar: "string",
173135 },
174− synthetic: false,
175− provenance: [],
176− },
177− shape_result_error_nullable: {
178− id: "shape_result_error_nullable",
179− kind: "shape",
180− resourceId: "res_1",
181− node: {
182− type: "nullable",
183− itemShapeId: "shape_result_error",
184− },
185− synthetic: true,
186− provenance: [],
187− },
188− shape_result_error: {
189− id: "shape_result_error",
190− kind: "shape",
191− resourceId: "res_1",
192− node: {
193− type: "unknown",
194− },
195− synthetic: false,
196− provenance: [],
197− },
198− shape_result_headers: {
199− id: "shape_result_headers",
200− kind: "shape",
201− resourceId: "res_1",
202− node: {
203− type: "object",
204− fields: {},
205− additionalProperties: "shape_result_header_value",
136+ error: {
137+ shapeId: "shape_result_error_nullable",
206138 },
207− synthetic: true,
208− provenance: [],
209− },
210− shape_result_header_value: {
211− id: "shape_result_header_value",
212− kind: "shape",
213− resourceId: "res_1",
214− node: {
215− type: "scalar",
216− scalar: "string",
139+ headers: {
140+ shapeId: "shape_result_headers",
217141 },
218− synthetic: true,
219− provenance: [],
220− },
221− shape_result_status_nullable: {
222− id: "shape_result_status_nullable",
223− kind: "shape",
224− resourceId: "res_1",
225− node: {
226− type: "nullable",
227− itemShapeId: "shape_result_status",
142+ status: {
143+ shapeId: "shape_result_status_nullable",
228144 },
229− synthetic: true,
230− provenance: [],
231− },
232− shape_result_status: {
233− id: "shape_result_status",
234− kind: "shape",
235− resourceId: "res_1",
236− node: {
237− type: "scalar",
238− scalar: "integer",
239− },
240− synthetic: true,
241− provenance: [],
242145 },
146+ required: ["data", "error", "headers", "status"],
147+ additionalProperties: false,
148+ },
149+ synthetic: true,
150+ provenance: [],
151+ },
152+ shape_result_data_nullable: {
153+ id: "shape_result_data_nullable",
154+ kind: "shape",
155+ resourceId: "res_1",
156+ node: {
157+ type: "nullable",
158+ itemShapeId: "shape_result_data",
159+ },
160+ synthetic: true,
161+ provenance: [],
162+ },
163+ shape_result_data: {
164+ id: "shape_result_data",
165+ kind: "shape",
166+ resourceId: "res_1",
167+ node: {
168+ type: "scalar",
169+ scalar: "string",
170+ },
171+ synthetic: false,
172+ provenance: [],
173+ },
174+ shape_result_error_nullable: {
175+ id: "shape_result_error_nullable",
176+ kind: "shape",
177+ resourceId: "res_1",
178+ node: {
179+ type: "nullable",
180+ itemShapeId: "shape_result_error",
181+ },
182+ synthetic: true,
183+ provenance: [],
184+ },
185+ shape_result_error: {
186+ id: "shape_result_error",
187+ kind: "shape",
188+ resourceId: "res_1",
189+ node: {
190+ type: "unknown",
191+ },
192+ synthetic: false,
193+ provenance: [],
194+ },
195+ shape_result_headers: {
196+ id: "shape_result_headers",
197+ kind: "shape",
198+ resourceId: "res_1",
199+ node: {
200+ type: "object",
201+ fields: {},
202+ additionalProperties: "shape_result_header_value",
203+ },
204+ synthetic: true,
205+ provenance: [],
206+ },
207+ shape_result_header_value: {
208+ id: "shape_result_header_value",
209+ kind: "shape",
210+ resourceId: "res_1",
211+ node: {
212+ type: "scalar",
213+ scalar: "string",
214+ },
215+ synthetic: true,
216+ provenance: [],
217+ },
218+ shape_result_status_nullable: {
219+ id: "shape_result_status_nullable",
220+ kind: "shape",
221+ resourceId: "res_1",
222+ node: {
223+ type: "nullable",
224+ itemShapeId: "shape_result_status",
243225 },
226+ synthetic: true,
227+ provenance: [],
244228 },
245− typeProjector: {} as LoadedSourceCatalogTool["typeProjector"],
246− } as LoadedSourceCatalogTool);
229+ shape_result_status: {
230+ id: "shape_result_status",
231+ kind: "shape",
232+ resourceId: "res_1",
233+ node: {
234+ type: "scalar",
235+ scalar: "integer",
236+ },
237+ synthetic: true,
238+ provenance: [],
239+ },
240+ },
241+ },
242+ typeProjector: {} as LoadedSourceCatalogTool["typeProjector"],
243+} as LoadedSourceCatalogTool;
244+
245+describe("source inspection", () => {
246+ it.effect("keeps the shared TypeScript contract first-class on tool detail", () =>
247+ Effect.gen(function* () {
248+ const detail = yield* inspectionToolDetailFromTool(
249+ structuredClone(baseLoadedSourceCatalogTool),
250+ );
247251
248252 expect(detail.summary.path).toBe("linear.administrableTeams");
249253 expect(detail.summary.method).toBe("query");
@@ -275,4 +279,28 @@ describe("source inspection", () => { · 25 unmodified lines
275279 ]),
276280 );
277281 }));
282+
283+ it.effect("truncates oversized executable sections to keep tool detail payload bounded", () =>
284+ Effect.gen(function* () {
285+ const tool = structuredClone(baseLoadedSourceCatalogTool);
286+ tool.executable.binding = {
287+ payload: "x".repeat(250_000),
288+ };
289+
290+ const detail = yield* inspectionToolDetailFromTool(tool);
291+ const executableSection = detail.sections.find(
292+ (section) => section.kind === "code" && section.title === "Executable",
293+ );
294+
295+ expect(executableSection).toBeDefined();
296+ if (!executableSection || executableSection.kind !== "code") {
297+ throw new Error("Expected Executable code section");
298+ }
299+
300+ expect(executableSection.body).toContain(
301+ "Executable truncated for inspection UI",
302+ );
303+ expect(executableSection.body.length).toBeLessThanOrEqual(100_128);
304+ expect(executableSection.body).toContain("\"payload\":\"");
305+ }));
278306 });
packages/platform/sdk/src/sources/inspection.ts+57−20
@@ -39,6 +39,9 @@ const sourceInspectOps = { · 38 unmodified lines
3939 discover: operationErrors("sources.inspect.discover"),
4040 } as const;
4141
42+const PRETTY_JSON_SECTION_MAX_CHARS = 20_000;
43+const MAX_CODE_SECTION_CHARS = 100_000;
44+
4245 const tokenize = (value: string): Array<string> =>
4346 value
4447 .trim()
@@ -172,18 +175,50 @@ const nativeEncodingLanguage = (encoding: string | undefined): string => · 127 unmodified lines
172175 ? encoding
173176 : "json";
174177
178+const truncatedCodeBody = (input: {
179+ title: string;
180+ body: string;
181+}): string =>
182+ input.body.length <= MAX_CODE_SECTION_CHARS
183+ ? input.body
184+ : [
185+ `[${input.title} truncated for inspection UI: ${String(input.body.length)} chars total, showing first ${String(MAX_CODE_SECTION_CHARS)}]`,
186+ input.body.slice(0, MAX_CODE_SECTION_CHARS),
187+ ].join("\n");
188+
189+const serializedJsonSectionBody = (title: string, value: unknown): string => {
190+ const compact = JSON.stringify(value);
191+ if (compact.length <= PRETTY_JSON_SECTION_MAX_CHARS) {
192+ return JSON.stringify(value, null, 2);
193+ }
194+
195+ return truncatedCodeBody({
196+ title,
197+ body: compact,
198+ });
199+};
200+
201+const codeSection = (
202+ title: string,
203+ language: string,
204+ body: string,
205+): SourceInspectionToolDetail["sections"][number] => ({
206+ kind: "code",
207+ title,
208+ language,
209+ body: truncatedCodeBody({
210+ title,
211+ body,
212+ }),
213+});
214+
175215 const jsonSection = (
176216 title: string,
177217 value: unknown | null | undefined,
178218 ): SourceInspectionToolDetail["sections"][number] | null =>
179219 value === null || value === undefined
180220 ? null
181− : {
182− kind: "code",
183− title,
184− language: "json",
185− body: JSON.stringify(value, null, 2),
186− };
221+ : codeSection(title, "json", serializedJsonSectionBody(title, value));
187222
188223 export const inspectionToolDetailFromTool = (
189224 tool: LoadedSourceCatalogTool,
@@ -220,24 +255,26 @@ export const inspectionToolDetailFromTool = ( · 30 unmodified lines
220255 { label: "Response set", value: contract.responseSetId, mono: true },
221256 ];
222257 const nativeSections = [
223− ...(tool.capability.native ?? []).map((blob, index) => ({
224− kind: "code" as const,
225− title: `Capability native ${String(index + 1)}: ${blob.kind}`,
226− language: nativeEncodingLanguage(blob.encoding),
227− body:
258+ ...(tool.capability.native ?? []).map((blob, index) => {
259+ const title = `Capability native ${String(index + 1)}: ${blob.kind}`;
260+ return codeSection(
261+ title,
262+ nativeEncodingLanguage(blob.encoding),
228263 typeof blob.value === "string"
229264 ? blob.value
230− : JSON.stringify(blob.value ?? null, null, 2),
231− })),
232− ...(tool.executable.native ?? []).map((blob, index) => ({
233− kind: "code" as const,
234− title: `Executable native ${String(index + 1)}: ${blob.kind}`,
235− language: nativeEncodingLanguage(blob.encoding),
236− body:
265+ : serializedJsonSectionBody(title, blob.value ?? null),
266+ );
267+ }),
268+ ...(tool.executable.native ?? []).map((blob, index) => {
269+ const title = `Executable native ${String(index + 1)}: ${blob.kind}`;
270+ return codeSection(
271+ title,
272+ nativeEncodingLanguage(blob.encoding),
237273 typeof blob.value === "string"
238274 ? blob.value
239− : JSON.stringify(blob.value ?? null, null, 2),
240− })),
275+ : serializedJsonSectionBody(title, blob.value ?? null),
276+ );
277+ }),
241278 ];
242279 const sections = [
243280 {
plugins/mcp/sdk/catalog.test.ts+35−0
@@ -101,4 +101,39 @@ describe("MCP catalog", () => { · 100 unmodified lines
101101 dataVariants.some((shape) => shape?.kind === "shape" && shape.node.type === "unknown"),
102102 ).toBe(true);
103103 });
104+
105+ it("stores lean executable bindings without dropping catalog semantics", () => {
106+ const snapshot = createMcpCatalogSnapshot({
107+ source: createSource(),
108+ documents: [{
109+ documentKind: "mcp_manifest",
110+ documentKey: "https://mcp.axiom.co/mcp",
111+ contentText: JSON.stringify({ version: 2, tools: [] }),
112+ }],
113+ operations: [createOperation()],
114+ });
115+
116+ const executable = Object.values(snapshot.catalog.executables)[0];
117+ if (!executable) {
118+ throw new Error("Expected MCP executable");
119+ }
120+
121+ expect(executable.binding).toEqual({
122+ toolId: "createdashboard",
123+ toolName: "createDashboard",
124+ });
125+ expect(executable.binding).not.toHaveProperty("rawTool");
126+ expect(executable.display?.title).toBe("createDashboard");
127+
128+ const capability = snapshot.catalog.capabilities[executable.capabilityId];
129+ if (!capability) {
130+ throw new Error("Expected MCP capability");
131+ }
132+
133+ expect(capability.surface.title).toBe("createDashboard");
134+ expect(capability.surface.summary).toBe(
135+ "Create a new dashboard from a JSON document.",
136+ );
137+ expect(capability.interaction.resume.supported).toBe(false);
138+ });
104139 });
plugins/mcp/sdk/catalog.ts+2−1
@@ -32,6 +32,7 @@ import { · 31 unmodified lines
3232 toolPathSegments,
3333 } from "@executor/source-core";
3434
35+import { mcpExecutableBindingFromProviderData } from "./executable-binding";
3536 import type {
3637 McpServerMetadata,
3738 McpToolAnnotations,
@@ -192,7 +193,7 @@ const createMcpCapability = (input: { · 154 unmodified lines
192193 scopeId: input.serviceScopeId,
193194 pluginKey: "mcp",
194195 bindingVersion: EXECUTABLE_BINDING_VERSION,
195− binding: input.operation.providerData,
196+ binding: mcpExecutableBindingFromProviderData(input.operation.providerData),
196197 projection: {
197198 responseSetId,
198199 callShapeId,
plugins/mcp/sdk/executable-binding.test.ts+44−0
@@ -0,0 +1,44 @@
1+import { describe, expect, it } from "@effect/vitest";
2+import * as Schema from "effect/Schema";
3+
4+import { McpExecutableBindingSchema } from "./executable-binding";
5+
6+describe("MCP executable binding", () => {
7+ it("accepts legacy verbose bindings and strips manifest-only metadata", () => {
8+ const decode = Schema.decodeUnknownSync(McpExecutableBindingSchema);
9+
10+ expect(
11+ decode({
12+ toolId: "read_file",
13+ toolName: "Read File",
14+ displayTitle: "Read File",
15+ title: "Read File",
16+ description: "Read a file",
17+ annotations: {
18+ readOnlyHint: true,
19+ },
20+ execution: {
21+ taskSupport: "optional",
22+ },
23+ icons: [{
24+ src: "https://example.test/icon.png",
25+ }],
26+ meta: {
27+ category: "filesystem",
28+ },
29+ rawTool: {
30+ name: "Read File",
31+ },
32+ server: {
33+ info: {
34+ name: "mcp-server",
35+ version: "1.0.0",
36+ },
37+ },
38+ }),
39+ ).toEqual({
40+ toolId: "read_file",
41+ toolName: "Read File",
42+ });
43+ });
44+});
plugins/mcp/sdk/executable-binding.ts+16−0
@@ -0,0 +1,16 @@
1+import * as Schema from "effect/Schema";
2+
3+export const McpExecutableBindingSchema = Schema.Struct({
4+ toolId: Schema.String,
5+ toolName: Schema.String,
6+});
7+
8+export type McpExecutableBinding = typeof McpExecutableBindingSchema.Type;
9+
10+export const mcpExecutableBindingFromProviderData = (input: {
11+ toolId: string;
12+ toolName: string;
13+}): McpExecutableBinding => ({
14+ toolId: input.toolId,
15+ toolName: input.toolName,
16+});
plugins/mcp/sdk/index.ts+5−25
@@ -74,6 +74,10 @@ import { · 73 unmodified lines
7474 createMcpCatalogFragment,
7575 type McpCatalogOperationInput,
7676 } from "./catalog";
77+import {
78+ McpExecutableBindingSchema,
79+ type McpExecutableBinding,
80+} from "./executable-binding";
7781 import {
7882 detectMcpSource,
7983 } from "./discovery";
@@ -107,22 +111,6 @@ const McpCompleteOAuthInputSchema = Schema.Struct({ · 27 unmodified lines
107111 errorDescription: Schema.optional(Schema.String),
108112 });
109113
110−const McpExecutableBindingSchema = Schema.Struct({
111− toolId: Schema.String,
112− toolName: Schema.String,
113− displayTitle: Schema.String,
114− title: Schema.NullOr(Schema.String),
115− description: Schema.NullOr(Schema.String),
116− annotations: Schema.NullOr(Schema.Unknown),
117− execution: Schema.NullOr(Schema.Unknown),
118− icons: Schema.NullOr(Schema.Unknown),
119− meta: Schema.NullOr(Schema.Unknown),
120− rawTool: Schema.NullOr(Schema.Unknown),
121− server: Schema.NullOr(Schema.Unknown),
122−});
123−
124−type McpExecutableBinding = typeof McpExecutableBindingSchema.Type;
125−
126114 const decodeProviderData = Schema.decodeUnknownSync(McpExecutableBindingSchema);
127115
128116 export type McpSourceStorage = {
@@ -824,19 +812,11 @@ export const mcpSdkPlugin = ( · 695 unmodified lines
824812 });
825813 const manifest = {
826814 version: 2 as const,
827− server: providerData.server as McpServerMetadata | null,
828815 tools: [
829816 {
830817 toolId: providerData.toolId,
831818 toolName: providerData.toolName,
832− displayTitle: providerData.displayTitle,
833− title: providerData.title,
834− description: providerData.description,
835− annotations: providerData.annotations as McpToolManifestEntry["annotations"],
836− execution: providerData.execution as McpToolManifestEntry["execution"],
837− icons: providerData.icons as McpToolManifestEntry["icons"],
838− meta: providerData.meta,
839− rawTool: providerData.rawTool,
819+ description: input.descriptor.description ?? null,
840820 inputSchema: input.descriptor.contract?.inputSchema,
841821 outputSchema: input.descriptor.contract?.outputSchema,
842822 },
plugins/openapi/sdk/catalog.ts+15−1
@@ -49,6 +49,7 @@ import type { · 48 unmodified lines
4949 OpenApiMediaContent,
5050 OpenApiSecurityRequirement,
5151 OpenApiSecurityScheme,
52+ OpenApiExecutableBinding,
5253 OpenApiToolProviderData,
5354 OpenApiToolDefinition,
5455 } from "./types";
@@ -791,7 +792,20 @@ const createHttpCapabilityFromOpenApi = (input: { · 736 unmodified lines
791792 })(),
792793 pluginKey: "openapi",
793794 bindingVersion: EXECUTABLE_BINDING_VERSION,
794− binding: input.operation.providerData,
795+ binding: {
796+ kind: "openapi",
797+ toolId: input.operation.providerData.toolId,
798+ ...(input.operation.providerData.operationId
799+ ? { operationId: input.operation.providerData.operationId }
800+ : {}),
801+ invocation: input.operation.providerData.invocation,
802+ ...(input.operation.providerData.documentServers
803+ ? { documentServers: input.operation.providerData.documentServers }
804+ : {}),
805+ ...(input.operation.providerData.servers
806+ ? { servers: input.operation.providerData.servers }
807+ : {}),
808+ } satisfies OpenApiExecutableBinding,
795809 projection: {
796810 responseSetId,
797811 callShapeId,
plugins/openapi/sdk/definitions.ts+12−0
@@ -1,4 +1,5 @@
11 import type {
2+ OpenApiExecutableBinding,
23 OpenApiExtractedTool,
34 OpenApiToolDefinition,
45 OpenApiToolManifest,
@@ -247,3 +248,14 @@ export const openApiProviderDataFromDefinition = ( · 242 unmodified lines
247248 documentServers: definition.documentServers,
248249 servers: definition.servers,
249250 });
251+
252+export const openApiExecutableBindingFromDefinition = (
253+ definition: OpenApiToolDefinition,
254+): OpenApiExecutableBinding => ({
255+ kind: "openapi",
256+ toolId: definition.toolId,
257+ operationId: definition.operationId,
258+ invocation: definition.invocation,
259+ documentServers: definition.documentServers,
260+ servers: definition.servers,
261+});
plugins/openapi/sdk/extraction.ts+42−1
@@ -199,8 +199,15 @@ const loadDereferencedOpenApiDocument = async (input: { · 198 unmodified lines
199199 currentDocument: OpenApiJsonObject;
200200 currentDocumentUrl?: string;
201201 activeRefs: ReadonlySet<string>;
202+ preserveLocalRefs: boolean;
202203 }): Promise<unknown> => {
203− const { value, currentDocument, currentDocumentUrl, activeRefs } = inputValue;
204+ const {
205+ value,
206+ currentDocument,
207+ currentDocumentUrl,
208+ activeRefs,
209+ preserveLocalRefs,
210+ } = inputValue;
204211
205212 if (Array.isArray(value)) {
206213 return Promise.all(
@@ -210,6 +217,7 @@ const loadDereferencedOpenApiDocument = async (input: { · 3 unmodified lines
210217 currentDocument,
211218 currentDocumentUrl,
212219 activeRefs,
220+ preserveLocalRefs,
213221 }),
214222 ),
215223 );
@@ -227,6 +235,35 @@ const loadDereferencedOpenApiDocument = async (input: { · 11 unmodified lines
227235 return value;
228236 }
229237
238+ const isLocalRef =
239+ ref.startsWith("#") &&
240+ (target?.documentUrl === undefined || target.documentUrl === currentDocumentUrl);
241+ if (isLocalRef && preserveLocalRefs) {
242+ const siblingEntries = Object.fromEntries(
243+ await Promise.all(
244+ Object.entries(object)
245+ .filter(([key]) => key !== "$ref")
246+ .map(async ([key, entry]) => [
247+ key,
248+ await dereference({
249+ value: entry,
250+ currentDocument,
251+ currentDocumentUrl,
252+ activeRefs,
253+ preserveLocalRefs,
254+ }),
255+ ]),
256+ ),
257+ );
258+
259+ return Object.keys(siblingEntries).length > 0
260+ ? {
261+ $ref: ref,
262+ ...siblingEntries,
263+ }
264+ : value;
265+ }
266+
230267 const activeKey = `${target?.documentUrl ?? currentDocumentUrl ?? "root"}|${target?.pointer ?? ref}`;
231268 if (activeRefs.has(activeKey)) {
232269 return value;
@@ -249,6 +286,7 @@ const loadDereferencedOpenApiDocument = async (input: { · 16 unmodified lines
249286 currentDocument: targetDocument,
250287 currentDocumentUrl: target?.documentUrl ?? currentDocumentUrl,
251288 activeRefs: nextActiveRefs,
289+ preserveLocalRefs: target?.documentUrl ? false : preserveLocalRefs,
252290 });
253291 const siblingEntries = Object.fromEntries(
254292 await Promise.all(
@@ -261,6 +299,7 @@ const loadDereferencedOpenApiDocument = async (input: { · 6 unmodified lines
261299 currentDocument,
262300 currentDocumentUrl,
263301 activeRefs: nextActiveRefs,
302+ preserveLocalRefs,
264303 }),
265304 ]),
266305 ),
@@ -281,6 +320,7 @@ const loadDereferencedOpenApiDocument = async (input: { · 14 unmodified lines
281320 currentDocument,
282321 currentDocumentUrl,
283322 activeRefs,
323+ preserveLocalRefs,
284324 }),
285325 ]),
286326 ),
@@ -292,6 +332,7 @@ const loadDereferencedOpenApiDocument = async (input: { · 5 unmodified lines
292332 currentDocument: input.document,
293333 currentDocumentUrl: input.documentUrl,
294334 activeRefs: new Set<string>(),
335+ preserveLocalRefs: true,
295336 }) as Promise<OpenApiJsonObject>;
296337 };
297338
plugins/openapi/sdk/index.ts+12−9
@@ -57,8 +57,8 @@ import { · 56 unmodified lines
5757 withSerializedQueryEntries,
5858 } from "./http-serialization";
5959 import {
60− OpenApiToolProviderDataSchema,
61− type OpenApiToolProviderData,
60+ OpenApiExecutableBindingSchema,
61+ type OpenApiExecutableBinding,
6262 } from "./types";
6363
6464 const OpenApiSourceIdInputSchema = Schema.Struct({
@@ -206,7 +206,9 @@ const openApiStoredSourceDataFromLocalConfig = (input: { · 141 unmodified lines
206206 throw new Error("Unsupported OpenAPI local source config.");
207207 };
208208
209−const decodeProviderData = Schema.decodeUnknownSync(OpenApiToolProviderDataSchema);
209+const decodeExecutableBinding = Schema.decodeUnknownSync(
210+ OpenApiExecutableBindingSchema,
211+);
210212
211213 const asRecord = (value: unknown): Record<string, unknown> =>
212214 typeof value === "object" && value !== null && !Array.isArray(value)
@@ -214,7 +216,8 @@ const asRecord = (value: unknown): Record<string, unknown> => · 1 unmodified line
214216 : {};
215217
216218 type OpenApiToolArgs = Record<string, unknown>;
217−type OpenApiToolParameter = OpenApiToolProviderData["invocation"]["parameters"][number];
219+type OpenApiToolParameter =
220+ OpenApiExecutableBinding["invocation"]["parameters"][number];
218221
219222 const parameterContainerKeys: Record<
220223 OpenApiToolParameter["location"],
@@ -257,7 +260,7 @@ const readParameterValue = ( · 36 unmodified lines
257260 const replacePathTemplate = (
258261 pathTemplate: string,
259262 args: OpenApiToolArgs,
260− payload: OpenApiToolProviderData["invocation"],
263+ payload: OpenApiExecutableBinding["invocation"],
261264 ): string => {
262265 let resolvedPath = pathTemplate;
263266
@@ -297,7 +300,7 @@ const replacePathTemplate = ( · 33 unmodified lines
297300
298301 const resolveOpenApiBaseUrl = (input: {
299302 stored: OpenApiStoredSourceData;
300− providerData: OpenApiToolProviderData;
303+ providerData: OpenApiExecutableBinding;
301304 }): string => {
302305 if (input.stored.baseUrl && input.stored.baseUrl.trim().length > 0) {
303306 return new URL(input.stored.baseUrl).toString();
@@ -641,9 +644,9 @@ export const openApiSdkPlugin = ( · 337 unmodified lines
641644 }
642645
643646 const normalizedStored = normalizeStoredSourceData(input.stored);
644− const providerData = decodeProviderData(
647+ const providerData = decodeExecutableBinding(
645648 input.executable.binding,
646− ) as OpenApiToolProviderData;
649+ ) as OpenApiExecutableBinding;
647650 const args = asRecord(input.args);
648651 const resolvedPath = replacePathTemplate(
649652 providerData.invocation.pathTemplate,
@@ -739,7 +742,7 @@ export const openApiSdkPlugin = ( · 89 unmodified lines
739742 const response = yield* Effect.tryPromise({
740743 try: () =>
741744 fetch(finalUrl.toString(), {
742− method: providerData.method.toUpperCase(),
745+ method: providerData.invocation.method.toUpperCase(),
743746 headers: requestHeaders,
744747 ...(body !== undefined
745748 ? {
plugins/openapi/sdk/types.ts+11−0
@@ -215,6 +215,15 @@ export const OpenApiToolProviderDataSchema = Schema.Struct({ · 214 unmodified lines
215215 servers: Schema.optional(Schema.Array(OpenApiServerSchema)),
216216 });
217217
218+export const OpenApiExecutableBindingSchema = Schema.Struct({
219+ kind: Schema.Literal("openapi"),
220+ toolId: Schema.String,
221+ operationId: Schema.optional(Schema.String),
222+ invocation: OpenApiInvocationPayloadSchema,
223+ documentServers: Schema.optional(Schema.Array(OpenApiServerSchema)),
224+ servers: Schema.optional(Schema.Array(OpenApiServerSchema)),
225+});
226+
218227 export type OpenApiHttpMethod = typeof OpenApiHttpMethodSchema.Type;
219228 export type OpenApiExample = typeof OpenApiExampleSchema.Type;
220229 export type OpenApiMediaContent = typeof OpenApiMediaContentSchema.Type;
@@ -237,6 +246,8 @@ export type OpenApiResponseVariant = · 16 unmodified lines
237246 typeof OpenApiResponseVariantSchema.Type;
238247 export type OpenApiToolProviderData =
239248 typeof OpenApiToolProviderDataSchema.Type;
249+export type OpenApiExecutableBinding =
250+ typeof OpenApiExecutableBindingSchema.Type;
240251
241252 export type OpenApiJsonPrimitive = string | number | boolean | null;
242253 export type OpenApiJsonValue =