Unified · Split
| @@ -95,6 +95,76 @@ const noContentSuccessOpenApiFixture = JSON.stringify({ · 94 unmodified lines | ||
| 95 | 95 | }, |
| 96 | 96 | }); |
| 97 | 97 | |
| 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 | + | |
| 98 | 168 | const makeSource = (input: { |
| 99 | 169 | id?: string; |
| 100 | 170 | name?: string; |
| @@ -290,4 +360,57 @@ describe("openapi type contracts", () => { · 189 unmodified lines | ||
| 290 | 360 | expect(contract.output.typeDeclaration).not.toContain("data: unknown"); |
| 291 | 361 | expect(contract.output.typePreview).toContain("data: null"); |
| 292 | 362 | }).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 | + })); | |
| 293 | 416 | }); |
| @@ -12,238 +12,242 @@ import { · 11 unmodified lines | ||
| 12 | 12 | inspectionToolDetailFromTool, |
| 13 | 13 | } from "../../sources/inspection"; |
| 14 | 14 | |
| 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" }, | |
| 57 | 68 | }, |
| 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"] }, | |
| 63 | 75 | }, |
| 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: { | |
| 68 | 95 | 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 | + }, | |
| 71 | 103 | }, |
| 72 | 104 | required: ["input"], |
| 105 | + additionalProperties: false, | |
| 73 | 106 | }, |
| 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", | |
| 80 | 118 | }, |
| 119 | + synthetic: false, | |
| 120 | + provenance: [], | |
| 81 | 121 | }, |
| 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.", | |
| 148 | 134 | }, |
| 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", | |
| 173 | 135 | }, |
| 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", | |
| 206 | 138 | }, |
| 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", | |
| 217 | 141 | }, |
| 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", | |
| 228 | 144 | }, |
| 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: [], | |
| 242 | 145 | }, |
| 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", | |
| 243 | 225 | }, |
| 226 | + synthetic: true, | |
| 227 | + provenance: [], | |
| 244 | 228 | }, |
| 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 | + ); | |
| 247 | 251 | |
| 248 | 252 | expect(detail.summary.path).toBe("linear.administrableTeams"); |
| 249 | 253 | expect(detail.summary.method).toBe("query"); |
| @@ -275,4 +279,28 @@ describe("source inspection", () => { · 25 unmodified lines | ||
| 275 | 279 | ]), |
| 276 | 280 | ); |
| 277 | 281 | })); |
| 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 | + })); | |
| 278 | 306 | }); |
| @@ -39,6 +39,9 @@ const sourceInspectOps = { · 38 unmodified lines | ||
| 39 | 39 | discover: operationErrors("sources.inspect.discover"), |
| 40 | 40 | } as const; |
| 41 | 41 | |
| 42 | +const PRETTY_JSON_SECTION_MAX_CHARS = 20_000; | |
| 43 | +const MAX_CODE_SECTION_CHARS = 100_000; | |
| 44 | + | |
| 42 | 45 | const tokenize = (value: string): Array<string> => |
| 43 | 46 | value |
| 44 | 47 | .trim() |
| @@ -172,18 +175,50 @@ const nativeEncodingLanguage = (encoding: string | undefined): string => · 127 unmodified lines | ||
| 172 | 175 | ? encoding |
| 173 | 176 | : "json"; |
| 174 | 177 | |
| 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 | + | |
| 175 | 215 | const jsonSection = ( |
| 176 | 216 | title: string, |
| 177 | 217 | value: unknown | null | undefined, |
| 178 | 218 | ): SourceInspectionToolDetail["sections"][number] | null => |
| 179 | 219 | value === null || value === undefined |
| 180 | 220 | ? 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)); | |
| 187 | 222 | |
| 188 | 223 | export const inspectionToolDetailFromTool = ( |
| 189 | 224 | tool: LoadedSourceCatalogTool, |
| @@ -220,24 +255,26 @@ export const inspectionToolDetailFromTool = ( · 30 unmodified lines | ||
| 220 | 255 | { label: "Response set", value: contract.responseSetId, mono: true }, |
| 221 | 256 | ]; |
| 222 | 257 | 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), | |
| 228 | 263 | typeof blob.value === "string" |
| 229 | 264 | ? 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), | |
| 237 | 273 | typeof blob.value === "string" |
| 238 | 274 | ? blob.value |
| 239 | − : JSON.stringify(blob.value ?? null, null, 2), | |
| 240 | − })), | |
| 275 | + : serializedJsonSectionBody(title, blob.value ?? null), | |
| 276 | + ); | |
| 277 | + }), | |
| 241 | 278 | ]; |
| 242 | 279 | const sections = [ |
| 243 | 280 | { |
| @@ -101,4 +101,39 @@ describe("MCP catalog", () => { · 100 unmodified lines | ||
| 101 | 101 | dataVariants.some((shape) => shape?.kind === "shape" && shape.node.type === "unknown"), |
| 102 | 102 | ).toBe(true); |
| 103 | 103 | }); |
| 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 | + }); | |
| 104 | 139 | }); |
| @@ -32,6 +32,7 @@ import { · 31 unmodified lines | ||
| 32 | 32 | toolPathSegments, |
| 33 | 33 | } from "@executor/source-core"; |
| 34 | 34 | |
| 35 | +import { mcpExecutableBindingFromProviderData } from "./executable-binding"; | |
| 35 | 36 | import type { |
| 36 | 37 | McpServerMetadata, |
| 37 | 38 | McpToolAnnotations, |
| @@ -192,7 +193,7 @@ const createMcpCapability = (input: { · 154 unmodified lines | ||
| 192 | 193 | scopeId: input.serviceScopeId, |
| 193 | 194 | pluginKey: "mcp", |
| 194 | 195 | bindingVersion: EXECUTABLE_BINDING_VERSION, |
| 195 | − binding: input.operation.providerData, | |
| 196 | + binding: mcpExecutableBindingFromProviderData(input.operation.providerData), | |
| 196 | 197 | projection: { |
| 197 | 198 | responseSetId, |
| 198 | 199 | callShapeId, |
| @@ -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 | +}); | |
| @@ -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 | +}); | |
| @@ -74,6 +74,10 @@ import { · 73 unmodified lines | ||
| 74 | 74 | createMcpCatalogFragment, |
| 75 | 75 | type McpCatalogOperationInput, |
| 76 | 76 | } from "./catalog"; |
| 77 | +import { | |
| 78 | + McpExecutableBindingSchema, | |
| 79 | + type McpExecutableBinding, | |
| 80 | +} from "./executable-binding"; | |
| 77 | 81 | import { |
| 78 | 82 | detectMcpSource, |
| 79 | 83 | } from "./discovery"; |
| @@ -107,22 +111,6 @@ const McpCompleteOAuthInputSchema = Schema.Struct({ · 27 unmodified lines | ||
| 107 | 111 | errorDescription: Schema.optional(Schema.String), |
| 108 | 112 | }); |
| 109 | 113 | |
| 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 | − | |
| 126 | 114 | const decodeProviderData = Schema.decodeUnknownSync(McpExecutableBindingSchema); |
| 127 | 115 | |
| 128 | 116 | export type McpSourceStorage = { |
| @@ -824,19 +812,11 @@ export const mcpSdkPlugin = ( · 695 unmodified lines | ||
| 824 | 812 | }); |
| 825 | 813 | const manifest = { |
| 826 | 814 | version: 2 as const, |
| 827 | − server: providerData.server as McpServerMetadata | null, | |
| 828 | 815 | tools: [ |
| 829 | 816 | { |
| 830 | 817 | toolId: providerData.toolId, |
| 831 | 818 | 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, | |
| 840 | 820 | inputSchema: input.descriptor.contract?.inputSchema, |
| 841 | 821 | outputSchema: input.descriptor.contract?.outputSchema, |
| 842 | 822 | }, |
| @@ -49,6 +49,7 @@ import type { · 48 unmodified lines | ||
| 49 | 49 | OpenApiMediaContent, |
| 50 | 50 | OpenApiSecurityRequirement, |
| 51 | 51 | OpenApiSecurityScheme, |
| 52 | + OpenApiExecutableBinding, | |
| 52 | 53 | OpenApiToolProviderData, |
| 53 | 54 | OpenApiToolDefinition, |
| 54 | 55 | } from "./types"; |
| @@ -791,7 +792,20 @@ const createHttpCapabilityFromOpenApi = (input: { · 736 unmodified lines | ||
| 791 | 792 | })(), |
| 792 | 793 | pluginKey: "openapi", |
| 793 | 794 | 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, | |
| 795 | 809 | projection: { |
| 796 | 810 | responseSetId, |
| 797 | 811 | callShapeId, |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | import type { |
| 2 | + OpenApiExecutableBinding, | |
| 2 | 3 | OpenApiExtractedTool, |
| 3 | 4 | OpenApiToolDefinition, |
| 4 | 5 | OpenApiToolManifest, |
| @@ -247,3 +248,14 @@ export const openApiProviderDataFromDefinition = ( · 242 unmodified lines | ||
| 247 | 248 | documentServers: definition.documentServers, |
| 248 | 249 | servers: definition.servers, |
| 249 | 250 | }); |
| 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 | +}); | |
| @@ -199,8 +199,15 @@ const loadDereferencedOpenApiDocument = async (input: { · 198 unmodified lines | ||
| 199 | 199 | currentDocument: OpenApiJsonObject; |
| 200 | 200 | currentDocumentUrl?: string; |
| 201 | 201 | activeRefs: ReadonlySet<string>; |
| 202 | + preserveLocalRefs: boolean; | |
| 202 | 203 | }): Promise<unknown> => { |
| 203 | − const { value, currentDocument, currentDocumentUrl, activeRefs } = inputValue; | |
| 204 | + const { | |
| 205 | + value, | |
| 206 | + currentDocument, | |
| 207 | + currentDocumentUrl, | |
| 208 | + activeRefs, | |
| 209 | + preserveLocalRefs, | |
| 210 | + } = inputValue; | |
| 204 | 211 | |
| 205 | 212 | if (Array.isArray(value)) { |
| 206 | 213 | return Promise.all( |
| @@ -210,6 +217,7 @@ const loadDereferencedOpenApiDocument = async (input: { · 3 unmodified lines | ||
| 210 | 217 | currentDocument, |
| 211 | 218 | currentDocumentUrl, |
| 212 | 219 | activeRefs, |
| 220 | + preserveLocalRefs, | |
| 213 | 221 | }), |
| 214 | 222 | ), |
| 215 | 223 | ); |
| @@ -227,6 +235,35 @@ const loadDereferencedOpenApiDocument = async (input: { · 11 unmodified lines | ||
| 227 | 235 | return value; |
| 228 | 236 | } |
| 229 | 237 | |
| 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 | + | |
| 230 | 267 | const activeKey = `${target?.documentUrl ?? currentDocumentUrl ?? "root"}|${target?.pointer ?? ref}`; |
| 231 | 268 | if (activeRefs.has(activeKey)) { |
| 232 | 269 | return value; |
| @@ -249,6 +286,7 @@ const loadDereferencedOpenApiDocument = async (input: { · 16 unmodified lines | ||
| 249 | 286 | currentDocument: targetDocument, |
| 250 | 287 | currentDocumentUrl: target?.documentUrl ?? currentDocumentUrl, |
| 251 | 288 | activeRefs: nextActiveRefs, |
| 289 | + preserveLocalRefs: target?.documentUrl ? false : preserveLocalRefs, | |
| 252 | 290 | }); |
| 253 | 291 | const siblingEntries = Object.fromEntries( |
| 254 | 292 | await Promise.all( |
| @@ -261,6 +299,7 @@ const loadDereferencedOpenApiDocument = async (input: { · 6 unmodified lines | ||
| 261 | 299 | currentDocument, |
| 262 | 300 | currentDocumentUrl, |
| 263 | 301 | activeRefs: nextActiveRefs, |
| 302 | + preserveLocalRefs, | |
| 264 | 303 | }), |
| 265 | 304 | ]), |
| 266 | 305 | ), |
| @@ -281,6 +320,7 @@ const loadDereferencedOpenApiDocument = async (input: { · 14 unmodified lines | ||
| 281 | 320 | currentDocument, |
| 282 | 321 | currentDocumentUrl, |
| 283 | 322 | activeRefs, |
| 323 | + preserveLocalRefs, | |
| 284 | 324 | }), |
| 285 | 325 | ]), |
| 286 | 326 | ), |
| @@ -292,6 +332,7 @@ const loadDereferencedOpenApiDocument = async (input: { · 5 unmodified lines | ||
| 292 | 332 | currentDocument: input.document, |
| 293 | 333 | currentDocumentUrl: input.documentUrl, |
| 294 | 334 | activeRefs: new Set<string>(), |
| 335 | + preserveLocalRefs: true, | |
| 295 | 336 | }) as Promise<OpenApiJsonObject>; |
| 296 | 337 | }; |
| 297 | 338 | |
| @@ -57,8 +57,8 @@ import { · 56 unmodified lines | ||
| 57 | 57 | withSerializedQueryEntries, |
| 58 | 58 | } from "./http-serialization"; |
| 59 | 59 | import { |
| 60 | − OpenApiToolProviderDataSchema, | |
| 61 | − type OpenApiToolProviderData, | |
| 60 | + OpenApiExecutableBindingSchema, | |
| 61 | + type OpenApiExecutableBinding, | |
| 62 | 62 | } from "./types"; |
| 63 | 63 | |
| 64 | 64 | const OpenApiSourceIdInputSchema = Schema.Struct({ |
| @@ -206,7 +206,9 @@ const openApiStoredSourceDataFromLocalConfig = (input: { · 141 unmodified lines | ||
| 206 | 206 | throw new Error("Unsupported OpenAPI local source config."); |
| 207 | 207 | }; |
| 208 | 208 | |
| 209 | −const decodeProviderData = Schema.decodeUnknownSync(OpenApiToolProviderDataSchema); | |
| 209 | +const decodeExecutableBinding = Schema.decodeUnknownSync( | |
| 210 | + OpenApiExecutableBindingSchema, | |
| 211 | +); | |
| 210 | 212 | |
| 211 | 213 | const asRecord = (value: unknown): Record<string, unknown> => |
| 212 | 214 | typeof value === "object" && value !== null && !Array.isArray(value) |
| @@ -214,7 +216,8 @@ const asRecord = (value: unknown): Record<string, unknown> => · 1 unmodified line | ||
| 214 | 216 | : {}; |
| 215 | 217 | |
| 216 | 218 | type OpenApiToolArgs = Record<string, unknown>; |
| 217 | −type OpenApiToolParameter = OpenApiToolProviderData["invocation"]["parameters"][number]; | |
| 219 | +type OpenApiToolParameter = | |
| 220 | + OpenApiExecutableBinding["invocation"]["parameters"][number]; | |
| 218 | 221 | |
| 219 | 222 | const parameterContainerKeys: Record< |
| 220 | 223 | OpenApiToolParameter["location"], |
| @@ -257,7 +260,7 @@ const readParameterValue = ( · 36 unmodified lines | ||
| 257 | 260 | const replacePathTemplate = ( |
| 258 | 261 | pathTemplate: string, |
| 259 | 262 | args: OpenApiToolArgs, |
| 260 | − payload: OpenApiToolProviderData["invocation"], | |
| 263 | + payload: OpenApiExecutableBinding["invocation"], | |
| 261 | 264 | ): string => { |
| 262 | 265 | let resolvedPath = pathTemplate; |
| 263 | 266 | |
| @@ -297,7 +300,7 @@ const replacePathTemplate = ( · 33 unmodified lines | ||
| 297 | 300 | |
| 298 | 301 | const resolveOpenApiBaseUrl = (input: { |
| 299 | 302 | stored: OpenApiStoredSourceData; |
| 300 | − providerData: OpenApiToolProviderData; | |
| 303 | + providerData: OpenApiExecutableBinding; | |
| 301 | 304 | }): string => { |
| 302 | 305 | if (input.stored.baseUrl && input.stored.baseUrl.trim().length > 0) { |
| 303 | 306 | return new URL(input.stored.baseUrl).toString(); |
| @@ -641,9 +644,9 @@ export const openApiSdkPlugin = ( · 337 unmodified lines | ||
| 641 | 644 | } |
| 642 | 645 | |
| 643 | 646 | const normalizedStored = normalizeStoredSourceData(input.stored); |
| 644 | − const providerData = decodeProviderData( | |
| 647 | + const providerData = decodeExecutableBinding( | |
| 645 | 648 | input.executable.binding, |
| 646 | − ) as OpenApiToolProviderData; | |
| 649 | + ) as OpenApiExecutableBinding; | |
| 647 | 650 | const args = asRecord(input.args); |
| 648 | 651 | const resolvedPath = replacePathTemplate( |
| 649 | 652 | providerData.invocation.pathTemplate, |
| @@ -739,7 +742,7 @@ export const openApiSdkPlugin = ( · 89 unmodified lines | ||
| 739 | 742 | const response = yield* Effect.tryPromise({ |
| 740 | 743 | try: () => |
| 741 | 744 | fetch(finalUrl.toString(), { |
| 742 | − method: providerData.method.toUpperCase(), | |
| 745 | + method: providerData.invocation.method.toUpperCase(), | |
| 743 | 746 | headers: requestHeaders, |
| 744 | 747 | ...(body !== undefined |
| 745 | 748 | ? { |
| @@ -215,6 +215,15 @@ export const OpenApiToolProviderDataSchema = Schema.Struct({ · 214 unmodified lines | ||
| 215 | 215 | servers: Schema.optional(Schema.Array(OpenApiServerSchema)), |
| 216 | 216 | }); |
| 217 | 217 | |
| 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 | + | |
| 218 | 227 | export type OpenApiHttpMethod = typeof OpenApiHttpMethodSchema.Type; |
| 219 | 228 | export type OpenApiExample = typeof OpenApiExampleSchema.Type; |
| 220 | 229 | export type OpenApiMediaContent = typeof OpenApiMediaContentSchema.Type; |
| @@ -237,6 +246,8 @@ export type OpenApiResponseVariant = · 16 unmodified lines | ||
| 237 | 246 | typeof OpenApiResponseVariantSchema.Type; |
| 238 | 247 | export type OpenApiToolProviderData = |
| 239 | 248 | typeof OpenApiToolProviderDataSchema.Type; |
| 249 | +export type OpenApiExecutableBinding = | |
| 250 | + typeof OpenApiExecutableBindingSchema.Type; | |
| 240 | 251 | |
| 241 | 252 | export type OpenApiJsonPrimitive = string | number | boolean | null; |
| 242 | 253 | export type OpenApiJsonValue = |