@fitchmultz/pi-mcp-adapter 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/OAUTH.md +3 -3
  3. package/README.md +153 -85
  4. package/app-bridge.bundle.js +75 -33
  5. package/commands.ts +9 -6
  6. package/direct-tools.ts +25 -358
  7. package/dist/app-bridge.bundle.js +75 -33
  8. package/dist/commands.js +9 -6
  9. package/dist/direct-tools.d.ts +2 -4
  10. package/dist/direct-tools.js +21 -319
  11. package/dist/gateway-arguments.d.ts +33 -0
  12. package/dist/gateway-arguments.js +63 -0
  13. package/dist/index.js +181 -276
  14. package/dist/init.js +6 -20
  15. package/dist/mcp-code.d.ts +2 -1
  16. package/dist/mcp-code.js +87 -41
  17. package/dist/mcp-output-guard.d.ts +57 -2
  18. package/dist/mcp-output-guard.js +129 -24
  19. package/dist/mcp-panel.d.ts +1 -1
  20. package/dist/mcp-panel.js +34 -97
  21. package/dist/mcp-script-worker.mjs +5 -2
  22. package/dist/mcp-status.js +2 -1
  23. package/dist/metadata-cache.d.ts +1 -1
  24. package/dist/metadata-cache.js +25 -88
  25. package/dist/prompts.d.ts +1 -1
  26. package/dist/prompts.js +1 -1
  27. package/dist/proxy-modes.d.ts +40 -6
  28. package/dist/proxy-modes.js +270 -485
  29. package/dist/resource-tools.d.ts +15 -0
  30. package/dist/resource-tools.js +26 -1
  31. package/dist/sampling-handler.js +4 -1
  32. package/dist/search-ranking.d.ts +0 -2
  33. package/dist/search-ranking.js +66 -137
  34. package/dist/state.d.ts +1 -1
  35. package/dist/tool-loader.d.ts +32 -0
  36. package/dist/tool-loader.js +152 -0
  37. package/dist/tool-metadata.d.ts +67 -0
  38. package/dist/tool-metadata.js +23 -17
  39. package/dist/tool-registrar.d.ts +4 -4
  40. package/dist/tool-registrar.js +18 -13
  41. package/dist/tool-result-renderer.d.ts +3 -0
  42. package/dist/tool-result-renderer.js +8 -3
  43. package/dist/types.d.ts +11 -36
  44. package/dist/utils.js +2 -4
  45. package/gateway-arguments.ts +73 -0
  46. package/index.ts +166 -299
  47. package/init.ts +6 -20
  48. package/mcp-code.ts +71 -45
  49. package/mcp-output-guard.ts +145 -27
  50. package/mcp-panel.ts +38 -109
  51. package/mcp-script-worker.mjs +5 -2
  52. package/mcp-status.ts +2 -1
  53. package/metadata-cache.ts +31 -93
  54. package/package.json +14 -9
  55. package/prompts.ts +1 -1
  56. package/proxy-modes.ts +265 -517
  57. package/resource-tools.ts +36 -1
  58. package/sampling-handler.ts +4 -1
  59. package/scripts/build-app-bridge.mjs +15 -0
  60. package/scripts/build.mjs +3 -2
  61. package/scripts/prepare.mjs +1 -0
  62. package/search-ranking.ts +71 -135
  63. package/skills/mcp-scripting/SKILL.md +67 -20
  64. package/state.ts +1 -1
  65. package/tool-loader.ts +153 -0
  66. package/tool-metadata.ts +22 -17
  67. package/tool-registrar.ts +18 -13
  68. package/tool-result-renderer.ts +10 -2
  69. package/types.ts +15 -40
  70. package/utils.ts +2 -4
  71. package/dist/ts-shape.d.ts +0 -2
  72. package/dist/ts-shape.js +0 -147
  73. package/ts-shape.ts +0 -145
@@ -1,12 +1,13 @@
1
1
  import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
2
- import { formatToolName, isToolAllowed, resolveToolPrefix } from "./types.js";
2
+ import { formatToolName, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.js";
3
3
  import { resourceNameToToolName } from "./resource-tools.js";
4
4
  import { extractToolUiStreamMode } from "./utils.js";
5
5
  import { extractUiToolVisibility, isUiToolVisibleToModel } from "./ui-tool-visibility.js";
6
6
  export function buildToolMetadata(tools, resources, definition, serverName, prefix) {
7
7
  const metadata = [];
8
8
  const failedTools = [];
9
- const seenNames = new Set();
9
+ if (isServerDisabled(definition))
10
+ return { metadata, failedTools };
10
11
  const effectivePrefix = resolveToolPrefix(definition, prefix);
11
12
  for (const tool of tools) {
12
13
  if (!tool?.name) {
@@ -17,14 +18,10 @@ export function buildToolMetadata(tools, resources, definition, serverName, pref
17
18
  continue;
18
19
  }
19
20
  const name = formatToolName(tool.name, serverName, effectivePrefix);
20
- if (seenNames.has(name)) {
21
- continue;
22
- }
23
21
  const uiVisibility = extractUiToolVisibility(tool._meta);
24
22
  if (!isUiToolVisibleToModel(uiVisibility)) {
25
23
  continue;
26
24
  }
27
- seenNames.add(name);
28
25
  let uiResourceUri;
29
26
  try {
30
27
  uiResourceUri = getToolUiResourceUri({ _meta: tool._meta });
@@ -34,11 +31,10 @@ export function buildToolMetadata(tools, resources, definition, serverName, pref
34
31
  }
35
32
  const uiStreamMode = extractToolUiStreamMode(tool._meta);
36
33
  metadata.push({
34
+ ...tool,
37
35
  name,
38
36
  originalName: tool.name,
39
37
  description: tool.description ?? "",
40
- ...(tool.inputSchema !== undefined ? { inputSchema: tool.inputSchema } : {}),
41
- ...(tool.annotations !== undefined ? { annotations: tool.annotations } : {}),
42
38
  ...(uiResourceUri !== undefined ? { uiResourceUri } : {}),
43
39
  ...(uiVisibility !== undefined ? { uiVisibility } : {}),
44
40
  ...(uiStreamMode !== undefined ? { uiStreamMode } : {}),
@@ -46,20 +42,19 @@ export function buildToolMetadata(tools, resources, definition, serverName, pref
46
42
  }
47
43
  if (definition.exposeResources !== false) {
48
44
  for (const resource of resources) {
45
+ if (!resource?.name || !resource?.uri)
46
+ continue;
49
47
  const baseName = `read_${resourceNameToToolName(resource.name)}`;
50
48
  if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools)) {
51
49
  continue;
52
50
  }
53
51
  const name = formatToolName(baseName, serverName, effectivePrefix);
54
- if (seenNames.has(name)) {
55
- continue;
56
- }
57
- seenNames.add(name);
58
52
  metadata.push({
59
53
  name,
60
54
  originalName: baseName,
61
55
  description: resource.description ?? `Read resource: ${resource.uri}`,
62
56
  resourceUri: resource.uri,
57
+ resourceDescriptor: resource,
63
58
  });
64
59
  }
65
60
  }
@@ -68,18 +63,29 @@ export function buildToolMetadata(tools, resources, definition, serverName, pref
68
63
  export function totalToolCount(state) {
69
64
  let count = 0;
70
65
  for (const metadata of state.toolMetadata.values()) {
71
- count += metadata.length;
66
+ count += metadata.filter(tool => tool.resourceUri === undefined).length;
72
67
  }
73
68
  return count;
74
69
  }
70
+ export function toToolDescriptor(server, tool) {
71
+ const { name: path, originalName: name, resourceUri: _resourceUri, resourceDescriptor: _resourceDescriptor, uiResourceUri: _uiResourceUri, uiVisibility: _uiVisibility, uiStreamMode: _uiStreamMode, ...descriptor } = tool;
72
+ return { ...descriptor, server, path, name };
73
+ }
74
+ export function catalogCoverage(state, server) {
75
+ const servers = Object.keys(state.config.mcpServers).filter(name => (!server || name === server) && !isServerDisabled(state.config.mcpServers[name]));
76
+ const knownServers = servers.filter(name => state.toolMetadata.has(name));
77
+ const unknownServers = servers.filter(name => !state.toolMetadata.has(name));
78
+ return { complete: unknownServers.length === 0, knownServers, unknownServers };
79
+ }
75
80
  export function findToolByName(metadata, toolName) {
76
81
  if (!metadata)
77
82
  return undefined;
78
- const exact = metadata.find(m => m.name === toolName);
79
- if (exact)
80
- return exact;
83
+ const exact = metadata.filter(m => m.name === toolName);
84
+ if (exact.length > 0)
85
+ return exact.length === 1 ? exact[0] : undefined;
81
86
  const normalized = toolName.replace(/-/g, "_");
82
- return metadata.find(m => m.name.replace(/-/g, "_") === normalized);
87
+ const matches = metadata.filter(m => m.name.replace(/-/g, "_") === normalized);
88
+ return matches.length === 1 ? matches[0] : undefined;
83
89
  }
84
90
  export function formatSchema(schema, indent = " ") {
85
91
  if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
@@ -1,10 +1,10 @@
1
1
  import type { McpContent, ContentBlock } from "./types.ts";
2
+ import { type McpPayloadFile } from "./mcp-output-guard.ts";
2
3
  /**
3
4
  * Transform MCP content types to Pi content blocks.
4
5
  */
5
6
  export declare function transformMcpContent(content: McpContent[]): ContentBlock[];
6
- /**
7
- * Resolve a tool result's content blocks, falling back to structuredContent
8
- * when content is empty.
9
- */
7
+ /** Human content and structured content are complementary protocol fields. */
10
8
  export declare function resolveMcpResultContent(result: Record<string, unknown>): ContentBlock[];
9
+ /** Persist content the model cannot consume; never imply an audio/blob payload was rendered. */
10
+ export declare function renderMcpResultContent(result: Record<string, unknown>, payloadFiles?: McpPayloadFile[]): ContentBlock[];
@@ -1,6 +1,5 @@
1
- // tool-registrar.ts - MCP content transformation
2
- // NOTE: Tools are NOT registered with Pi - only the unified `mcp` proxy tool is registered.
3
- // This keeps the LLM context small (1 tool instead of 100s).
1
+ // MCP protocol content to model-visible content.
2
+ import { formatMcpPayloadFile } from "./mcp-output-guard.js";
4
3
  /**
5
4
  * Transform MCP content types to Pi content blocks.
6
5
  */
@@ -18,7 +17,7 @@ export function transformMcpContent(content) {
18
17
  }
19
18
  if (c.type === "resource") {
20
19
  const resourceUri = c.resource?.uri ?? "(no URI)";
21
- const resourceContent = c.resource?.text ?? (c.resource ? JSON.stringify(c.resource) : "(no content)");
20
+ const resourceContent = c.resource?.text ?? (c.resource?.blob !== undefined ? "[Binary content; not rendered]" : "(no content)");
22
21
  return {
23
22
  type: "text",
24
23
  text: `[Resource: ${resourceUri}]\n${resourceContent}`,
@@ -35,24 +34,30 @@ export function transformMcpContent(content) {
35
34
  if (c.type === "audio") {
36
35
  return {
37
36
  type: "text",
38
- text: `[Audio content: ${c.mimeType ?? "audio/*"}]`,
37
+ text: `[Audio content: ${c.mimeType ?? "audio/*"}; not rendered]`,
39
38
  };
40
39
  }
41
40
  return { type: "text", text: JSON.stringify(c) };
42
41
  });
43
42
  }
44
- /**
45
- * Resolve a tool result's content blocks, falling back to structuredContent
46
- * when content is empty.
47
- */
43
+ /** Human content and structured content are complementary protocol fields. */
48
44
  export function resolveMcpResultContent(result) {
49
45
  const blocks = transformMcpContent((Array.isArray(result.content) ? result.content : []));
50
- if (blocks.length > 0)
51
- return blocks;
52
46
  if (result.structuredContent !== undefined) {
53
- return [{ type: "text", text: stringifyStructuredContent(result.structuredContent) }];
47
+ blocks.push({ type: "text", text: stringifyStructuredContent(result.structuredContent) });
54
48
  }
55
- return [];
49
+ return blocks;
50
+ }
51
+ /** Persist content the model cannot consume; never imply an audio/blob payload was rendered. */
52
+ export function renderMcpResultContent(result, payloadFiles = []) {
53
+ const content = Array.isArray(result.contents)
54
+ ? result.contents.map(resource => typeof resource.text === "string" ? { type: "text", text: resource.text } : { type: "resource", resource })
55
+ : Array.isArray(result.content) ? result.content : [];
56
+ const rendered = content.map((block, index) => {
57
+ const file = payloadFiles.find(file => file.index === index);
58
+ return file ? { type: "text", text: formatMcpPayloadFile(file) } : block;
59
+ });
60
+ return resolveMcpResultContent({ ...result, content: rendered });
56
61
  }
57
62
  function stringifyStructuredContent(value) {
58
63
  try {
@@ -10,6 +10,9 @@ export interface McpProxyToolCallInput {
10
10
  connect?: string;
11
11
  describe?: string;
12
12
  search?: string;
13
+ query?: string;
14
+ uri?: string;
15
+ ref?: string;
13
16
  includeSchemas?: boolean;
14
17
  server?: string;
15
18
  action?: string;
@@ -79,8 +79,15 @@ function hasUsefulObjectContent(value) {
79
79
  return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
80
80
  }
81
81
  export function formatMcpProxyToolCallLines(args, maxInputChars = DEFAULT_MAX_CALL_INPUT_CHARS) {
82
- if (args.action === "ui-messages")
82
+ if (args.action === "ui-messages" || args.action === "status")
83
83
  return [`mcp ${args.action}`];
84
+ if (args.action) {
85
+ const target = args.tool ?? args.query ?? args.uri ?? args.ref;
86
+ let line = `mcp ${args.action}${target ? ` ${target}` : ""}${args.server ? `${target ? " @" : ""} ${args.server}` : ""}`;
87
+ if (args.action === "search" && args.includeSchemas === false)
88
+ line += " (schemas hidden)";
89
+ return args.action === "call" && args.args ? [line, formatJsonish(args.args, maxInputChars)] : [line];
90
+ }
84
91
  if (args.tool) {
85
92
  const target = args.server ? `${args.tool} @ ${args.server}` : args.tool;
86
93
  const lines = [`mcp call ${target}`];
@@ -102,8 +109,6 @@ export function formatMcpProxyToolCallLines(args, maxInputChars = DEFAULT_MAX_CA
102
109
  }
103
110
  if (args.server)
104
111
  return [`mcp list ${args.server}`];
105
- if (args.action)
106
- return [`mcp ${args.action}`];
107
112
  return ["mcp status"];
108
113
  }
109
114
  export function formatMcpDirectToolCallLines(displayName, args, maxInputChars = DEFAULT_MAX_CALL_INPUT_CHARS) {
package/dist/types.d.ts CHANGED
@@ -13,6 +13,8 @@ export interface McpServerStatusSnapshot {
13
13
  readonly name: string;
14
14
  readonly status: McpServerRuntimeStatus;
15
15
  readonly toolCount: number;
16
+ /** False means the server has not been discovered, rather than an empty inventory. */
17
+ readonly catalogKnown?: boolean;
16
18
  readonly resourceCount?: number;
17
19
  readonly failedAgoSeconds?: number;
18
20
  readonly disabled: boolean;
@@ -30,21 +32,10 @@ type SdkTool = ListToolsResult["tools"][number];
30
32
  type SdkResource = ListResourcesResult["resources"][number];
31
33
  type SdkPrompt = ListPromptsResult["prompts"][number];
32
34
  type SdkPromptArgument = NonNullable<SdkPrompt["arguments"]>[number];
33
- export interface McpTool {
34
- name: SdkTool["name"];
35
- title?: SdkTool["title"];
36
- description?: SdkTool["description"];
35
+ export interface McpTool extends Omit<SdkTool, "inputSchema"> {
37
36
  inputSchema?: SdkTool["inputSchema"];
38
- annotations?: SdkTool["annotations"];
39
- _meta?: SdkTool["_meta"];
40
- }
41
- export interface McpResource {
42
- uri: SdkResource["uri"];
43
- name: SdkResource["name"];
44
- description?: SdkResource["description"];
45
- mimeType?: SdkResource["mimeType"];
46
- _meta?: SdkResource["_meta"];
47
37
  }
38
+ export type McpResource = SdkResource;
48
39
  export interface McpPromptArgument {
49
40
  name: SdkPromptArgument["name"];
50
41
  description?: SdkPromptArgument["description"];
@@ -327,7 +318,7 @@ export interface McpSettings {
327
318
  /** Freeze direct-tool registration after the initial sync. Automatic metadata updates
328
319
  * (reconnects, lazy-connect, tool-list-changed) won't rebuild the system prompt,
329
320
  * preserving the prompt-cache prefix. The agent rediscovers explicitly via
330
- * mcp({ connect: "server" }). Default: false. */
321
+ * mcp({ action: "connect", server: "server" }). Default: false. */
331
322
  freezeDirectTools?: boolean;
332
323
  autoAuth?: boolean;
333
324
  sampling?: boolean;
@@ -411,15 +402,14 @@ export interface McpAdapterOptions {
411
402
  onToolCall?: (event: McpToolCallEvent) => Promise<void>;
412
403
  }
413
404
  export type ServerDefinition = ServerEntry;
414
- export interface ToolMetadata {
405
+ export interface ToolMetadata extends Omit<McpTool, "name" | "description"> {
415
406
  name: string;
416
407
  originalName: string;
417
408
  description: string;
418
409
  resourceUri?: string;
410
+ resourceDescriptor?: McpResource;
419
411
  uiResourceUri?: string;
420
412
  uiVisibility?: UiToolVisibility[];
421
- inputSchema?: unknown;
422
- annotations?: McpTool["annotations"];
423
413
  uiStreamMode?: UiStreamMode;
424
414
  }
425
415
  export interface PromptMetadata {
@@ -430,16 +420,9 @@ export interface PromptMetadata {
430
420
  description: string;
431
421
  arguments: McpPromptArgument[];
432
422
  }
433
- export interface DirectToolSpec {
423
+ export interface DirectToolSpec extends Omit<ToolMetadata, "name"> {
434
424
  serverName: string;
435
- originalName: string;
436
425
  prefixedName: string;
437
- description: string;
438
- inputSchema?: unknown;
439
- annotations?: McpTool["annotations"];
440
- resourceUri?: string;
441
- uiResourceUri?: string;
442
- uiStreamMode?: UiStreamMode;
443
426
  }
444
427
  export interface ServerProvenance {
445
428
  path: string;
@@ -450,20 +433,12 @@ export interface McpAuthResult {
450
433
  ok: boolean;
451
434
  message?: string;
452
435
  }
453
- export interface CachedTool {
454
- name: string;
455
- description?: string;
456
- inputSchema?: unknown;
457
- annotations?: McpTool["annotations"];
436
+ export interface CachedTool extends McpTool {
458
437
  uiResourceUri?: string;
459
438
  uiVisibility?: UiToolVisibility[];
460
- uiStreamMode?: "eager" | "stream-first";
461
- }
462
- export interface CachedResource {
463
- uri: string;
464
- name: string;
465
- description?: string;
439
+ uiStreamMode?: UiStreamMode;
466
440
  }
441
+ export type CachedResource = McpResource;
467
442
  export interface CachedPrompt {
468
443
  name: string;
469
444
  title?: string;
package/dist/utils.js CHANGED
@@ -246,11 +246,9 @@ export function truncateAtWord(text, target) {
246
246
  return truncated + "...";
247
247
  }
248
248
  export function normalizeDirectToolInputSchema(schema) {
249
- const inputSchema = schema && typeof schema === "object" && !Array.isArray(schema)
250
- ? schema
249
+ return schema && typeof schema === "object" && !Array.isArray(schema)
250
+ ? structuredClone(schema)
251
251
  : { type: "object", properties: {} };
252
- const { $schema, additionalProperties, ...normalized } = inputSchema;
253
- return normalized;
254
252
  }
255
253
  export function formatAuthRequiredMessage(config, serverName, defaultMessage) {
256
254
  const template = config.settings?.authRequiredMessage;
@@ -0,0 +1,73 @@
1
+ import { Type } from "typebox";
2
+ import { MAX_PAGE_SIZE, MAX_TOOL_NAME_LENGTH } from "./search-ranking.ts";
3
+
4
+ const actions = ["status", "search", "list", "describe", "call", "connect", "instructions", "resources", "read-resource", "read-result", "auth-start", "auth-complete", "ui-messages"] as const;
5
+ export type GatewayArguments = {
6
+ action: typeof actions[number];
7
+ server?: string;
8
+ tool?: string;
9
+ args?: Record<string, unknown>;
10
+ query?: string;
11
+ includeSchemas?: boolean;
12
+ limit?: number;
13
+ offset?: number;
14
+ uri?: string;
15
+ ref?: string;
16
+ path?: string;
17
+ fields?: string[];
18
+ };
19
+
20
+ const requirements: Partial<Record<GatewayArguments["action"], string[]>> = {
21
+ search: ["query"], list: ["server"], describe: ["tool"], call: ["tool"],
22
+ connect: ["server"], instructions: ["server"], resources: ["server"],
23
+ "read-resource": ["server", "uri"], "read-result": ["ref"], "auth-start": ["server"], "auth-complete": ["server"],
24
+ };
25
+
26
+ export const gatewayParameters = Type.Object({
27
+ action: Type.Unsafe<GatewayArguments["action"]>({ type: "string", enum: [...actions] }),
28
+ server: Type.Optional(Type.String({ description: "MCP server; required for server and resource actions." })),
29
+ tool: Type.Optional(Type.String({ maxLength: MAX_TOOL_NAME_LENGTH, description: "Exact tool name for call or describe. Use server to disambiguate." })),
30
+ args: Type.Optional(Type.Object({}, { additionalProperties: true, description: "Tool arguments, or {redirectUrl} for auth-complete." })),
31
+ query: Type.Optional(Type.String({ description: "Search terms. Prefer mcp_search to discover and load tools." })),
32
+ includeSchemas: Type.Optional(Type.Boolean({ description: "Include complete schemas in search results (default true)." })),
33
+ limit: Type.Optional(Type.Integer({ minimum: 1, description: "Page size (default 12, max 100); for read-result, characters of selected JSON." })),
34
+ offset: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based item offset; for read-result, character offset in selected JSON." })),
35
+ uri: Type.Optional(Type.String({ description: "Exact MCP resource URI for read-resource." })),
36
+ ref: Type.Optional(Type.String({ description: "Saved result reference returned by an earlier MCP call; read-result never calls the server again." })),
37
+ path: Type.Optional(Type.String({ description: "JSON Pointer into a saved result, e.g. /structuredContent/rows." })),
38
+ fields: Type.Optional(Type.Array(Type.String(), { description: "Immediate object fields to retain when reading a saved result." })),
39
+ }, {
40
+ additionalProperties: false,
41
+ allOf: [
42
+ ...Object.entries(requirements).map(([action, required]) => ({ if: { properties: { action: { const: action } } }, then: { required } })),
43
+ { if: { properties: { action: { enum: ["search", "list", "connect", "resources"] } } }, then: { properties: { limit: { maximum: MAX_PAGE_SIZE } } } },
44
+ ],
45
+ });
46
+
47
+ /** Migrate stored v5 calls before host validation; the advertised schema stays explicit. */
48
+ export function prepareGatewayArguments(input: unknown): GatewayArguments {
49
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("MCP arguments must be an object");
50
+ const value = input as Record<string, unknown>;
51
+ let action = value.action;
52
+ const result = { ...value };
53
+ if (action === undefined) {
54
+ action = value.tool ? "call" : value.connect ? "connect" : value.describe ? "describe" : value.instructions ? "instructions" : value.search !== undefined ? "search" : value.server ? "list" : "status";
55
+ if (action === "connect") result.server = value.connect;
56
+ if (action === "describe") result.tool = value.describe;
57
+ if (action === "instructions") result.server = value.instructions;
58
+ if (action === "search") result.query = value.search;
59
+ }
60
+ for (const key of ["connect", "describe", "instructions", "search"]) delete result[key];
61
+ if (!actions.includes(action as GatewayArguments["action"])) throw new Error(`Unknown MCP action: ${String(action)}`);
62
+ result.action = action;
63
+ if (typeof result.args === "string") {
64
+ result.args = result.args === "" ? undefined : JSON.parse(result.args);
65
+ }
66
+ if (result.args !== undefined && (!result.args || typeof result.args !== "object" || Array.isArray(result.args))) {
67
+ throw new Error("Invalid args: expected a JSON object");
68
+ }
69
+ for (const field of requirements[action as GatewayArguments["action"]] ?? []) {
70
+ if (typeof result[field] !== "string" || (field !== "query" && result[field] === "")) throw new Error(`${action} requires ${field}`);
71
+ }
72
+ return result as GatewayArguments;
73
+ }