History

refactor: separate ir-execution into plan and execute phases

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.

Rhys Sullivan committed 8 months ago

Commit bdbcd4e1 parent 8019c0b1 file · +429 −259Browse files

Unified · Split

packages/control-plane/src/runtime/ir-execution.ts+429−259
@@ -21,6 +21,7 @@ import type { · 20 unmodified lines
2121 ToolInput,
2222 ToolPath,
2323 } from "@executor/codemode-core";
24+import { buildInvocationPlan, type InvocationPlan } from "../ir/catalog";
2425 import type {
2526 CatalogV1,
2627 Capability,
@@ -72,6 +73,43 @@ type ExecutionEnvelope = { · 45 unmodified lines
7273 status: number | null;
7374 };
7475
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+
75113 const decodeFetchBody = async (response: Response): Promise<unknown> => {
76114 const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
77115 if (contentType.includes("application/json")) {
@@ -93,6 +131,16 @@ const responseHeadersRecord = (response: Response): Record<string, string> => { · 15 unmodified lines
93131
94132 const executionEnvelope = (input: ExecutionEnvelope): ExecutionEnvelope => input;
95133
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+
96144 const parameterById = (catalog: CatalogV1, parameterId: string): ParameterSymbol | undefined => {
97145 const symbol = catalog.symbols[parameterId];
98146 return symbol?.kind === "parameter" ? symbol : undefined;
@@ -203,133 +251,149 @@ const resolveHttpRequestUrl = (baseUrl: URL, resolvedPath: string): URL => { · 104 unmodified lines
203251 }
204252 };
205253
206−const executeHttp = (input: {
254+const planHttpInvocation = (input: {
207255 source: Source;
208256 catalog: CatalogV1;
209257 executable: HttpExecutable;
210258 auth: ResolvedSourceAuthMaterial;
211259 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+ }
286288
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}`);
308298 }
299+ continue;
300+ }
309301
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 })),
326347 },
348+ body: applyJsonBodyPlacements({
349+ body: bodyValue,
350+ bodyValues: input.auth.bodyValues,
351+ label: `${input.executable.method} ${input.executable.pathTemplate}`,
352+ }),
327353 });
354+ headers["content-type"] = serializedBody.contentType;
355+ body = serializedBody.bodyText;
356+ }
357+ }
328358
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 } : {}),
333397 });
334398 const responseBody = await decodeFetchBody(response);
335399 return executionEnvelope({
@@ -377,101 +441,121 @@ const graphqlArgsPayload = (input: { · 41 unmodified lines
377441 return asObject(input.args.args);
378442 };
379443
380−const executeGraphql = (input: {
444+const planGraphqlInvocation = (input: {
381445 source: Source;
382446 catalog: CatalogV1;
383447 executable: GraphQLExecutable;
384448 auth: ResolvedSourceAuthMaterial;
385449 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) =>
387530 Effect.tryPromise({
388531 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);
429545 const errors = Array.isArray(bodyRecord.errors) ? bodyRecord.errors : [];
430546 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),
433549 headers: responseHeadersRecord(response),
434550 status: response.status,
435551 });
436552 }
437553
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);
471555 const errors = Array.isArray(body.errors) ? body.errors : [];
472556 const data = asObject(body.data);
473557 return executionEnvelope({
474− data: data[fieldName] ?? null,
558+ data: data[requestPlan.responseRootField] ?? null,
475559 error: errors.length > 0 ? errors : (response.status >= 400 ? body : null),
476560 headers: responseHeadersRecord(response),
477561 status: response.status,
@@ -480,74 +564,101 @@ const executeGraphql = (input: { · 2 unmodified lines
480564 catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
481565 });
482566
483−const executeMcp = (input: {
567+const planMcpInvocation = (input: {
484568 source: Source;
485569 catalog: CatalogV1;
486570 tool: LoadedSourceCatalogToolIndexEntry;
487571 executable: McpExecutable;
488572 auth: ResolvedSourceAuthMaterial;
489573 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;
490617 onElicitation?: OnElicitation;
491618 context?: Record<string, unknown>;
492619 }) =>
493620 Effect.tryPromise({
494621 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;
502622 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,
516627 });
517628 const tools = createMcpToolsFromManifest({
518629 manifest: {
519630 version: 2,
520631 tools: [{
521− toolId: input.executable.toolName,
522− toolName: input.executable.toolName,
632+ toolId: input.requestPlan.toolName,
633+ toolName: input.requestPlan.toolName,
523634 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,
527638 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}`,
531642 annotations: null,
532643 execution: null,
533644 icons: null,
534645 meta: null,
535646 rawTool: null,
536− inputSchema: input.tool.descriptor.inputSchema,
537− outputSchema: input.tool.descriptor.outputSchema,
647+ inputSchema: input.requestPlan.inputSchema,
648+ outputSchema: input.requestPlan.outputSchema,
538649 }],
539650 },
540651 connect: connector,
541652 sourceKey: input.source.id,
542653 });
543− const entry = tools[input.executable.toolName] as ToolInput | undefined;
654+ const entry = tools[input.requestPlan.toolName] as ToolInput | undefined;
544655 const definition =
545656 entry && typeof entry === "object" && entry !== null && "tool" in entry
546657 ? entry.tool
547658 : entry;
548659
549660 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}`);
551662 }
552663
553664 const executionContext: ToolExecutionContext | undefined =
@@ -568,7 +679,7 @@ const executeMcp = (input: { · 14 unmodified lines
568679 }
569680 : undefined;
570681
571− const result = await definition.execute(asObject(payload), executionContext);
682+ const result = await definition.execute(asObject(input.requestPlan.payload), executionContext);
572683 const resultRecord = asObject(result);
573684 const isError = resultRecord.isError === true;
574685 return executionEnvelope({
@@ -581,6 +692,84 @@ const executeMcp = (input: { · 6 unmodified lines
581692 catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
582693 });
583694
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+
584773 export const invocationDescriptorFromTool = (input: {
585774 tool: LoadedSourceCatalogToolIndexEntry;
586775 }): {
@@ -621,34 +810,15 @@ export const invokeIrTool = (input: { · 34 unmodified lines
621810 args: unknown;
622811 onElicitation?: OnElicitation;
623812 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+ });