Extract planHttpInvocation, planGraphqlInvocation, and planMcpInvocation from their execute counterparts. The new planIrToolInvocation builds an InvocationPlan that captures the resolved request details, and executeIrInvocationPlan dispatches it. invokeIrTool now composes both steps, keeping the existing call-site API unchanged.
Unified · Split
| @@ -21,6 +21,7 @@ import type { · 20 unmodified lines | |||
| 21 | ToolInput, | 21 | ToolInput, |
| 22 | ToolPath, | 22 | ToolPath, |
| 23 | } from "@executor/codemode-core"; | 23 | } from "@executor/codemode-core"; |
| 24 | import { buildInvocationPlan, type InvocationPlan } from "../ir/catalog"; | ||
| 24 | import type { | 25 | import type { |
| 25 | CatalogV1, | 26 | CatalogV1, |
| 26 | Capability, | 27 | Capability, |
| @@ -72,6 +73,43 @@ type ExecutionEnvelope = { · 45 unmodified lines | |||
| 72 | status: number | null; | 73 | status: number | null; |
| 73 | }; | 74 | }; |
| 74 | 75 | ||
| 76 | type HttpInvocationRequestPlan = { | ||
| 77 | protocol: "http"; | ||
| 78 | method: HttpExecutable["method"]; | ||
| 79 | url: string; | ||
| 80 | headers: Record<string, string>; | ||
| 81 | body?: string; | ||
| 82 | }; | ||
| 83 | |||
| 84 | type GraphqlInvocationRequestPlan = { | ||
| 85 | protocol: "graphql"; | ||
| 86 | endpoint: string; | ||
| 87 | headers: Record<string, string>; | ||
| 88 | query: string; | ||
| 89 | variables?: Record<string, unknown>; | ||
| 90 | operationName?: string; | ||
| 91 | responseRootField?: string; | ||
| 92 | }; | ||
| 93 | |||
| 94 | type McpInvocationRequestPlan = { | ||
| 95 | protocol: "mcp"; | ||
| 96 | endpoint: string; | ||
| 97 | transport?: "auto" | "streamable-http" | "sse"; | ||
| 98 | queryParams: Record<string, string>; | ||
| 99 | headers: Record<string, string>; | ||
| 100 | toolName: string; | ||
| 101 | payload: unknown; | ||
| 102 | inputSchema: unknown; | ||
| 103 | outputSchema: unknown; | ||
| 104 | toolTitle?: string; | ||
| 105 | toolSummary?: string; | ||
| 106 | }; | ||
| 107 | |||
| 108 | type IrInvocationRequestPlan = | ||
| 109 | | HttpInvocationRequestPlan | ||
| 110 | | GraphqlInvocationRequestPlan | ||
| 111 | | McpInvocationRequestPlan; | ||
| 112 | |||
| 75 | const decodeFetchBody = async (response: Response): Promise<unknown> => { | 113 | const decodeFetchBody = async (response: Response): Promise<unknown> => { |
| 76 | const contentType = response.headers.get("content-type")?.toLowerCase() ?? ""; | 114 | const contentType = response.headers.get("content-type")?.toLowerCase() ?? ""; |
| 77 | if (contentType.includes("application/json")) { | 115 | if (contentType.includes("application/json")) { |
| @@ -93,6 +131,16 @@ const responseHeadersRecord = (response: Response): Record<string, string> => { · 15 unmodified lines | |||
| 93 | 131 | ||
| 94 | const executionEnvelope = (input: ExecutionEnvelope): ExecutionEnvelope => input; | 132 | const executionEnvelope = (input: ExecutionEnvelope): ExecutionEnvelope => input; |
| 95 | 133 | ||
| 134 | const resolvedAuthSummary = (auth: ResolvedSourceAuthMaterial) => ({ | ||
| 135 | placementCount: auth.placements.length, | ||
| 136 | headerKeys: Object.keys(auth.headers).sort(), | ||
| 137 | queryParamKeys: Object.keys(auth.queryParams).sort(), | ||
| 138 | cookieKeys: Object.keys(auth.cookies).sort(), | ||
| 139 | bodyValueKeys: Object.keys(auth.bodyValues).sort(), | ||
| 140 | expiresAt: auth.expiresAt, | ||
| 141 | refreshAfter: auth.refreshAfter, | ||
| 142 | }); | ||
| 143 | |||
| 96 | const parameterById = (catalog: CatalogV1, parameterId: string): ParameterSymbol | undefined => { | 144 | const parameterById = (catalog: CatalogV1, parameterId: string): ParameterSymbol | undefined => { |
| 97 | const symbol = catalog.symbols[parameterId]; | 145 | const symbol = catalog.symbols[parameterId]; |
| 98 | return symbol?.kind === "parameter" ? symbol : undefined; | 146 | return symbol?.kind === "parameter" ? symbol : undefined; |
| @@ -203,133 +251,149 @@ const resolveHttpRequestUrl = (baseUrl: URL, resolvedPath: string): URL => { · 104 unmodified lines | |||
| 203 | } | 251 | } |
| 204 | }; | 252 | }; |
| 205 | 253 | ||
| 206 | const executeHttp = (input: { | 254 | const planHttpInvocation = (input: { |
| 207 | source: Source; | 255 | source: Source; |
| 208 | catalog: CatalogV1; | 256 | catalog: CatalogV1; |
| 209 | executable: HttpExecutable; | 257 | executable: HttpExecutable; |
| 210 | auth: ResolvedSourceAuthMaterial; | 258 | auth: ResolvedSourceAuthMaterial; |
| 211 | args: unknown; | 259 | args: unknown; |
| 212 | }) => | 260 | }): { |
| 213 | Effect.tryPromise({ | 261 | resolvedEndpoint: string; |
| 214 | try: async () => { | 262 | requestPlan: HttpInvocationRequestPlan; |
| 215 | const argsRecord = asObject(input.args); | 263 | } => { |
| 216 | let resolvedPath = input.executable.pathTemplate; | 264 | const argsRecord = asObject(input.args); |
| 217 | const url = applyHttpQueryPlacementsToUrl({ | 265 | let resolvedPath = input.executable.pathTemplate; |
| 218 | url: resolveHttpBaseUrl(input.source, input.catalog, input.executable), | 266 | const url = applyHttpQueryPlacementsToUrl({ |
| 219 | queryParams: readSourceQueryParams(input.source), | 267 | url: resolveHttpBaseUrl(input.source, input.catalog, input.executable), |
| 220 | }); | 268 | queryParams: readSourceQueryParams(input.source), |
| 221 | const headers: Record<string, string> = { | 269 | }); |
| 222 | ...readSourceHeaders(input.source), | 270 | const headers: Record<string, string> = { |
| 223 | }; | 271 | ...readSourceHeaders(input.source), |
| 224 | const queryEntries: Array<{ name: string; value: string; allowReserved?: boolean }> = []; | 272 | }; |
| 225 | const cookieParts: string[] = []; | 273 | const queryEntries: Array<{ name: string; value: string; allowReserved?: boolean }> = []; |
| 226 | 274 | const cookieParts: string[] = []; | |
| 227 | const allParameterIds = [ | 275 | |
| 228 | ...(input.executable.pathParameterIds ?? []), | 276 | const allParameterIds = [ |
| 229 | ...(input.executable.queryParameterIds ?? []), | 277 | ...(input.executable.pathParameterIds ?? []), |
| 230 | ...(input.executable.headerParameterIds ?? []), | 278 | ...(input.executable.queryParameterIds ?? []), |
| 231 | ...(input.executable.cookieParameterIds ?? []), | 279 | ...(input.executable.headerParameterIds ?? []), |
| 232 | ]; | 280 | ...(input.executable.cookieParameterIds ?? []), |
| 233 | 281 | ]; | |
| 234 | for (const parameterId of allParameterIds) { | 282 | |
| 235 | const parameter = parameterById(input.catalog, parameterId); | 283 | for (const parameterId of allParameterIds) { |
| 236 | if (!parameter) { | 284 | const parameter = parameterById(input.catalog, parameterId); |
| 237 | continue; | 285 | if (!parameter) { |
| 238 | } | 286 | continue; |
| 239 | 287 | } | |
| 240 | const value = readHttpParameterValue({ | ||
| 241 | catalog: input.catalog, | ||
| 242 | executable: input.executable, | ||
| 243 | parameter, | ||
| 244 | args: argsRecord, | ||
| 245 | }); | ||
| 246 | if (value === undefined || value === null) { | ||
| 247 | if (parameter.required) { | ||
| 248 | throw new Error(`Missing required ${parameter.location} parameter ${parameter.name}`); | ||
| 249 | } | ||
| 250 | continue; | ||
| 251 | } | ||
| 252 | |||
| 253 | const serialized = serializeOpenApiParameterValue({ | ||
| 254 | name: parameter.name, | ||
| 255 | location: parameter.location, | ||
| 256 | style: parameter.style, | ||
| 257 | explode: parameter.explode, | ||
| 258 | allowReserved: parameter.allowReserved, | ||
| 259 | content: parameter.content?.map((content) => ({ mediaType: content.mediaType })), | ||
| 260 | }, value); | ||
| 261 | |||
| 262 | if (serialized.kind === "path") { | ||
| 263 | resolvedPath = resolvedPath.replace( | ||
| 264 | new RegExp(`{${parameter.name}}`, "g"), | ||
| 265 | serialized.value, | ||
| 266 | ); | ||
| 267 | continue; | ||
| 268 | } | ||
| 269 | |||
| 270 | if (serialized.kind === "query") { | ||
| 271 | queryEntries.push(...serialized.entries); | ||
| 272 | continue; | ||
| 273 | } | ||
| 274 | |||
| 275 | if (serialized.kind === "header") { | ||
| 276 | headers[parameter.name] = serialized.value; | ||
| 277 | continue; | ||
| 278 | } | ||
| 279 | |||
| 280 | if (serialized.kind === "cookie") { | ||
| 281 | cookieParts.push( | ||
| 282 | ...serialized.pairs.map((pair) => `${pair.name}=${encodeURIComponent(pair.value)}`), | ||
| 283 | ); | ||
| 284 | } | ||
| 285 | } | ||
| 286 | 288 | ||
| 287 | const bodySymbol = input.executable.requestBodyId | 289 | const value = readHttpParameterValue({ |
| 288 | ? input.catalog.symbols[input.executable.requestBodyId] | 290 | catalog: input.catalog, |
| 289 | : undefined; | 291 | executable: input.executable, |
| 290 | let body: string | undefined; | 292 | parameter, |
| 291 | if (bodySymbol?.kind === "requestBody") { | 293 | args: argsRecord, |
| 292 | const bodyValue = argsRecord.body ?? argsRecord.input; | 294 | }); |
| 293 | if (bodyValue !== undefined) { | 295 | if (value === undefined || value === null) { |
| 294 | const serializedBody = serializeOpenApiRequestBody({ | 296 | if (parameter.required) { |
| 295 | requestBody: { | 297 | throw new Error(`Missing required ${parameter.location} parameter ${parameter.name}`); |
| 296 | contentTypes: bodySymbol.contents.map((content) => content.mediaType), | ||
| 297 | contents: bodySymbol.contents.map((content) => ({ mediaType: content.mediaType })), | ||
| 298 | }, | ||
| 299 | body: applyJsonBodyPlacements({ | ||
| 300 | body: bodyValue, | ||
| 301 | bodyValues: input.auth.bodyValues, | ||
| 302 | label: `${input.executable.method} ${input.executable.pathTemplate}`, | ||
| 303 | }), | ||
| 304 | }); | ||
| 305 | headers["content-type"] = serializedBody.contentType; | ||
| 306 | body = serializedBody.bodyText; | ||
| 307 | } | ||
| 308 | } | 298 | } |
| 299 | continue; | ||
| 300 | } | ||
| 309 | 301 | ||
| 310 | const requestUrl = resolveHttpRequestUrl(url, resolvedPath); | 302 | const serialized = serializeOpenApiParameterValue({ |
| 311 | const urlWithAuth = applyHttpQueryPlacementsToUrl({ | 303 | name: parameter.name, |
| 312 | url: requestUrl, | 304 | location: parameter.location, |
| 313 | queryParams: input.auth.queryParams, | 305 | style: parameter.style, |
| 314 | }); | 306 | explode: parameter.explode, |
| 315 | const urlWithOpenApiQuery = withSerializedQueryEntries(urlWithAuth, queryEntries); | 307 | allowReserved: parameter.allowReserved, |
| 316 | if (cookieParts.length > 0) { | 308 | content: parameter.content?.map((content) => ({ mediaType: content.mediaType })), |
| 317 | headers.cookie = cookieParts.join("; "); | 309 | }, value); |
| 318 | } | 310 | |
| 319 | const headersWithCookies = applyCookiePlacementsToHeaders({ | 311 | if (serialized.kind === "path") { |
| 320 | headers: { | 312 | resolvedPath = resolvedPath.replace( |
| 321 | ...headers, | 313 | new RegExp(`{${parameter.name}}`, "g"), |
| 322 | ...input.auth.headers, | 314 | serialized.value, |
| 323 | }, | 315 | ); |
| 324 | cookies: { | 316 | continue; |
| 325 | ...input.auth.cookies, | 317 | } |
| 318 | |||
| 319 | if (serialized.kind === "query") { | ||
| 320 | queryEntries.push(...serialized.entries); | ||
| 321 | continue; | ||
| 322 | } | ||
| 323 | |||
| 324 | if (serialized.kind === "header") { | ||
| 325 | headers[parameter.name] = serialized.value; | ||
| 326 | continue; | ||
| 327 | } | ||
| 328 | |||
| 329 | if (serialized.kind === "cookie") { | ||
| 330 | cookieParts.push( | ||
| 331 | ...serialized.pairs.map((pair) => `${pair.name}=${encodeURIComponent(pair.value)}`), | ||
| 332 | ); | ||
| 333 | } | ||
| 334 | } | ||
| 335 | |||
| 336 | const bodySymbol = input.executable.requestBodyId | ||
| 337 | ? input.catalog.symbols[input.executable.requestBodyId] | ||
| 338 | : undefined; | ||
| 339 | let body: string | undefined; | ||
| 340 | if (bodySymbol?.kind === "requestBody") { | ||
| 341 | const bodyValue = argsRecord.body ?? argsRecord.input; | ||
| 342 | if (bodyValue !== undefined) { | ||
| 343 | const serializedBody = serializeOpenApiRequestBody({ | ||
| 344 | requestBody: { | ||
| 345 | contentTypes: bodySymbol.contents.map((content) => content.mediaType), | ||
| 346 | contents: bodySymbol.contents.map((content) => ({ mediaType: content.mediaType })), | ||
| 326 | }, | 347 | }, |
| 348 | body: applyJsonBodyPlacements({ | ||
| 349 | body: bodyValue, | ||
| 350 | bodyValues: input.auth.bodyValues, | ||
| 351 | label: `${input.executable.method} ${input.executable.pathTemplate}`, | ||
| 352 | }), | ||
| 327 | }); | 353 | }); |
| 354 | headers["content-type"] = serializedBody.contentType; | ||
| 355 | body = serializedBody.bodyText; | ||
| 356 | } | ||
| 357 | } | ||
| 328 | 358 | ||
| 329 | const response = await fetch(urlWithOpenApiQuery, { | 359 | const requestUrl = resolveHttpRequestUrl(url, resolvedPath); |
| 330 | method: input.executable.method, | 360 | const urlWithAuth = applyHttpQueryPlacementsToUrl({ |
| 331 | headers: headersWithCookies, | 361 | url: requestUrl, |
| 332 | ...(body !== undefined ? { body } : {}), | 362 | queryParams: input.auth.queryParams, |
| 363 | }); | ||
| 364 | const urlWithOpenApiQuery = withSerializedQueryEntries(urlWithAuth, queryEntries); | ||
| 365 | if (cookieParts.length > 0) { | ||
| 366 | headers.cookie = cookieParts.join("; "); | ||
| 367 | } | ||
| 368 | const headersWithCookies = applyCookiePlacementsToHeaders({ | ||
| 369 | headers: { | ||
| 370 | ...headers, | ||
| 371 | ...input.auth.headers, | ||
| 372 | }, | ||
| 373 | cookies: { | ||
| 374 | ...input.auth.cookies, | ||
| 375 | }, | ||
| 376 | }); | ||
| 377 | |||
| 378 | return { | ||
| 379 | resolvedEndpoint: urlWithOpenApiQuery.toString(), | ||
| 380 | requestPlan: { | ||
| 381 | protocol: "http", | ||
| 382 | method: input.executable.method, | ||
| 383 | url: urlWithOpenApiQuery.toString(), | ||
| 384 | headers: headersWithCookies, | ||
| 385 | ...(body !== undefined ? { body } : {}), | ||
| 386 | }, | ||
| 387 | }; | ||
| 388 | }; | ||
| 389 | |||
| 390 | const executeHttpPlan = (requestPlan: HttpInvocationRequestPlan) => | ||
| 391 | Effect.tryPromise({ | ||
| 392 | try: async () => { | ||
| 393 | const response = await fetch(requestPlan.url, { | ||
| 394 | method: requestPlan.method, | ||
| 395 | headers: requestPlan.headers, | ||
| 396 | ...(requestPlan.body !== undefined ? { body: requestPlan.body } : {}), | ||
| 333 | }); | 397 | }); |
| 334 | const responseBody = await decodeFetchBody(response); | 398 | const responseBody = await decodeFetchBody(response); |
| 335 | return executionEnvelope({ | 399 | return executionEnvelope({ |
| @@ -377,101 +441,121 @@ const graphqlArgsPayload = (input: { · 41 unmodified lines | |||
| 377 | return asObject(input.args.args); | 441 | return asObject(input.args.args); |
| 378 | }; | 442 | }; |
| 379 | 443 | ||
| 380 | const executeGraphql = (input: { | 444 | const planGraphqlInvocation = (input: { |
| 381 | source: Source; | 445 | source: Source; |
| 382 | catalog: CatalogV1; | 446 | catalog: CatalogV1; |
| 383 | executable: GraphQLExecutable; | 447 | executable: GraphQLExecutable; |
| 384 | auth: ResolvedSourceAuthMaterial; | 448 | auth: ResolvedSourceAuthMaterial; |
| 385 | args: unknown; | 449 | args: unknown; |
| 386 | }) => | 450 | }): { |
| 451 | resolvedEndpoint: string; | ||
| 452 | requestPlan: GraphqlInvocationRequestPlan; | ||
| 453 | } => { | ||
| 454 | const argsRecord = asObject(input.args); | ||
| 455 | const requestHeaders = applyCookiePlacementsToHeaders({ | ||
| 456 | headers: { | ||
| 457 | ...readSourceHeaders(input.source), | ||
| 458 | ...input.auth.headers, | ||
| 459 | ...asStringRecord(argsRecord.headers), | ||
| 460 | "content-type": "application/json", | ||
| 461 | }, | ||
| 462 | cookies: input.auth.cookies, | ||
| 463 | }); | ||
| 464 | const queryParams = { | ||
| 465 | ...readSourceQueryParams(input.source), | ||
| 466 | ...input.auth.queryParams, | ||
| 467 | }; | ||
| 468 | const endpoint = applyHttpQueryPlacementsToUrl({ | ||
| 469 | url: input.source.endpoint, | ||
| 470 | queryParams, | ||
| 471 | }).toString(); | ||
| 472 | |||
| 473 | if ( | ||
| 474 | input.executable.toolKind === "request" | ||
| 475 | || typeof input.executable.operationDocument !== "string" | ||
| 476 | || input.executable.operationDocument.trim().length === 0 | ||
| 477 | ) { | ||
| 478 | const query = asString(argsRecord.query); | ||
| 479 | if (query === null) { | ||
| 480 | throw new Error("GraphQL request tools require args.query"); | ||
| 481 | } | ||
| 482 | |||
| 483 | return { | ||
| 484 | resolvedEndpoint: endpoint, | ||
| 485 | requestPlan: { | ||
| 486 | protocol: "graphql", | ||
| 487 | endpoint, | ||
| 488 | headers: requestHeaders, | ||
| 489 | query, | ||
| 490 | ...(argsRecord.variables ? { variables: asObject(argsRecord.variables) } : {}), | ||
| 491 | ...(asString(argsRecord.operationName) ? { operationName: asString(argsRecord.operationName)! } : {}), | ||
| 492 | }, | ||
| 493 | }; | ||
| 494 | } | ||
| 495 | |||
| 496 | const operationDocument = input.executable.operationDocument; | ||
| 497 | const operationName = input.executable.operationName; | ||
| 498 | const variables = graphqlArgsPayload({ | ||
| 499 | catalog: input.catalog, | ||
| 500 | executable: input.executable, | ||
| 501 | args: argsRecord, | ||
| 502 | }); | ||
| 503 | |||
| 504 | const query = | ||
| 505 | input.executable.selectionMode === "caller" | ||
| 506 | ? (() => { | ||
| 507 | const selection = graphqlSelectionFromArgs(argsRecord.select); | ||
| 508 | if (selection.length === 0) { | ||
| 509 | throw new Error("GraphQL caller selection tools require args.select"); | ||
| 510 | } | ||
| 511 | return `${input.executable.operationType} ${input.executable.rootField} { ${input.executable.rootField} ${selection ? `{ ${selection} }` : ""} }`; | ||
| 512 | })() | ||
| 513 | : operationDocument; | ||
| 514 | |||
| 515 | return { | ||
| 516 | resolvedEndpoint: endpoint, | ||
| 517 | requestPlan: { | ||
| 518 | protocol: "graphql", | ||
| 519 | endpoint, | ||
| 520 | headers: requestHeaders, | ||
| 521 | query, | ||
| 522 | variables, | ||
| 523 | ...(operationName ? { operationName } : {}), | ||
| 524 | responseRootField: input.executable.rootField, | ||
| 525 | }, | ||
| 526 | }; | ||
| 527 | }; | ||
| 528 | |||
| 529 | const executeGraphqlPlan = (requestPlan: GraphqlInvocationRequestPlan) => | ||
| 387 | Effect.tryPromise({ | 530 | Effect.tryPromise({ |
| 388 | try: async () => { | 531 | try: async () => { |
| 389 | const argsRecord = asObject(input.args); | 532 | const response = await fetch(requestPlan.endpoint, { |
| 390 | const requestHeaders = { | 533 | method: "POST", |
| 391 | ...readSourceHeaders(input.source), | 534 | headers: requestPlan.headers, |
| 392 | ...input.auth.headers, | 535 | body: JSON.stringify({ |
| 393 | ...asStringRecord(argsRecord.headers), | 536 | query: requestPlan.query, |
| 394 | "content-type": "application/json", | 537 | ...(requestPlan.variables ? { variables: requestPlan.variables } : {}), |
| 395 | }; | 538 | ...(requestPlan.operationName ? { operationName: requestPlan.operationName } : {}), |
| 396 | const queryParams = { | 539 | }), |
| 397 | ...readSourceQueryParams(input.source), | 540 | }); |
| 398 | ...input.auth.queryParams, | 541 | const decodedBody = await decodeFetchBody(response); |
| 399 | }; | 542 | |
| 400 | const endpoint = applyHttpQueryPlacementsToUrl({ | 543 | if (requestPlan.responseRootField === undefined) { |
| 401 | url: input.source.endpoint, | 544 | const bodyRecord = asObject(decodedBody); |
| 402 | queryParams, | ||
| 403 | }).toString(); | ||
| 404 | |||
| 405 | if ( | ||
| 406 | input.executable.toolKind === "request" | ||
| 407 | || typeof input.executable.operationDocument !== "string" | ||
| 408 | || input.executable.operationDocument.trim().length === 0 | ||
| 409 | ) { | ||
| 410 | const query = asString(argsRecord.query); | ||
| 411 | if (query === null) { | ||
| 412 | throw new Error(`GraphQL request tools require args.query`); | ||
| 413 | } | ||
| 414 | |||
| 415 | const response = await fetch(endpoint, { | ||
| 416 | method: "POST", | ||
| 417 | headers: applyCookiePlacementsToHeaders({ | ||
| 418 | headers: requestHeaders, | ||
| 419 | cookies: input.auth.cookies, | ||
| 420 | }), | ||
| 421 | body: JSON.stringify({ | ||
| 422 | query, | ||
| 423 | ...(argsRecord.variables ? { variables: asObject(argsRecord.variables) } : {}), | ||
| 424 | ...(asString(argsRecord.operationName) ? { operationName: asString(argsRecord.operationName)! } : {}), | ||
| 425 | }), | ||
| 426 | }); | ||
| 427 | const body = await decodeFetchBody(response); | ||
| 428 | const bodyRecord = asObject(body); | ||
| 429 | const errors = Array.isArray(bodyRecord.errors) ? bodyRecord.errors : []; | 545 | const errors = Array.isArray(bodyRecord.errors) ? bodyRecord.errors : []; |
| 430 | return executionEnvelope({ | 546 | return executionEnvelope({ |
| 431 | data: body, | 547 | data: decodedBody, |
| 432 | error: errors.length > 0 ? errors : (response.status >= 400 ? body : null), | 548 | error: errors.length > 0 ? errors : (response.status >= 400 ? decodedBody : null), |
| 433 | headers: responseHeadersRecord(response), | 549 | headers: responseHeadersRecord(response), |
| 434 | status: response.status, | 550 | status: response.status, |
| 435 | }); | 551 | }); |
| 436 | } | 552 | } |
| 437 | 553 | ||
| 438 | const operationDocument = input.executable.operationDocument; | 554 | const body = asObject(decodedBody); |
| 439 | const operationName = input.executable.operationName; | ||
| 440 | const fieldName = input.executable.rootField; | ||
| 441 | const variables = graphqlArgsPayload({ | ||
| 442 | catalog: input.catalog, | ||
| 443 | executable: input.executable, | ||
| 444 | args: argsRecord, | ||
| 445 | }); | ||
| 446 | |||
| 447 | const query = | ||
| 448 | input.executable.selectionMode === "caller" | ||
| 449 | ? (() => { | ||
| 450 | const selection = graphqlSelectionFromArgs(argsRecord.select); | ||
| 451 | if (selection.length === 0) { | ||
| 452 | throw new Error(`GraphQL caller selection tools require args.select`); | ||
| 453 | } | ||
| 454 | return `${input.executable.operationType} ${input.executable.rootField} { ${input.executable.rootField} ${selection ? `{ ${selection} }` : ""} }`; | ||
| 455 | })() | ||
| 456 | : operationDocument; | ||
| 457 | |||
| 458 | const response = await fetch(endpoint, { | ||
| 459 | method: "POST", | ||
| 460 | headers: applyCookiePlacementsToHeaders({ | ||
| 461 | headers: requestHeaders, | ||
| 462 | cookies: input.auth.cookies, | ||
| 463 | }), | ||
| 464 | body: JSON.stringify({ | ||
| 465 | query, | ||
| 466 | variables, | ||
| 467 | ...(operationName ? { operationName } : {}), | ||
| 468 | }), | ||
| 469 | }); | ||
| 470 | const body = asObject(await decodeFetchBody(response)); | ||
| 471 | const errors = Array.isArray(body.errors) ? body.errors : []; | 555 | const errors = Array.isArray(body.errors) ? body.errors : []; |
| 472 | const data = asObject(body.data); | 556 | const data = asObject(body.data); |
| 473 | return executionEnvelope({ | 557 | return executionEnvelope({ |
| 474 | data: data[fieldName] ?? null, | 558 | data: data[requestPlan.responseRootField] ?? null, |
| 475 | error: errors.length > 0 ? errors : (response.status >= 400 ? body : null), | 559 | error: errors.length > 0 ? errors : (response.status >= 400 ? body : null), |
| 476 | headers: responseHeadersRecord(response), | 560 | headers: responseHeadersRecord(response), |
| 477 | status: response.status, | 561 | status: response.status, |
| @@ -480,74 +564,101 @@ const executeGraphql = (input: { · 2 unmodified lines | |||
| 480 | catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)), | 564 | catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)), |
| 481 | }); | 565 | }); |
| 482 | 566 | ||
| 483 | const executeMcp = (input: { | 567 | const planMcpInvocation = (input: { |
| 484 | source: Source; | 568 | source: Source; |
| 485 | catalog: CatalogV1; | 569 | catalog: CatalogV1; |
| 486 | tool: LoadedSourceCatalogToolIndexEntry; | 570 | tool: LoadedSourceCatalogToolIndexEntry; |
| 487 | executable: McpExecutable; | 571 | executable: McpExecutable; |
| 488 | auth: ResolvedSourceAuthMaterial; | 572 | auth: ResolvedSourceAuthMaterial; |
| 489 | args: unknown; | 573 | args: unknown; |
| 574 | }): { | ||
| 575 | resolvedEndpoint: string; | ||
| 576 | requestPlan: McpInvocationRequestPlan; | ||
| 577 | } => { | ||
| 578 | const inputShape = input.executable.inputShapeId | ||
| 579 | ? input.catalog.symbols[input.executable.inputShapeId] | ||
| 580 | : undefined; | ||
| 581 | const payload = | ||
| 582 | inputShape?.kind === "shape" && inputShape.node.type !== "object" | ||
| 583 | ? asObject(input.args).input | ||
| 584 | : input.args; | ||
| 585 | |||
| 586 | return { | ||
| 587 | resolvedEndpoint: input.source.endpoint, | ||
| 588 | requestPlan: { | ||
| 589 | protocol: "mcp", | ||
| 590 | endpoint: input.source.endpoint, | ||
| 591 | transport: readSourceTransport(input.source), | ||
| 592 | queryParams: { | ||
| 593 | ...readSourceQueryParams(input.source), | ||
| 594 | ...input.auth.queryParams, | ||
| 595 | }, | ||
| 596 | headers: applyCookiePlacementsToHeaders({ | ||
| 597 | headers: { | ||
| 598 | ...readSourceHeaders(input.source), | ||
| 599 | ...input.auth.headers, | ||
| 600 | }, | ||
| 601 | cookies: input.auth.cookies, | ||
| 602 | }), | ||
| 603 | toolName: input.executable.toolName, | ||
| 604 | payload, | ||
| 605 | inputSchema: input.tool.descriptor.inputSchema, | ||
| 606 | outputSchema: input.tool.descriptor.outputSchema, | ||
| 607 | ...(input.tool.capability.surface.title ? { toolTitle: input.tool.capability.surface.title } : {}), | ||
| 608 | ...(input.tool.capability.surface.summary ? { toolSummary: input.tool.capability.surface.summary } : {}), | ||
| 609 | }, | ||
| 610 | }; | ||
| 611 | }; | ||
| 612 | |||
| 613 | const executeMcpPlan = (input: { | ||
| 614 | source: Source; | ||
| 615 | tool: LoadedSourceCatalogToolIndexEntry; | ||
| 616 | requestPlan: McpInvocationRequestPlan; | ||
| 490 | onElicitation?: OnElicitation; | 617 | onElicitation?: OnElicitation; |
| 491 | context?: Record<string, unknown>; | 618 | context?: Record<string, unknown>; |
| 492 | }) => | 619 | }) => |
| 493 | Effect.tryPromise({ | 620 | Effect.tryPromise({ |
| 494 | try: async () => { | 621 | try: async () => { |
| 495 | const inputShape = input.executable.inputShapeId | ||
| 496 | ? input.catalog.symbols[input.executable.inputShapeId] | ||
| 497 | : undefined; | ||
| 498 | const payload = | ||
| 499 | inputShape?.kind === "shape" && inputShape.node.type !== "object" | ||
| 500 | ? asObject(input.args).input | ||
| 501 | : input.args; | ||
| 502 | const connector = createSdkMcpConnector({ | 622 | const connector = createSdkMcpConnector({ |
| 503 | endpoint: input.source.endpoint, | 623 | endpoint: input.requestPlan.endpoint, |
| 504 | transport: readSourceTransport(input.source), | 624 | transport: input.requestPlan.transport, |
| 505 | queryParams: { | 625 | queryParams: input.requestPlan.queryParams, |
| 506 | ...readSourceQueryParams(input.source), | 626 | headers: input.requestPlan.headers, |
| 507 | ...input.auth.queryParams, | ||
| 508 | }, | ||
| 509 | headers: applyCookiePlacementsToHeaders({ | ||
| 510 | headers: { | ||
| 511 | ...readSourceHeaders(input.source), | ||
| 512 | ...input.auth.headers, | ||
| 513 | }, | ||
| 514 | cookies: input.auth.cookies, | ||
| 515 | }), | ||
| 516 | }); | 627 | }); |
| 517 | const tools = createMcpToolsFromManifest({ | 628 | const tools = createMcpToolsFromManifest({ |
| 518 | manifest: { | 629 | manifest: { |
| 519 | version: 2, | 630 | version: 2, |
| 520 | tools: [{ | 631 | tools: [{ |
| 521 | toolId: input.executable.toolName, | 632 | toolId: input.requestPlan.toolName, |
| 522 | toolName: input.executable.toolName, | 633 | toolName: input.requestPlan.toolName, |
| 523 | displayTitle: | 634 | displayTitle: |
| 524 | input.tool.capability.surface.title | 635 | input.requestPlan.toolTitle |
| 525 | ?? input.executable.toolName, | 636 | ?? input.requestPlan.toolName, |
| 526 | title: input.tool.capability.surface.title ?? null, | 637 | title: input.requestPlan.toolTitle ?? null, |
| 527 | description: | 638 | description: |
| 528 | input.tool.capability.surface.summary | 639 | input.requestPlan.toolSummary |
| 529 | ?? input.tool.capability.surface.title | 640 | ?? input.requestPlan.toolTitle |
| 530 | ?? `MCP tool: ${input.executable.toolName}`, | 641 | ?? `MCP tool: ${input.requestPlan.toolName}`, |
| 531 | annotations: null, | 642 | annotations: null, |
| 532 | execution: null, | 643 | execution: null, |
| 533 | icons: null, | 644 | icons: null, |
| 534 | meta: null, | 645 | meta: null, |
| 535 | rawTool: null, | 646 | rawTool: null, |
| 536 | inputSchema: input.tool.descriptor.inputSchema, | 647 | inputSchema: input.requestPlan.inputSchema, |
| 537 | outputSchema: input.tool.descriptor.outputSchema, | 648 | outputSchema: input.requestPlan.outputSchema, |
| 538 | }], | 649 | }], |
| 539 | }, | 650 | }, |
| 540 | connect: connector, | 651 | connect: connector, |
| 541 | sourceKey: input.source.id, | 652 | sourceKey: input.source.id, |
| 542 | }); | 653 | }); |
| 543 | const entry = tools[input.executable.toolName] as ToolInput | undefined; | 654 | const entry = tools[input.requestPlan.toolName] as ToolInput | undefined; |
| 544 | const definition = | 655 | const definition = |
| 545 | entry && typeof entry === "object" && entry !== null && "tool" in entry | 656 | entry && typeof entry === "object" && entry !== null && "tool" in entry |
| 546 | ? entry.tool | 657 | ? entry.tool |
| 547 | : entry; | 658 | : entry; |
| 548 | 659 | ||
| 549 | if (!definition) { | 660 | if (!definition) { |
| 550 | throw new Error(`Missing MCP tool definition for ${input.executable.toolName}`); | 661 | throw new Error(`Missing MCP tool definition for ${input.requestPlan.toolName}`); |
| 551 | } | 662 | } |
| 552 | 663 | ||
| 553 | const executionContext: ToolExecutionContext | undefined = | 664 | const executionContext: ToolExecutionContext | undefined = |
| @@ -568,7 +679,7 @@ const executeMcp = (input: { · 14 unmodified lines | |||
| 568 | } | 679 | } |
| 569 | : undefined; | 680 | : undefined; |
| 570 | 681 | ||
| 571 | const result = await definition.execute(asObject(payload), executionContext); | 682 | const result = await definition.execute(asObject(input.requestPlan.payload), executionContext); |
| 572 | const resultRecord = asObject(result); | 683 | const resultRecord = asObject(result); |
| 573 | const isError = resultRecord.isError === true; | 684 | const isError = resultRecord.isError === true; |
| 574 | return executionEnvelope({ | 685 | return executionEnvelope({ |
| @@ -581,6 +692,84 @@ const executeMcp = (input: { · 6 unmodified lines | |||
| 581 | catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)), | 692 | catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)), |
| 582 | }); | 693 | }); |
| 583 | 694 | ||
| 695 | const requestPlanFromInvocationPlan = ( | ||
| 696 | plan: InvocationPlan, | ||
| 697 | ): IrInvocationRequestPlan => { | ||
| 698 | const requestPlan = asObject(plan.requestPlan); | ||
| 699 | const protocol = asString(requestPlan.protocol); | ||
| 700 | if (protocol !== "http" && protocol !== "graphql" && protocol !== "mcp") { | ||
| 701 | throw new Error(`Unsupported invocation request plan for executable ${plan.executableId}`); | ||
| 702 | } | ||
| 703 | return requestPlan as IrInvocationRequestPlan; | ||
| 704 | }; | ||
| 705 | |||
| 706 | export const planIrToolInvocation = (input: { | ||
| 707 | tool: LoadedSourceCatalogToolIndexEntry; | ||
| 708 | auth: ResolvedSourceAuthMaterial; | ||
| 709 | args: unknown; | ||
| 710 | }): InvocationPlan => { | ||
| 711 | const planned = | ||
| 712 | input.tool.executable.protocol === "http" | ||
| 713 | ? planHttpInvocation({ | ||
| 714 | source: input.tool.source, | ||
| 715 | catalog: input.tool.projectedCatalog, | ||
| 716 | executable: input.tool.executable, | ||
| 717 | auth: input.auth, | ||
| 718 | args: input.args, | ||
| 719 | }) | ||
| 720 | : input.tool.executable.protocol === "graphql" | ||
| 721 | ? planGraphqlInvocation({ | ||
| 722 | source: input.tool.source, | ||
| 723 | catalog: input.tool.projectedCatalog, | ||
| 724 | executable: input.tool.executable, | ||
| 725 | auth: input.auth, | ||
| 726 | args: input.args, | ||
| 727 | }) | ||
| 728 | : planMcpInvocation({ | ||
| 729 | source: input.tool.source, | ||
| 730 | catalog: input.tool.projectedCatalog, | ||
| 731 | tool: input.tool, | ||
| 732 | executable: input.tool.executable, | ||
| 733 | auth: input.auth, | ||
| 734 | args: input.args, | ||
| 735 | }); | ||
| 736 | |||
| 737 | return buildInvocationPlan({ | ||
| 738 | catalog: input.tool.projectedCatalog, | ||
| 739 | capabilityId: input.tool.capabilityId, | ||
| 740 | executableId: input.tool.executableId, | ||
| 741 | connectionBindingId: input.tool.source.id, | ||
| 742 | resolvedEndpoint: planned.resolvedEndpoint, | ||
| 743 | resolvedAuth: resolvedAuthSummary(input.auth), | ||
| 744 | requestPlan: planned.requestPlan, | ||
| 745 | }); | ||
| 746 | }; | ||
| 747 | |||
| 748 | export const executeIrInvocationPlan = (input: { | ||
| 749 | plan: InvocationPlan; | ||
| 750 | source: Source; | ||
| 751 | tool: LoadedSourceCatalogToolIndexEntry; | ||
| 752 | onElicitation?: OnElicitation; | ||
| 753 | context?: Record<string, unknown>; | ||
| 754 | }) => { | ||
| 755 | const requestPlan = requestPlanFromInvocationPlan(input.plan); | ||
| 756 | |||
| 757 | switch (requestPlan.protocol) { | ||
| 758 | case "http": | ||
| 759 | return executeHttpPlan(requestPlan); | ||
| 760 | case "graphql": | ||
| 761 | return executeGraphqlPlan(requestPlan); | ||
| 762 | case "mcp": | ||
| 763 | return executeMcpPlan({ | ||
| 764 | source: input.source, | ||
| 765 | tool: input.tool, | ||
| 766 | requestPlan, | ||
| 767 | onElicitation: input.onElicitation, | ||
| 768 | context: input.context, | ||
| 769 | }); | ||
| 770 | } | ||
| 771 | }; | ||
| 772 | |||
| 584 | export const invocationDescriptorFromTool = (input: { | 773 | export const invocationDescriptorFromTool = (input: { |
| 585 | tool: LoadedSourceCatalogToolIndexEntry; | 774 | tool: LoadedSourceCatalogToolIndexEntry; |
| 586 | }): { | 775 | }): { |
| @@ -621,34 +810,15 @@ export const invokeIrTool = (input: { · 34 unmodified lines | |||
| 621 | args: unknown; | 810 | args: unknown; |
| 622 | onElicitation?: OnElicitation; | 811 | onElicitation?: OnElicitation; |
| 623 | context?: Record<string, unknown>; | 812 | context?: Record<string, unknown>; |
| 624 | }) => { | 813 | }) => |
| 625 | switch (input.tool.executable.protocol) { | 814 | executeIrInvocationPlan({ |
| 626 | case "http": | 815 | plan: planIrToolInvocation({ |
| 627 | return executeHttp({ | 816 | tool: input.tool, |
| 628 | source: input.tool.source, | 817 | auth: input.auth, |
| 629 | catalog: input.tool.projectedCatalog, | 818 | args: input.args, |
| 630 | executable: input.tool.executable, | 819 | }), |
| 631 | auth: input.auth, | 820 | source: input.tool.source, |
| 632 | args: input.args, | 821 | tool: input.tool, |
| 633 | }); | 822 | onElicitation: input.onElicitation, |
| 634 | case "graphql": | 823 | context: input.context, |
| 635 | return executeGraphql({ | 824 | }); |
| 636 | source: input.tool.source, | ||
| 637 | catalog: input.tool.projectedCatalog, | ||
| 638 | executable: input.tool.executable, | ||
| 639 | auth: input.auth, | ||
| 640 | args: input.args, | ||
| 641 | }); | ||
| 642 | case "mcp": | ||
| 643 | return executeMcp({ | ||
| 644 | source: input.tool.source, | ||
| 645 | catalog: input.tool.projectedCatalog, | ||
| 646 | tool: input.tool, | ||
| 647 | executable: input.tool.executable, | ||
| 648 | auth: input.auth, | ||
| 649 | args: input.args, | ||
| 650 | onElicitation: input.onElicitation, | ||
| 651 | context: input.context, | ||
| 652 | }); | ||
| 653 | } | ||
| 654 | }; | ||