@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.
- package/CHANGELOG.md +25 -1
- package/OAUTH.md +3 -3
- package/README.md +153 -85
- package/app-bridge.bundle.js +75 -33
- package/commands.ts +9 -6
- package/direct-tools.ts +25 -358
- package/dist/app-bridge.bundle.js +75 -33
- package/dist/commands.js +9 -6
- package/dist/direct-tools.d.ts +2 -4
- package/dist/direct-tools.js +21 -319
- package/dist/gateway-arguments.d.ts +33 -0
- package/dist/gateway-arguments.js +63 -0
- package/dist/index.js +181 -276
- package/dist/init.js +6 -20
- package/dist/mcp-code.d.ts +2 -1
- package/dist/mcp-code.js +87 -41
- package/dist/mcp-output-guard.d.ts +57 -2
- package/dist/mcp-output-guard.js +129 -24
- package/dist/mcp-panel.d.ts +1 -1
- package/dist/mcp-panel.js +34 -97
- package/dist/mcp-script-worker.mjs +5 -2
- package/dist/mcp-status.js +2 -1
- package/dist/metadata-cache.d.ts +1 -1
- package/dist/metadata-cache.js +25 -88
- package/dist/prompts.d.ts +1 -1
- package/dist/prompts.js +1 -1
- package/dist/proxy-modes.d.ts +40 -6
- package/dist/proxy-modes.js +270 -485
- package/dist/resource-tools.d.ts +15 -0
- package/dist/resource-tools.js +26 -1
- package/dist/sampling-handler.js +4 -1
- package/dist/search-ranking.d.ts +0 -2
- package/dist/search-ranking.js +66 -137
- package/dist/state.d.ts +1 -1
- package/dist/tool-loader.d.ts +32 -0
- package/dist/tool-loader.js +152 -0
- package/dist/tool-metadata.d.ts +67 -0
- package/dist/tool-metadata.js +23 -17
- package/dist/tool-registrar.d.ts +4 -4
- package/dist/tool-registrar.js +18 -13
- package/dist/tool-result-renderer.d.ts +3 -0
- package/dist/tool-result-renderer.js +8 -3
- package/dist/types.d.ts +11 -36
- package/dist/utils.js +2 -4
- package/gateway-arguments.ts +73 -0
- package/index.ts +166 -299
- package/init.ts +6 -20
- package/mcp-code.ts +71 -45
- package/mcp-output-guard.ts +145 -27
- package/mcp-panel.ts +38 -109
- package/mcp-script-worker.mjs +5 -2
- package/mcp-status.ts +2 -1
- package/metadata-cache.ts +31 -93
- package/package.json +14 -9
- package/prompts.ts +1 -1
- package/proxy-modes.ts +265 -517
- package/resource-tools.ts +36 -1
- package/sampling-handler.ts +4 -1
- package/scripts/build-app-bridge.mjs +15 -0
- package/scripts/build.mjs +3 -2
- package/scripts/prepare.mjs +1 -0
- package/search-ranking.ts +71 -135
- package/skills/mcp-scripting/SKILL.md +67 -20
- package/state.ts +1 -1
- package/tool-loader.ts +153 -0
- package/tool-metadata.ts +22 -17
- package/tool-registrar.ts +18 -13
- package/tool-result-renderer.ts +10 -2
- package/types.ts +15 -40
- package/utils.ts +2 -4
- package/dist/ts-shape.d.ts +0 -2
- package/dist/ts-shape.js +0 -147
- package/ts-shape.ts +0 -145
package/dist/tool-metadata.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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)) {
|
package/dist/tool-registrar.d.ts
CHANGED
|
@@ -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[];
|
package/dist/tool-registrar.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
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 ?
|
|
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
|
-
|
|
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 {
|
|
@@ -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?:
|
|
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
|
-
|
|
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
|
+
}
|