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 | const makeSource = (input: { | 168 | const makeSource = (input: { |
| 99 | id?: string; | 169 | id?: string; |
| 100 | name?: string; | 170 | name?: string; |
| @@ -290,4 +360,57 @@ describe("openapi type contracts", () => { · 189 unmodified lines | |||
| 290 | expect(contract.output.typeDeclaration).not.toContain("data: unknown"); | 360 | expect(contract.output.typeDeclaration).not.toContain("data: unknown"); |
| 291 | expect(contract.output.typePreview).toContain("data: null"); | 361 | expect(contract.output.typePreview).toContain("data: null"); |
| 292 | }).pipe(Effect.provide(NodeFileSystem.layer))); | 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 | inspectionToolDetailFromTool, | 12 | inspectionToolDetailFromTool, |
| 13 | } from "../../sources/inspection"; | 13 | } from "../../sources/inspection"; |
| 14 | 14 | ||
| 15 | describe("source inspection", () => { | 15 | const baseLoadedSourceCatalogTool = { |
| 16 | it.effect("keeps the shared TypeScript contract first-class on tool detail", () => | 16 | path: "linear.administrableTeams", |
| 17 | Effect.gen(function* () { | 17 | source: { |
| 18 | const detail = yield* inspectionToolDetailFromTool({ | 18 | id: "linear", |
| 19 | path: "linear.administrableTeams", | 19 | }, |
| 20 | source: { | 20 | capability: { |
| 21 | id: "linear", | 21 | surface: { |
| 22 | }, | 22 | title: "Administrable Teams", |
| 23 | capability: { | 23 | summary: "All teams the user can administrate.", |
| 24 | surface: { | 24 | description: "All teams the user can administrate.", |
| 25 | title: "Administrable Teams", | 25 | tags: ["query"], |
| 26 | summary: "All teams the user can administrate.", | 26 | }, |
| 27 | description: "All teams the user can administrate.", | 27 | native: [], |
| 28 | tags: ["query"], | 28 | }, |
| 29 | }, | 29 | executable: { |
| 30 | native: [], | 30 | id: "exec_graphql_administrableTeams", |
| 31 | }, | 31 | pluginKey: "graphql", |
| 32 | executable: { | 32 | bindingVersion: 1, |
| 33 | id: "exec_graphql_administrableTeams", | 33 | binding: { |
| 34 | pluginKey: "graphql", | 34 | operationKind: "query", |
| 35 | bindingVersion: 1, | 35 | rootTypeName: "Query", |
| 36 | binding: { | 36 | fieldName: "administrableTeams", |
| 37 | operationKind: "query", | 37 | }, |
| 38 | rootTypeName: "Query", | 38 | projection: { |
| 39 | fieldName: "administrableTeams", | 39 | callShapeId: "shape_call", |
| 40 | }, | 40 | resultDataShapeId: "shape_result_data", |
| 41 | projection: { | 41 | resultErrorShapeId: "shape_result_error", |
| 42 | callShapeId: "shape_call", | 42 | responseSetId: "response_set_1", |
| 43 | resultDataShapeId: "shape_result_data", | 43 | }, |
| 44 | resultErrorShapeId: "shape_result_error", | 44 | display: { |
| 45 | responseSetId: "response_set_1", | 45 | protocol: "graphql", |
| 46 | }, | 46 | method: "query", |
| 47 | display: { | 47 | pathTemplate: "administrableTeams", |
| 48 | protocol: "graphql", | 48 | operationId: "administrableTeams", |
| 49 | method: "query", | 49 | group: "query", |
| 50 | pathTemplate: "administrableTeams", | 50 | leaf: "administrableTeams", |
| 51 | operationId: "administrableTeams", | 51 | rawToolId: "administrableTeams", |
| 52 | group: "query", | 52 | }, |
| 53 | leaf: "administrableTeams", | 53 | native: [], |
| 54 | rawToolId: "administrableTeams", | 54 | }, |
| 55 | }, | 55 | projectedDescriptor: { |
| 56 | native: [], | 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: { | 69 | required: ["input"], |
| 59 | toolPath: ["linear", "administrableTeams"], | 70 | }, |
| 60 | callShapeId: "shape_call", | 71 | outputSchema: { |
| 61 | resultShapeId: "shape_result", | 72 | type: "object", |
| 62 | responseSetId: "response_set_1", | 73 | properties: { |
| 74 | data: { type: ["string", "null"] }, | ||
| 63 | }, | 75 | }, |
| 64 | descriptor: { | 76 | required: ["data"], |
| 65 | inputTypePreview: "{ input: string }", | 77 | }, |
| 66 | outputTypePreview: "{ data: string | null; error: unknown | null; headers: Record<string, string>; status: number | null }", | 78 | }, |
| 67 | inputSchema: { | 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 | type: "object", | 95 | type: "object", |
| 69 | properties: { | 96 | fields: { |
| 70 | input: { type: "string" }, | 97 | input: { |
| 98 | shapeId: "shape_call_input", | ||
| 99 | docs: { | ||
| 100 | description: "The exact input string to filter by.", | ||
| 101 | }, | ||
| 102 | }, | ||
| 71 | }, | 103 | }, |
| 72 | required: ["input"], | 104 | required: ["input"], |
| 105 | additionalProperties: false, | ||
| 73 | }, | 106 | }, |
| 74 | outputSchema: { | 107 | synthetic: false, |
| 75 | type: "object", | 108 | provenance: [], |
| 76 | properties: { | 109 | }, |
| 77 | data: { type: ["string", "null"] }, | 110 | shape_call_input: { |
| 78 | }, | 111 | id: "shape_call_input", |
| 79 | required: ["data"], | 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: { | 122 | shape_result: { |
| 83 | version: "ir.v1", | 123 | id: "shape_result", |
| 84 | documents: {}, | 124 | kind: "shape", |
| 85 | resources: {}, | 125 | resourceId: "res_1", |
| 86 | scopes: {}, | 126 | title: "AdministrableTeamsResult", |
| 87 | capabilities: {}, | 127 | node: { |
| 88 | executables: {}, | 128 | type: "object", |
| 89 | responseSets: {}, | 129 | fields: { |
| 90 | diagnostics: {}, | 130 | data: { |
| 91 | symbols: { | 131 | shapeId: "shape_result_data_nullable", |
| 92 | shape_call: { | 132 | docs: { |
| 93 | id: "shape_call", | 133 | description: "Matching team data when present.", |
| 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 | }, | ||
| 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, | 136 | error: { |
| 175 | provenance: [], | 137 | shapeId: "shape_result_error_nullable", |
| 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", | ||
| 206 | }, | 138 | }, |
| 207 | synthetic: true, | 139 | headers: { |
| 208 | provenance: [], | 140 | shapeId: "shape_result_headers", |
| 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", | ||
| 217 | }, | 141 | }, |
| 218 | synthetic: true, | 142 | status: { |
| 219 | provenance: [], | 143 | shapeId: "shape_result_status_nullable", |
| 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", | ||
| 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"], | 229 | shape_result_status: { |
| 246 | } as LoadedSourceCatalogTool); | 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 | expect(detail.summary.path).toBe("linear.administrableTeams"); | 252 | expect(detail.summary.path).toBe("linear.administrableTeams"); |
| 249 | expect(detail.summary.method).toBe("query"); | 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 | discover: operationErrors("sources.inspect.discover"), | 39 | discover: operationErrors("sources.inspect.discover"), |
| 40 | } as const; | 40 | } as const; |
| 41 | 41 | ||
| 42 | const PRETTY_JSON_SECTION_MAX_CHARS = 20_000; | ||
| 43 | const MAX_CODE_SECTION_CHARS = 100_000; | ||
| 44 | |||
| 42 | const tokenize = (value: string): Array<string> => | 45 | const tokenize = (value: string): Array<string> => |
| 43 | value | 46 | value |
| 44 | .trim() | 47 | .trim() |
| @@ -172,18 +175,50 @@ const nativeEncodingLanguage = (encoding: string | undefined): string => · 127 unmodified lines | |||
| 172 | ? encoding | 175 | ? encoding |
| 173 | : "json"; | 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 | const jsonSection = ( | 215 | const jsonSection = ( |
| 176 | title: string, | 216 | title: string, |
| 177 | value: unknown | null | undefined, | 217 | value: unknown | null | undefined, |
| 178 | ): SourceInspectionToolDetail["sections"][number] | null => | 218 | ): SourceInspectionToolDetail["sections"][number] | null => |
| 179 | value === null || value === undefined | 219 | value === null || value === undefined |
| 180 | ? null | 220 | ? null |
| 181 | : { | 221 | : codeSection(title, "json", serializedJsonSectionBody(title, value)); |
| 182 | kind: "code", | ||
| 183 | title, | ||
| 184 | language: "json", | ||
| 185 | body: JSON.stringify(value, null, 2), | ||
| 186 | }; | ||
| 187 | 222 | ||
| 188 | export const inspectionToolDetailFromTool = ( | 223 | export const inspectionToolDetailFromTool = ( |
| 189 | tool: LoadedSourceCatalogTool, | 224 | tool: LoadedSourceCatalogTool, |
| @@ -220,24 +255,26 @@ export const inspectionToolDetailFromTool = ( · 30 unmodified lines | |||
| 220 | { label: "Response set", value: contract.responseSetId, mono: true }, | 255 | { label: "Response set", value: contract.responseSetId, mono: true }, |
| 221 | ]; | 256 | ]; |
| 222 | const nativeSections = [ | 257 | const nativeSections = [ |
| 223 | ...(tool.capability.native ?? []).map((blob, index) => ({ | 258 | ...(tool.capability.native ?? []).map((blob, index) => { |
| 224 | kind: "code" as const, | 259 | const title = `Capability native ${String(index + 1)}: ${blob.kind}`; |
| 225 | title: `Capability native ${String(index + 1)}: ${blob.kind}`, | 260 | return codeSection( |
| 226 | language: nativeEncodingLanguage(blob.encoding), | 261 | title, |
| 227 | body: | 262 | nativeEncodingLanguage(blob.encoding), |
| 228 | typeof blob.value === "string" | 263 | typeof blob.value === "string" |
| 229 | ? blob.value | 264 | ? blob.value |
| 230 | : JSON.stringify(blob.value ?? null, null, 2), | 265 | : serializedJsonSectionBody(title, blob.value ?? null), |
| 231 | })), | 266 | ); |
| 232 | ...(tool.executable.native ?? []).map((blob, index) => ({ | 267 | }), |
| 233 | kind: "code" as const, | 268 | ...(tool.executable.native ?? []).map((blob, index) => { |
| 234 | title: `Executable native ${String(index + 1)}: ${blob.kind}`, | 269 | const title = `Executable native ${String(index + 1)}: ${blob.kind}`; |
| 235 | language: nativeEncodingLanguage(blob.encoding), | 270 | return codeSection( |
| 236 | body: | 271 | title, |
| 272 | nativeEncodingLanguage(blob.encoding), | ||
| 237 | typeof blob.value === "string" | 273 | typeof blob.value === "string" |
| 238 | ? blob.value | 274 | ? blob.value |
| 239 | : JSON.stringify(blob.value ?? null, null, 2), | 275 | : serializedJsonSectionBody(title, blob.value ?? null), |
| 240 | })), | 276 | ); |
| 277 | }), | ||
| 241 | ]; | 278 | ]; |
| 242 | const sections = [ | 279 | const sections = [ |
| 243 | { | 280 | { |
| @@ -101,4 +101,39 @@ describe("MCP catalog", () => { · 100 unmodified lines | |||
| 101 | dataVariants.some((shape) => shape?.kind === "shape" && shape.node.type === "unknown"), | 101 | dataVariants.some((shape) => shape?.kind === "shape" && shape.node.type === "unknown"), |
| 102 | ).toBe(true); | 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 | toolPathSegments, | 32 | toolPathSegments, |
| 33 | } from "@executor/source-core"; | 33 | } from "@executor/source-core"; |
| 34 | 34 | ||
| 35 | import { mcpExecutableBindingFromProviderData } from "./executable-binding"; | ||
| 35 | import type { | 36 | import type { |
| 36 | McpServerMetadata, | 37 | McpServerMetadata, |
| 37 | McpToolAnnotations, | 38 | McpToolAnnotations, |
| @@ -192,7 +193,7 @@ const createMcpCapability = (input: { · 154 unmodified lines | |||
| 192 | scopeId: input.serviceScopeId, | 193 | scopeId: input.serviceScopeId, |
| 193 | pluginKey: "mcp", | 194 | pluginKey: "mcp", |
| 194 | bindingVersion: EXECUTABLE_BINDING_VERSION, | 195 | bindingVersion: EXECUTABLE_BINDING_VERSION, |
| 195 | binding: input.operation.providerData, | 196 | binding: mcpExecutableBindingFromProviderData(input.operation.providerData), |
| 196 | projection: { | 197 | projection: { |
| 197 | responseSetId, | 198 | responseSetId, |
| 198 | callShapeId, | 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 | createMcpCatalogFragment, | 74 | createMcpCatalogFragment, |
| 75 | type McpCatalogOperationInput, | 75 | type McpCatalogOperationInput, |
| 76 | } from "./catalog"; | 76 | } from "./catalog"; |
| 77 | import { | ||
| 78 | McpExecutableBindingSchema, | ||
| 79 | type McpExecutableBinding, | ||
| 80 | } from "./executable-binding"; | ||
| 77 | import { | 81 | import { |
| 78 | detectMcpSource, | 82 | detectMcpSource, |
| 79 | } from "./discovery"; | 83 | } from "./discovery"; |
| @@ -107,22 +111,6 @@ const McpCompleteOAuthInputSchema = Schema.Struct({ · 27 unmodified lines | |||
| 107 | errorDescription: Schema.optional(Schema.String), | 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 | const decodeProviderData = Schema.decodeUnknownSync(McpExecutableBindingSchema); | 114 | const decodeProviderData = Schema.decodeUnknownSync(McpExecutableBindingSchema); |
| 127 | 115 | ||
| 128 | export type McpSourceStorage = { | 116 | export type McpSourceStorage = { |
| @@ -824,19 +812,11 @@ export const mcpSdkPlugin = ( · 695 unmodified lines | |||
| 824 | }); | 812 | }); |
| 825 | const manifest = { | 813 | const manifest = { |
| 826 | version: 2 as const, | 814 | version: 2 as const, |
| 827 | server: providerData.server as McpServerMetadata | null, | ||
| 828 | tools: [ | 815 | tools: [ |
| 829 | { | 816 | { |
| 830 | toolId: providerData.toolId, | 817 | toolId: providerData.toolId, |
| 831 | toolName: providerData.toolName, | 818 | toolName: providerData.toolName, |
| 832 | displayTitle: providerData.displayTitle, | 819 | description: input.descriptor.description ?? null, |
| 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, | ||
| 840 | inputSchema: input.descriptor.contract?.inputSchema, | 820 | inputSchema: input.descriptor.contract?.inputSchema, |
| 841 | outputSchema: input.descriptor.contract?.outputSchema, | 821 | outputSchema: input.descriptor.contract?.outputSchema, |
| 842 | }, | 822 | }, |
| @@ -49,6 +49,7 @@ import type { · 48 unmodified lines | |||
| 49 | OpenApiMediaContent, | 49 | OpenApiMediaContent, |
| 50 | OpenApiSecurityRequirement, | 50 | OpenApiSecurityRequirement, |
| 51 | OpenApiSecurityScheme, | 51 | OpenApiSecurityScheme, |
| 52 | OpenApiExecutableBinding, | ||
| 52 | OpenApiToolProviderData, | 53 | OpenApiToolProviderData, |
| 53 | OpenApiToolDefinition, | 54 | OpenApiToolDefinition, |
| 54 | } from "./types"; | 55 | } from "./types"; |
| @@ -791,7 +792,20 @@ const createHttpCapabilityFromOpenApi = (input: { · 736 unmodified lines | |||
| 791 | })(), | 792 | })(), |
| 792 | pluginKey: "openapi", | 793 | pluginKey: "openapi", |
| 793 | bindingVersion: EXECUTABLE_BINDING_VERSION, | 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 | projection: { | 809 | projection: { |
| 796 | responseSetId, | 810 | responseSetId, |
| 797 | callShapeId, | 811 | callShapeId, |
| @@ -1,4 +1,5 @@ | |||
| 1 | import type { | 1 | import type { |
| 2 | OpenApiExecutableBinding, | ||
| 2 | OpenApiExtractedTool, | 3 | OpenApiExtractedTool, |
| 3 | OpenApiToolDefinition, | 4 | OpenApiToolDefinition, |
| 4 | OpenApiToolManifest, | 5 | OpenApiToolManifest, |
| @@ -247,3 +248,14 @@ export const openApiProviderDataFromDefinition = ( · 242 unmodified lines | |||
| 247 | documentServers: definition.documentServers, | 248 | documentServers: definition.documentServers, |
| 248 | servers: definition.servers, | 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 | currentDocument: OpenApiJsonObject; | 199 | currentDocument: OpenApiJsonObject; |
| 200 | currentDocumentUrl?: string; | 200 | currentDocumentUrl?: string; |
| 201 | activeRefs: ReadonlySet<string>; | 201 | activeRefs: ReadonlySet<string>; |
| 202 | preserveLocalRefs: boolean; | ||
| 202 | }): Promise<unknown> => { | 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 | if (Array.isArray(value)) { | 212 | if (Array.isArray(value)) { |
| 206 | return Promise.all( | 213 | return Promise.all( |
| @@ -210,6 +217,7 @@ const loadDereferencedOpenApiDocument = async (input: { · 3 unmodified lines | |||
| 210 | currentDocument, | 217 | currentDocument, |
| 211 | currentDocumentUrl, | 218 | currentDocumentUrl, |
| 212 | activeRefs, | 219 | activeRefs, |
| 220 | preserveLocalRefs, | ||
| 213 | }), | 221 | }), |
| 214 | ), | 222 | ), |
| 215 | ); | 223 | ); |
| @@ -227,6 +235,35 @@ const loadDereferencedOpenApiDocument = async (input: { · 11 unmodified lines | |||
| 227 | return value; | 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 | const activeKey = `${target?.documentUrl ?? currentDocumentUrl ?? "root"}|${target?.pointer ?? ref}`; | 267 | const activeKey = `${target?.documentUrl ?? currentDocumentUrl ?? "root"}|${target?.pointer ?? ref}`; |
| 231 | if (activeRefs.has(activeKey)) { | 268 | if (activeRefs.has(activeKey)) { |
| 232 | return value; | 269 | return value; |
| @@ -249,6 +286,7 @@ const loadDereferencedOpenApiDocument = async (input: { · 16 unmodified lines | |||
| 249 | currentDocument: targetDocument, | 286 | currentDocument: targetDocument, |
| 250 | currentDocumentUrl: target?.documentUrl ?? currentDocumentUrl, | 287 | currentDocumentUrl: target?.documentUrl ?? currentDocumentUrl, |
| 251 | activeRefs: nextActiveRefs, | 288 | activeRefs: nextActiveRefs, |
| 289 | preserveLocalRefs: target?.documentUrl ? false : preserveLocalRefs, | ||
| 252 | }); | 290 | }); |
| 253 | const siblingEntries = Object.fromEntries( | 291 | const siblingEntries = Object.fromEntries( |
| 254 | await Promise.all( | 292 | await Promise.all( |
| @@ -261,6 +299,7 @@ const loadDereferencedOpenApiDocument = async (input: { · 6 unmodified lines | |||
| 261 | currentDocument, | 299 | currentDocument, |
| 262 | currentDocumentUrl, | 300 | currentDocumentUrl, |
| 263 | activeRefs: nextActiveRefs, | 301 | activeRefs: nextActiveRefs, |
| 302 | preserveLocalRefs, | ||
| 264 | }), | 303 | }), |
| 265 | ]), | 304 | ]), |
| 266 | ), | 305 | ), |
| @@ -281,6 +320,7 @@ const loadDereferencedOpenApiDocument = async (input: { · 14 unmodified lines | |||
| 281 | currentDocument, | 320 | currentDocument, |
| 282 | currentDocumentUrl, | 321 | currentDocumentUrl, |
| 283 | activeRefs, | 322 | activeRefs, |
| 323 | preserveLocalRefs, | ||
| 284 | }), | 324 | }), |
| 285 | ]), | 325 | ]), |
| 286 | ), | 326 | ), |
| @@ -292,6 +332,7 @@ const loadDereferencedOpenApiDocument = async (input: { · 5 unmodified lines | |||
| 292 | currentDocument: input.document, | 332 | currentDocument: input.document, |
| 293 | currentDocumentUrl: input.documentUrl, | 333 | currentDocumentUrl: input.documentUrl, |
| 294 | activeRefs: new Set<string>(), | 334 | activeRefs: new Set<string>(), |
| 335 | preserveLocalRefs: true, | ||
| 295 | }) as Promise<OpenApiJsonObject>; | 336 | }) as Promise<OpenApiJsonObject>; |
| 296 | }; | 337 | }; |
| 297 | 338 | ||
| @@ -57,8 +57,8 @@ import { · 56 unmodified lines | |||
| 57 | withSerializedQueryEntries, | 57 | withSerializedQueryEntries, |
| 58 | } from "./http-serialization"; | 58 | } from "./http-serialization"; |
| 59 | import { | 59 | import { |
| 60 | OpenApiToolProviderDataSchema, | 60 | OpenApiExecutableBindingSchema, |
| 61 | type OpenApiToolProviderData, | 61 | type OpenApiExecutableBinding, |
| 62 | } from "./types"; | 62 | } from "./types"; |
| 63 | 63 | ||
| 64 | const OpenApiSourceIdInputSchema = Schema.Struct({ | 64 | const OpenApiSourceIdInputSchema = Schema.Struct({ |
| @@ -206,7 +206,9 @@ const openApiStoredSourceDataFromLocalConfig = (input: { · 141 unmodified lines | |||
| 206 | throw new Error("Unsupported OpenAPI local source config."); | 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 | const asRecord = (value: unknown): Record<string, unknown> => | 213 | const asRecord = (value: unknown): Record<string, unknown> => |
| 212 | typeof value === "object" && value !== null && !Array.isArray(value) | 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 | type OpenApiToolArgs = Record<string, unknown>; | 218 | type OpenApiToolArgs = Record<string, unknown>; |
| 217 | type OpenApiToolParameter = OpenApiToolProviderData["invocation"]["parameters"][number]; | 219 | type OpenApiToolParameter = |
| 220 | OpenApiExecutableBinding["invocation"]["parameters"][number]; | ||
| 218 | 221 | ||
| 219 | const parameterContainerKeys: Record< | 222 | const parameterContainerKeys: Record< |
| 220 | OpenApiToolParameter["location"], | 223 | OpenApiToolParameter["location"], |
| @@ -257,7 +260,7 @@ const readParameterValue = ( · 36 unmodified lines | |||
| 257 | const replacePathTemplate = ( | 260 | const replacePathTemplate = ( |
| 258 | pathTemplate: string, | 261 | pathTemplate: string, |
| 259 | args: OpenApiToolArgs, | 262 | args: OpenApiToolArgs, |
| 260 | payload: OpenApiToolProviderData["invocation"], | 263 | payload: OpenApiExecutableBinding["invocation"], |
| 261 | ): string => { | 264 | ): string => { |
| 262 | let resolvedPath = pathTemplate; | 265 | let resolvedPath = pathTemplate; |
| 263 | 266 | ||
| @@ -297,7 +300,7 @@ const replacePathTemplate = ( · 33 unmodified lines | |||
| 297 | 300 | ||
| 298 | const resolveOpenApiBaseUrl = (input: { | 301 | const resolveOpenApiBaseUrl = (input: { |
| 299 | stored: OpenApiStoredSourceData; | 302 | stored: OpenApiStoredSourceData; |
| 300 | providerData: OpenApiToolProviderData; | 303 | providerData: OpenApiExecutableBinding; |
| 301 | }): string => { | 304 | }): string => { |
| 302 | if (input.stored.baseUrl && input.stored.baseUrl.trim().length > 0) { | 305 | if (input.stored.baseUrl && input.stored.baseUrl.trim().length > 0) { |
| 303 | return new URL(input.stored.baseUrl).toString(); | 306 | return new URL(input.stored.baseUrl).toString(); |
| @@ -641,9 +644,9 @@ export const openApiSdkPlugin = ( · 337 unmodified lines | |||
| 641 | } | 644 | } |
| 642 | 645 | ||
| 643 | const normalizedStored = normalizeStoredSourceData(input.stored); | 646 | const normalizedStored = normalizeStoredSourceData(input.stored); |
| 644 | const providerData = decodeProviderData( | 647 | const providerData = decodeExecutableBinding( |
| 645 | input.executable.binding, | 648 | input.executable.binding, |
| 646 | ) as OpenApiToolProviderData; | 649 | ) as OpenApiExecutableBinding; |
| 647 | const args = asRecord(input.args); | 650 | const args = asRecord(input.args); |
| 648 | const resolvedPath = replacePathTemplate( | 651 | const resolvedPath = replacePathTemplate( |
| 649 | providerData.invocation.pathTemplate, | 652 | providerData.invocation.pathTemplate, |
| @@ -739,7 +742,7 @@ export const openApiSdkPlugin = ( · 89 unmodified lines | |||
| 739 | const response = yield* Effect.tryPromise({ | 742 | const response = yield* Effect.tryPromise({ |
| 740 | try: () => | 743 | try: () => |
| 741 | fetch(finalUrl.toString(), { | 744 | fetch(finalUrl.toString(), { |
| 742 | method: providerData.method.toUpperCase(), | 745 | method: providerData.invocation.method.toUpperCase(), |
| 743 | headers: requestHeaders, | 746 | headers: requestHeaders, |
| 744 | ...(body !== undefined | 747 | ...(body !== undefined |
| 745 | ? { | 748 | ? { |
| @@ -215,6 +215,15 @@ export const OpenApiToolProviderDataSchema = Schema.Struct({ · 214 unmodified lines | |||
| 215 | servers: Schema.optional(Schema.Array(OpenApiServerSchema)), | 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 | export type OpenApiHttpMethod = typeof OpenApiHttpMethodSchema.Type; | 227 | export type OpenApiHttpMethod = typeof OpenApiHttpMethodSchema.Type; |
| 219 | export type OpenApiExample = typeof OpenApiExampleSchema.Type; | 228 | export type OpenApiExample = typeof OpenApiExampleSchema.Type; |
| 220 | export type OpenApiMediaContent = typeof OpenApiMediaContentSchema.Type; | 229 | export type OpenApiMediaContent = typeof OpenApiMediaContentSchema.Type; |
| @@ -237,6 +246,8 @@ export type OpenApiResponseVariant = · 16 unmodified lines | |||
| 237 | typeof OpenApiResponseVariantSchema.Type; | 246 | typeof OpenApiResponseVariantSchema.Type; |
| 238 | export type OpenApiToolProviderData = | 247 | export type OpenApiToolProviderData = |
| 239 | typeof OpenApiToolProviderDataSchema.Type; | 248 | typeof OpenApiToolProviderDataSchema.Type; |
| 249 | export type OpenApiExecutableBinding = | ||
| 250 | typeof OpenApiExecutableBindingSchema.Type; | ||
| 240 | 251 | ||
| 241 | export type OpenApiJsonPrimitive = string | number | boolean | null; | 252 | export type OpenApiJsonPrimitive = string | number | boolean | null; |
| 242 | export type OpenApiJsonValue = | 253 | export type OpenApiJsonValue = |