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 | 21 | ToolInput, |
| 22 | 22 | ToolPath, |
| 23 | 23 | } from "@executor/codemode-core"; |
| 24 | +import { buildInvocationPlan, type InvocationPlan } from "../ir/catalog"; | |
| 24 | 25 | import type { |
| 25 | 26 | CatalogV1, |
| 26 | 27 | Capability, |
| @@ -72,6 +73,43 @@ type ExecutionEnvelope = { · 45 unmodified lines | ||
| 72 | 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 | 113 | const decodeFetchBody = async (response: Response): Promise<unknown> => { |
| 76 | 114 | const contentType = response.headers.get("content-type")?.toLowerCase() ?? ""; |
| 77 | 115 | if (contentType.includes("application/json")) { |
| @@ -93,6 +131,16 @@ const responseHeadersRecord = (response: Response): Record<string, string> => { · 15 unmodified lines | ||
| 93 | 131 | |
| 94 | 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 | 144 | const parameterById = (catalog: CatalogV1, parameterId: string): ParameterSymbol | undefined => { |
| 97 | 145 | const symbol = catalog.symbols[parameterId]; |
| 98 | 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 | 255 | source: Source; |
| 208 | 256 | catalog: CatalogV1; |
| 209 | 257 | executable: HttpExecutable; |
| 210 | 258 | auth: ResolvedSourceAuthMaterial; |
| 211 | 259 | args: unknown; |
| 212 | −}) => | |
| 213 | − Effect.tryPromise({ | |
| 214 | − try: async () => { | |
| 215 | − const argsRecord = asObject(input.args); | |
| 216 | − let resolvedPath = input.executable.pathTemplate; | |
| 217 | − const url = applyHttpQueryPlacementsToUrl({ | |
| 218 | − url: resolveHttpBaseUrl(input.source, input.catalog, input.executable), | |
| 219 | − queryParams: readSourceQueryParams(input.source), | |
| 220 | − }); | |
| 221 | − const headers: Record<string, string> = { | |
| 222 | − ...readSourceHeaders(input.source), | |
| 223 | − }; | |
| 224 | − const queryEntries: Array<{ name: string; value: string; allowReserved?: boolean }> = []; | |
| 225 | − const cookieParts: string[] = []; | |
| 226 | − | |
| 227 | − const allParameterIds = [ | |
| 228 | − ...(input.executable.pathParameterIds ?? []), | |
| 229 | − ...(input.executable.queryParameterIds ?? []), | |
| 230 | − ...(input.executable.headerParameterIds ?? []), | |
| 231 | − ...(input.executable.cookieParameterIds ?? []), | |
| 232 | − ]; | |
| 233 | − | |
| 234 | − for (const parameterId of allParameterIds) { | |
| 235 | − const parameter = parameterById(input.catalog, parameterId); | |
| 236 | − if (!parameter) { | |
| 237 | − continue; | |
| 238 | − } | |
| 239 | − | |
| 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 | − } | |
| 260 | +}): { | |
| 261 | + resolvedEndpoint: string; | |
| 262 | + requestPlan: HttpInvocationRequestPlan; | |
| 263 | +} => { | |
| 264 | + const argsRecord = asObject(input.args); | |
| 265 | + let resolvedPath = input.executable.pathTemplate; | |
| 266 | + const url = applyHttpQueryPlacementsToUrl({ | |
| 267 | + url: resolveHttpBaseUrl(input.source, input.catalog, input.executable), | |
| 268 | + queryParams: readSourceQueryParams(input.source), | |
| 269 | + }); | |
| 270 | + const headers: Record<string, string> = { | |
| 271 | + ...readSourceHeaders(input.source), | |
| 272 | + }; | |
| 273 | + const queryEntries: Array<{ name: string; value: string; allowReserved?: boolean }> = []; | |
| 274 | + const cookieParts: string[] = []; | |
| 275 | + | |
| 276 | + const allParameterIds = [ | |
| 277 | + ...(input.executable.pathParameterIds ?? []), | |
| 278 | + ...(input.executable.queryParameterIds ?? []), | |
| 279 | + ...(input.executable.headerParameterIds ?? []), | |
| 280 | + ...(input.executable.cookieParameterIds ?? []), | |
| 281 | + ]; | |
| 282 | + | |
| 283 | + for (const parameterId of allParameterIds) { | |
| 284 | + const parameter = parameterById(input.catalog, parameterId); | |
| 285 | + if (!parameter) { | |
| 286 | + continue; | |
| 287 | + } | |
| 286 | 288 | |
| 287 | − const bodySymbol = input.executable.requestBodyId | |
| 288 | − ? input.catalog.symbols[input.executable.requestBodyId] | |
| 289 | − : undefined; | |
| 290 | − let body: string | undefined; | |
| 291 | − if (bodySymbol?.kind === "requestBody") { | |
| 292 | − const bodyValue = argsRecord.body ?? argsRecord.input; | |
| 293 | − if (bodyValue !== undefined) { | |
| 294 | − const serializedBody = serializeOpenApiRequestBody({ | |
| 295 | − requestBody: { | |
| 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 | − } | |
| 289 | + const value = readHttpParameterValue({ | |
| 290 | + catalog: input.catalog, | |
| 291 | + executable: input.executable, | |
| 292 | + parameter, | |
| 293 | + args: argsRecord, | |
| 294 | + }); | |
| 295 | + if (value === undefined || value === null) { | |
| 296 | + if (parameter.required) { | |
| 297 | + throw new Error(`Missing required ${parameter.location} parameter ${parameter.name}`); | |
| 308 | 298 | } |
| 299 | + continue; | |
| 300 | + } | |
| 309 | 301 | |
| 310 | − const requestUrl = resolveHttpRequestUrl(url, resolvedPath); | |
| 311 | − const urlWithAuth = applyHttpQueryPlacementsToUrl({ | |
| 312 | − url: requestUrl, | |
| 313 | − queryParams: input.auth.queryParams, | |
| 314 | − }); | |
| 315 | − const urlWithOpenApiQuery = withSerializedQueryEntries(urlWithAuth, queryEntries); | |
| 316 | − if (cookieParts.length > 0) { | |
| 317 | − headers.cookie = cookieParts.join("; "); | |
| 318 | − } | |
| 319 | − const headersWithCookies = applyCookiePlacementsToHeaders({ | |
| 320 | − headers: { | |
| 321 | − ...headers, | |
| 322 | − ...input.auth.headers, | |
| 323 | − }, | |
| 324 | − cookies: { | |
| 325 | − ...input.auth.cookies, | |
| 302 | + const serialized = serializeOpenApiParameterValue({ | |
| 303 | + name: parameter.name, | |
| 304 | + location: parameter.location, | |
| 305 | + style: parameter.style, | |
| 306 | + explode: parameter.explode, | |
| 307 | + allowReserved: parameter.allowReserved, | |
| 308 | + content: parameter.content?.map((content) => ({ mediaType: content.mediaType })), | |
| 309 | + }, value); | |
| 310 | + | |
| 311 | + if (serialized.kind === "path") { | |
| 312 | + resolvedPath = resolvedPath.replace( | |
| 313 | + new RegExp(`{${parameter.name}}`, "g"), | |
| 314 | + serialized.value, | |
| 315 | + ); | |
| 316 | + continue; | |
| 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, { | |
| 330 | − method: input.executable.method, | |
| 331 | − headers: headersWithCookies, | |
| 332 | − ...(body !== undefined ? { body } : {}), | |
| 359 | + const requestUrl = resolveHttpRequestUrl(url, resolvedPath); | |
| 360 | + const urlWithAuth = applyHttpQueryPlacementsToUrl({ | |
| 361 | + url: requestUrl, | |
| 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 | 398 | const responseBody = await decodeFetchBody(response); |
| 335 | 399 | return executionEnvelope({ |
| @@ -377,101 +441,121 @@ const graphqlArgsPayload = (input: { · 41 unmodified lines | ||
| 377 | 441 | return asObject(input.args.args); |
| 378 | 442 | }; |
| 379 | 443 | |
| 380 | −const executeGraphql = (input: { | |
| 444 | +const planGraphqlInvocation = (input: { | |
| 381 | 445 | source: Source; |
| 382 | 446 | catalog: CatalogV1; |
| 383 | 447 | executable: GraphQLExecutable; |
| 384 | 448 | auth: ResolvedSourceAuthMaterial; |
| 385 | 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 | 530 | Effect.tryPromise({ |
| 388 | 531 | try: async () => { |
| 389 | − const argsRecord = asObject(input.args); | |
| 390 | − const requestHeaders = { | |
| 391 | − ...readSourceHeaders(input.source), | |
| 392 | − ...input.auth.headers, | |
| 393 | − ...asStringRecord(argsRecord.headers), | |
| 394 | − "content-type": "application/json", | |
| 395 | − }; | |
| 396 | − const queryParams = { | |
| 397 | − ...readSourceQueryParams(input.source), | |
| 398 | − ...input.auth.queryParams, | |
| 399 | − }; | |
| 400 | − const endpoint = applyHttpQueryPlacementsToUrl({ | |
| 401 | − url: input.source.endpoint, | |
| 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); | |
| 532 | + const response = await fetch(requestPlan.endpoint, { | |
| 533 | + method: "POST", | |
| 534 | + headers: requestPlan.headers, | |
| 535 | + body: JSON.stringify({ | |
| 536 | + query: requestPlan.query, | |
| 537 | + ...(requestPlan.variables ? { variables: requestPlan.variables } : {}), | |
| 538 | + ...(requestPlan.operationName ? { operationName: requestPlan.operationName } : {}), | |
| 539 | + }), | |
| 540 | + }); | |
| 541 | + const decodedBody = await decodeFetchBody(response); | |
| 542 | + | |
| 543 | + if (requestPlan.responseRootField === undefined) { | |
| 544 | + const bodyRecord = asObject(decodedBody); | |
| 429 | 545 | const errors = Array.isArray(bodyRecord.errors) ? bodyRecord.errors : []; |
| 430 | 546 | return executionEnvelope({ |
| 431 | − data: body, | |
| 432 | − error: errors.length > 0 ? errors : (response.status >= 400 ? body : null), | |
| 547 | + data: decodedBody, | |
| 548 | + error: errors.length > 0 ? errors : (response.status >= 400 ? decodedBody : null), | |
| 433 | 549 | headers: responseHeadersRecord(response), |
| 434 | 550 | status: response.status, |
| 435 | 551 | }); |
| 436 | 552 | } |
| 437 | 553 | |
| 438 | − const operationDocument = input.executable.operationDocument; | |
| 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)); | |
| 554 | + const body = asObject(decodedBody); | |
| 471 | 555 | const errors = Array.isArray(body.errors) ? body.errors : []; |
| 472 | 556 | const data = asObject(body.data); |
| 473 | 557 | return executionEnvelope({ |
| 474 | − data: data[fieldName] ?? null, | |
| 558 | + data: data[requestPlan.responseRootField] ?? null, | |
| 475 | 559 | error: errors.length > 0 ? errors : (response.status >= 400 ? body : null), |
| 476 | 560 | headers: responseHeadersRecord(response), |
| 477 | 561 | status: response.status, |
| @@ -480,74 +564,101 @@ const executeGraphql = (input: { · 2 unmodified lines | ||
| 480 | 564 | catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)), |
| 481 | 565 | }); |
| 482 | 566 | |
| 483 | −const executeMcp = (input: { | |
| 567 | +const planMcpInvocation = (input: { | |
| 484 | 568 | source: Source; |
| 485 | 569 | catalog: CatalogV1; |
| 486 | 570 | tool: LoadedSourceCatalogToolIndexEntry; |
| 487 | 571 | executable: McpExecutable; |
| 488 | 572 | auth: ResolvedSourceAuthMaterial; |
| 489 | 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 | 617 | onElicitation?: OnElicitation; |
| 491 | 618 | context?: Record<string, unknown>; |
| 492 | 619 | }) => |
| 493 | 620 | Effect.tryPromise({ |
| 494 | 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 | 622 | const connector = createSdkMcpConnector({ |
| 503 | − endpoint: input.source.endpoint, | |
| 504 | − transport: readSourceTransport(input.source), | |
| 505 | − queryParams: { | |
| 506 | − ...readSourceQueryParams(input.source), | |
| 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 | − }), | |
| 623 | + endpoint: input.requestPlan.endpoint, | |
| 624 | + transport: input.requestPlan.transport, | |
| 625 | + queryParams: input.requestPlan.queryParams, | |
| 626 | + headers: input.requestPlan.headers, | |
| 516 | 627 | }); |
| 517 | 628 | const tools = createMcpToolsFromManifest({ |
| 518 | 629 | manifest: { |
| 519 | 630 | version: 2, |
| 520 | 631 | tools: [{ |
| 521 | − toolId: input.executable.toolName, | |
| 522 | − toolName: input.executable.toolName, | |
| 632 | + toolId: input.requestPlan.toolName, | |
| 633 | + toolName: input.requestPlan.toolName, | |
| 523 | 634 | displayTitle: |
| 524 | − input.tool.capability.surface.title | |
| 525 | − ?? input.executable.toolName, | |
| 526 | − title: input.tool.capability.surface.title ?? null, | |
| 635 | + input.requestPlan.toolTitle | |
| 636 | + ?? input.requestPlan.toolName, | |
| 637 | + title: input.requestPlan.toolTitle ?? null, | |
| 527 | 638 | description: |
| 528 | − input.tool.capability.surface.summary | |
| 529 | − ?? input.tool.capability.surface.title | |
| 530 | − ?? `MCP tool: ${input.executable.toolName}`, | |
| 639 | + input.requestPlan.toolSummary | |
| 640 | + ?? input.requestPlan.toolTitle | |
| 641 | + ?? `MCP tool: ${input.requestPlan.toolName}`, | |
| 531 | 642 | annotations: null, |
| 532 | 643 | execution: null, |
| 533 | 644 | icons: null, |
| 534 | 645 | meta: null, |
| 535 | 646 | rawTool: null, |
| 536 | − inputSchema: input.tool.descriptor.inputSchema, | |
| 537 | − outputSchema: input.tool.descriptor.outputSchema, | |
| 647 | + inputSchema: input.requestPlan.inputSchema, | |
| 648 | + outputSchema: input.requestPlan.outputSchema, | |
| 538 | 649 | }], |
| 539 | 650 | }, |
| 540 | 651 | connect: connector, |
| 541 | 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 | 655 | const definition = |
| 545 | 656 | entry && typeof entry === "object" && entry !== null && "tool" in entry |
| 546 | 657 | ? entry.tool |
| 547 | 658 | : entry; |
| 548 | 659 | |
| 549 | 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 | 664 | const executionContext: ToolExecutionContext | undefined = |
| @@ -568,7 +679,7 @@ const executeMcp = (input: { · 14 unmodified lines | ||
| 568 | 679 | } |
| 569 | 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 | 683 | const resultRecord = asObject(result); |
| 573 | 684 | const isError = resultRecord.isError === true; |
| 574 | 685 | return executionEnvelope({ |
| @@ -581,6 +692,84 @@ const executeMcp = (input: { · 6 unmodified lines | ||
| 581 | 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 | 773 | export const invocationDescriptorFromTool = (input: { |
| 585 | 774 | tool: LoadedSourceCatalogToolIndexEntry; |
| 586 | 775 | }): { |
| @@ -621,34 +810,15 @@ export const invokeIrTool = (input: { · 34 unmodified lines | ||
| 621 | 810 | args: unknown; |
| 622 | 811 | onElicitation?: OnElicitation; |
| 623 | 812 | context?: Record<string, unknown>; |
| 624 | −}) => { | |
| 625 | − switch (input.tool.executable.protocol) { | |
| 626 | − case "http": | |
| 627 | − return executeHttp({ | |
| 628 | − source: input.tool.source, | |
| 629 | − catalog: input.tool.projectedCatalog, | |
| 630 | − executable: input.tool.executable, | |
| 631 | − auth: input.auth, | |
| 632 | − args: input.args, | |
| 633 | − }); | |
| 634 | − case "graphql": | |
| 635 | − return executeGraphql({ | |
| 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 | −}; | |
| 813 | +}) => | |
| 814 | + executeIrInvocationPlan({ | |
| 815 | + plan: planIrToolInvocation({ | |
| 816 | + tool: input.tool, | |
| 817 | + auth: input.auth, | |
| 818 | + args: input.args, | |
| 819 | + }), | |
| 820 | + source: input.tool.source, | |
| 821 | + tool: input.tool, | |
| 822 | + onElicitation: input.onElicitation, | |
| 823 | + context: input.context, | |
| 824 | + }); | |