@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/tool-loader.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type, type TSchema } from "typebox";
|
|
3
|
+
import { createDirectToolExecutor, directToolSelection } from "./direct-tools.ts";
|
|
4
|
+
import { isServerCacheValid, reconstructToolMetadata, type MetadataCache } from "./metadata-cache.ts";
|
|
5
|
+
import type { McpExtensionState } from "./state.ts";
|
|
6
|
+
import { createMcpDirectToolCallRenderer, renderMcpToolResult } from "./tool-result-renderer.ts";
|
|
7
|
+
import { isServerDisabled, type DirectToolSpec, type McpAdapterOptions, type McpConfig, type ToolMetadata } from "./types.ts";
|
|
8
|
+
import { normalizeDirectToolInputSchema } from "./utils.ts";
|
|
9
|
+
import { isToolCallApprovalRequired } from "./tool-approval.ts";
|
|
10
|
+
|
|
11
|
+
type Reference = { name: string; namespace?: string };
|
|
12
|
+
type Selection = { server: string; tool: string };
|
|
13
|
+
type SearchDefinition<TParams extends TSchema, TDetails> = Omit<ToolDefinition<TParams, TDetails>, "execute"> & {
|
|
14
|
+
execute: (...args: Parameters<ToolDefinition<TParams, TDetails>["execute"]>) => Promise<AgentToolResult<TDetails> & { tools: Reference[] }>;
|
|
15
|
+
};
|
|
16
|
+
// Optional fork APIs; official Pi uses the same loader as an ordinary tool.
|
|
17
|
+
export type SearchAPI = ExtensionAPI & {
|
|
18
|
+
registerToolSearch?: <TParams extends TSchema, TDetails>(definition: SearchDefinition<TParams, TDetails>) => void;
|
|
19
|
+
getActiveToolReferences?: () => Reference[];
|
|
20
|
+
setActiveToolReferences?: (tools: Reference[]) => void;
|
|
21
|
+
};
|
|
22
|
+
const ENTRY = "mcp-tool-selection";
|
|
23
|
+
const key = (value: Selection) => JSON.stringify([value.server, value.tool]);
|
|
24
|
+
const refKey = (value: Reference) => JSON.stringify([value.namespace ?? null, value.name]);
|
|
25
|
+
const reserved = new Set(["read", "bash", "powershell", "edit", "write", "grep", "find", "ls", "mcp", "mcp_search", "mcp_script"]);
|
|
26
|
+
|
|
27
|
+
export function supportsNativeAsync(ctx: ExtensionContext): boolean {
|
|
28
|
+
const compat = ctx.model?.compat;
|
|
29
|
+
return "getPendingToolCalls" in ctx && typeof ctx.getPendingToolCalls === "function"
|
|
30
|
+
&& !!compat && "supportsAsyncTools" in compat && compat.supportsAsyncTools === true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createToolLoader(
|
|
34
|
+
pi: SearchAPI,
|
|
35
|
+
getState: () => McpExtensionState | null,
|
|
36
|
+
getInitPromise: () => Promise<McpExtensionState> | null,
|
|
37
|
+
beforeExecute?: McpAdapterOptions["beforeExecute"],
|
|
38
|
+
) {
|
|
39
|
+
const native = typeof pi.registerToolSearch === "function"
|
|
40
|
+
&& typeof pi.getActiveToolReferences === "function"
|
|
41
|
+
&& typeof pi.setActiveToolReferences === "function";
|
|
42
|
+
pi.registerEntryRenderer(ENTRY, () => undefined);
|
|
43
|
+
const registered = new Map<string, { selection: Selection; ref: Reference; fingerprint: string; pinned: boolean }>();
|
|
44
|
+
let available = new Set<string>();
|
|
45
|
+
let selected = new Map<string, Selection>();
|
|
46
|
+
let inactive = new Map<string, Selection>();
|
|
47
|
+
let restoring = true;
|
|
48
|
+
const active = (): Reference[] => native ? pi.getActiveToolReferences!() : pi.getActiveTools().map(name => ({ name }));
|
|
49
|
+
const setActive = (refs: Reference[]) => {
|
|
50
|
+
const unique = [...new Map(refs.map(ref => [refKey(ref), ref])).values()];
|
|
51
|
+
if (JSON.stringify(active()) === JSON.stringify(unique)) return;
|
|
52
|
+
if (native) pi.setActiveToolReferences!(unique);
|
|
53
|
+
else pi.setActiveTools(unique.map(ref => ref.name));
|
|
54
|
+
};
|
|
55
|
+
function capture() {
|
|
56
|
+
const current = new Set(active().map(refKey));
|
|
57
|
+
for (const [id, { selection, ref, pinned }] of registered) {
|
|
58
|
+
if (!available.has(id)) continue;
|
|
59
|
+
if (current.has(refKey(ref))) { if (!pinned) selected.set(id, selection); inactive.delete(id); }
|
|
60
|
+
else { selected.delete(id); if (pinned) inactive.set(id, selection); }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function persist() {
|
|
64
|
+
capture();
|
|
65
|
+
pi.appendEntry(ENTRY, { selected: [...selected.values()], inactive: [...inactive.values()] });
|
|
66
|
+
}
|
|
67
|
+
function restore(ctx: ExtensionContext) {
|
|
68
|
+
selected = new Map();
|
|
69
|
+
inactive = new Map();
|
|
70
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
71
|
+
if (entry.type !== "custom" || entry.customType !== ENTRY || !entry.data || typeof entry.data !== "object") continue;
|
|
72
|
+
const data = entry.data as { selected?: Selection[]; inactive?: Selection[] };
|
|
73
|
+
for (const [field, target] of [[data.selected, selected], [data.inactive, inactive]] as const) {
|
|
74
|
+
target.clear();
|
|
75
|
+
for (const selection of Array.isArray(field) ? field : []) {
|
|
76
|
+
if (typeof selection?.server === "string" && typeof selection.tool === "string") target.set(key(selection), selection);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
restoring = true;
|
|
81
|
+
}
|
|
82
|
+
function sync(config: McpConfig, cache: MetadataCache | null, env?: string[] | null, asyncSupported = false, registerGateways?: (pins: DirectToolSpec[]) => void) {
|
|
83
|
+
if (!restoring) capture();
|
|
84
|
+
const previousRefs = new Set([...registered.values()].map(item => refKey(item.ref)));
|
|
85
|
+
const existing = new Set(pi.getAllTools().map(tool => refKey(tool)));
|
|
86
|
+
const specs: Array<{ spec: DirectToolSpec; selection: Selection; ref: Reference; pinned: boolean }> = [];
|
|
87
|
+
for (const [server, definition] of Object.entries(env === null ? {} : config.mcpServers)) {
|
|
88
|
+
if (isServerDisabled(definition)) continue;
|
|
89
|
+
const entry = cache?.servers[server];
|
|
90
|
+
const metadata = getState()?.toolMetadata.get(server) ?? (entry && isServerCacheValid(entry, definition)
|
|
91
|
+
? reconstructToolMetadata(server, entry, config.settings?.toolPrefix ?? "server", definition) : []);
|
|
92
|
+
const selection = directToolSelection(config, server, env ?? undefined);
|
|
93
|
+
for (const { name: prefixedName, ...tool } of metadata) {
|
|
94
|
+
if (tool.resourceUri) continue;
|
|
95
|
+
const pinned = selection === true || (Array.isArray(selection) && selection.includes(tool.originalName));
|
|
96
|
+
const ref = native && !pinned ? { namespace: `mcp_${server}`, name: tool.originalName } : { name: prefixedName };
|
|
97
|
+
specs.push({ spec: { ...tool, prefixedName, serverName: server }, selection: { server, tool: tool.originalName }, ref, pinned });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const counts = new Map<string, number>();
|
|
101
|
+
for (const { ref } of specs) counts.set(refKey(ref), (counts.get(refKey(ref)) ?? 0) + 1);
|
|
102
|
+
available = new Set();
|
|
103
|
+
const enabled: Reference[] = [];
|
|
104
|
+
for (const { spec, selection, ref, pinned } of specs) {
|
|
105
|
+
const id = key(selection);
|
|
106
|
+
const refId = refKey(ref);
|
|
107
|
+
if ((!ref.namespace && reserved.has(ref.name)) || counts.get(refId)! > 1 || (existing.has(refId) && !previousRefs.has(refId))) continue;
|
|
108
|
+
available.add(id);
|
|
109
|
+
const asyncTool = asyncSupported && !beforeExecute && !spec.uiResourceUri && !isToolCallApprovalRequired(config, spec.serverName, spec);
|
|
110
|
+
const fingerprint = JSON.stringify({ spec, ref, asyncTool });
|
|
111
|
+
if (registered.get(id)?.fingerprint !== fingerprint) {
|
|
112
|
+
pi.registerTool({
|
|
113
|
+
...ref,
|
|
114
|
+
...(asyncTool ? { async: true } : {}),
|
|
115
|
+
...(beforeExecute ? { executionMode: "sequential" as const } : {}),
|
|
116
|
+
label: spec.title ?? `MCP: ${spec.originalName}`,
|
|
117
|
+
description: spec.description || "(no description)",
|
|
118
|
+
parameters: Type.Unsafe<Record<string, unknown>>(normalizeDirectToolInputSchema(spec.inputSchema)),
|
|
119
|
+
execute: createDirectToolExecutor(getState, getInitPromise, spec, beforeExecute),
|
|
120
|
+
renderCall: createMcpDirectToolCallRenderer(spec.prefixedName),
|
|
121
|
+
renderResult: renderMcpToolResult,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
registered.set(id, { selection, ref, fingerprint, pinned });
|
|
125
|
+
if (selected.has(id) || (pinned && !inactive.has(id))) enabled.push(ref);
|
|
126
|
+
}
|
|
127
|
+
const permitted = new Set(pi.getAllTools().map(refKey));
|
|
128
|
+
const pins = specs.filter(item => item.pinned && available.has(key(item.selection)) && permitted.has(refKey(item.ref))).map(item => item.spec);
|
|
129
|
+
registerGateways?.(pins);
|
|
130
|
+
// Official 0.87 reactivates allowlisted definitions on registration. Apply our loadout last.
|
|
131
|
+
const ownedRefs = new Set([...previousRefs, ...[...registered.values()].map(item => refKey(item.ref))]);
|
|
132
|
+
setActive([...active().filter(ref => !ownedRefs.has(refKey(ref))), ...enabled]);
|
|
133
|
+
restoring = false;
|
|
134
|
+
return pins;
|
|
135
|
+
}
|
|
136
|
+
function activate(matches: Array<{ server: string; tool: ToolMetadata }>) {
|
|
137
|
+
const requested = matches.flatMap(({ server, tool }) => {
|
|
138
|
+
const id = key({ server, tool: tool.originalName });
|
|
139
|
+
const registration = available.has(id) ? registered.get(id) : undefined;
|
|
140
|
+
return registration ? [registration] : [];
|
|
141
|
+
});
|
|
142
|
+
// No await between reading and extending the current loadout: parallel searches union their selections.
|
|
143
|
+
setActive([...active(), ...requested.map(item => item.ref)]);
|
|
144
|
+
const current = new Set(active().map(refKey));
|
|
145
|
+
const tools = requested.filter(item => current.has(refKey(item.ref))).map(({ selection, ref }) => {
|
|
146
|
+
selected.set(key(selection), selection);
|
|
147
|
+
return ref;
|
|
148
|
+
});
|
|
149
|
+
persist();
|
|
150
|
+
return tools;
|
|
151
|
+
}
|
|
152
|
+
return { native, restore, sync, activate, persist, selectedServers: () => [...new Set([...selected.values()].map(item => item.server))] };
|
|
153
|
+
}
|
package/tool-metadata.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
|
|
2
2
|
import type { McpExtensionState } from "./state.ts";
|
|
3
3
|
import type { ToolMetadata, McpTool, McpResource, ServerEntry, ToolPrefix } from "./types.ts";
|
|
4
|
-
import { formatToolName, isToolAllowed, resolveToolPrefix } from "./types.ts";
|
|
4
|
+
import { formatToolName, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.ts";
|
|
5
5
|
import { resourceNameToToolName } from "./resource-tools.ts";
|
|
6
6
|
import { extractToolUiStreamMode } from "./utils.ts";
|
|
7
7
|
import { extractUiToolVisibility, isUiToolVisibleToModel } from "./ui-tool-visibility.ts";
|
|
@@ -15,7 +15,7 @@ export function buildToolMetadata(
|
|
|
15
15
|
): { metadata: ToolMetadata[]; failedTools: string[] } {
|
|
16
16
|
const metadata: ToolMetadata[] = [];
|
|
17
17
|
const failedTools: string[] = [];
|
|
18
|
-
|
|
18
|
+
if (isServerDisabled(definition)) return { metadata, failedTools };
|
|
19
19
|
const effectivePrefix = resolveToolPrefix(definition, prefix);
|
|
20
20
|
|
|
21
21
|
for (const tool of tools) {
|
|
@@ -28,16 +28,11 @@ export function buildToolMetadata(
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const name = formatToolName(tool.name, serverName, effectivePrefix);
|
|
31
|
-
if (seenNames.has(name)) {
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
31
|
|
|
35
32
|
const uiVisibility = extractUiToolVisibility(tool._meta);
|
|
36
33
|
if (!isUiToolVisibleToModel(uiVisibility)) {
|
|
37
34
|
continue;
|
|
38
35
|
}
|
|
39
|
-
seenNames.add(name);
|
|
40
|
-
|
|
41
36
|
let uiResourceUri: string | undefined;
|
|
42
37
|
try {
|
|
43
38
|
uiResourceUri = getToolUiResourceUri({ _meta: tool._meta });
|
|
@@ -46,11 +41,10 @@ export function buildToolMetadata(
|
|
|
46
41
|
}
|
|
47
42
|
const uiStreamMode = extractToolUiStreamMode(tool._meta);
|
|
48
43
|
metadata.push({
|
|
44
|
+
...tool,
|
|
49
45
|
name,
|
|
50
46
|
originalName: tool.name,
|
|
51
47
|
description: tool.description ?? "",
|
|
52
|
-
...(tool.inputSchema !== undefined ? { inputSchema: tool.inputSchema } : {}),
|
|
53
|
-
...(tool.annotations !== undefined ? { annotations: tool.annotations } : {}),
|
|
54
48
|
...(uiResourceUri !== undefined ? { uiResourceUri } : {}),
|
|
55
49
|
...(uiVisibility !== undefined ? { uiVisibility } : {}),
|
|
56
50
|
...(uiStreamMode !== undefined ? { uiStreamMode } : {}),
|
|
@@ -59,22 +53,20 @@ export function buildToolMetadata(
|
|
|
59
53
|
|
|
60
54
|
if (definition.exposeResources !== false) {
|
|
61
55
|
for (const resource of resources) {
|
|
56
|
+
if (!resource?.name || !resource?.uri) continue;
|
|
62
57
|
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
63
58
|
if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools)) {
|
|
64
59
|
continue;
|
|
65
60
|
}
|
|
66
61
|
|
|
67
62
|
const name = formatToolName(baseName, serverName, effectivePrefix);
|
|
68
|
-
if (seenNames.has(name)) {
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
seenNames.add(name);
|
|
72
63
|
|
|
73
64
|
metadata.push({
|
|
74
65
|
name,
|
|
75
66
|
originalName: baseName,
|
|
76
67
|
description: resource.description ?? `Read resource: ${resource.uri}`,
|
|
77
68
|
resourceUri: resource.uri,
|
|
69
|
+
resourceDescriptor: resource,
|
|
78
70
|
});
|
|
79
71
|
}
|
|
80
72
|
}
|
|
@@ -85,17 +77,30 @@ export function buildToolMetadata(
|
|
|
85
77
|
export function totalToolCount(state: McpExtensionState): number {
|
|
86
78
|
let count = 0;
|
|
87
79
|
for (const metadata of state.toolMetadata.values()) {
|
|
88
|
-
count += metadata.length;
|
|
80
|
+
count += metadata.filter(tool => tool.resourceUri === undefined).length;
|
|
89
81
|
}
|
|
90
82
|
return count;
|
|
91
83
|
}
|
|
92
84
|
|
|
85
|
+
export function toToolDescriptor(server: string, tool: ToolMetadata) {
|
|
86
|
+
const { name: path, originalName: name, resourceUri: _resourceUri, resourceDescriptor: _resourceDescriptor, uiResourceUri: _uiResourceUri, uiVisibility: _uiVisibility, uiStreamMode: _uiStreamMode, ...descriptor } = tool;
|
|
87
|
+
return { ...descriptor, server, path, name };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function catalogCoverage(state: McpExtensionState, server?: string) {
|
|
91
|
+
const servers = Object.keys(state.config.mcpServers).filter(name => (!server || name === server) && !isServerDisabled(state.config.mcpServers[name]));
|
|
92
|
+
const knownServers = servers.filter(name => state.toolMetadata.has(name));
|
|
93
|
+
const unknownServers = servers.filter(name => !state.toolMetadata.has(name));
|
|
94
|
+
return { complete: unknownServers.length === 0, knownServers, unknownServers };
|
|
95
|
+
}
|
|
96
|
+
|
|
93
97
|
export function findToolByName(metadata: ToolMetadata[] | undefined, toolName: string): ToolMetadata | undefined {
|
|
94
98
|
if (!metadata) return undefined;
|
|
95
|
-
const exact = metadata.
|
|
96
|
-
if (exact) return exact;
|
|
99
|
+
const exact = metadata.filter(m => m.name === toolName);
|
|
100
|
+
if (exact.length > 0) return exact.length === 1 ? exact[0] : undefined;
|
|
97
101
|
const normalized = toolName.replace(/-/g, "_");
|
|
98
|
-
|
|
102
|
+
const matches = metadata.filter(m => m.name.replace(/-/g, "_") === normalized);
|
|
103
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
export function formatSchema(schema: unknown, indent = " "): string {
|
package/tool-registrar.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
//
|
|
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.
|
|
4
2
|
|
|
5
3
|
import type { McpContent, ContentBlock } from "./types.ts";
|
|
4
|
+
import { formatMcpPayloadFile, type McpPayloadFile } from "./mcp-output-guard.ts";
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Transform MCP content types to Pi content blocks.
|
|
@@ -21,7 +20,7 @@ export function transformMcpContent(content: McpContent[]): ContentBlock[] {
|
|
|
21
20
|
}
|
|
22
21
|
if (c.type === "resource") {
|
|
23
22
|
const resourceUri = c.resource?.uri ?? "(no URI)";
|
|
24
|
-
const resourceContent = c.resource?.text ?? (c.resource ?
|
|
23
|
+
const resourceContent = c.resource?.text ?? (c.resource?.blob !== undefined ? "[Binary content; not rendered]" : "(no content)");
|
|
25
24
|
return {
|
|
26
25
|
type: "text" as const,
|
|
27
26
|
text: `[Resource: ${resourceUri}]\n${resourceContent}`,
|
|
@@ -38,26 +37,32 @@ export function transformMcpContent(content: McpContent[]): ContentBlock[] {
|
|
|
38
37
|
if (c.type === "audio") {
|
|
39
38
|
return {
|
|
40
39
|
type: "text" as const,
|
|
41
|
-
text: `[Audio content: ${c.mimeType ?? "audio/*"}]`,
|
|
40
|
+
text: `[Audio content: ${c.mimeType ?? "audio/*"}; not rendered]`,
|
|
42
41
|
};
|
|
43
42
|
}
|
|
44
43
|
return { type: "text" as const, text: JSON.stringify(c) };
|
|
45
44
|
});
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
/**
|
|
49
|
-
* Resolve a tool result's content blocks, falling back to structuredContent
|
|
50
|
-
* when content is empty.
|
|
51
|
-
*/
|
|
47
|
+
/** Human content and structured content are complementary protocol fields. */
|
|
52
48
|
export function resolveMcpResultContent(result: Record<string, unknown>): ContentBlock[] {
|
|
53
49
|
const blocks = transformMcpContent((Array.isArray(result.content) ? result.content : []) as McpContent[]);
|
|
54
|
-
if (blocks.length > 0) return blocks;
|
|
55
|
-
|
|
56
50
|
if (result.structuredContent !== undefined) {
|
|
57
|
-
|
|
51
|
+
blocks.push({ type: "text", text: stringifyStructuredContent(result.structuredContent) });
|
|
58
52
|
}
|
|
53
|
+
return blocks;
|
|
54
|
+
}
|
|
59
55
|
|
|
60
|
-
|
|
56
|
+
/** Persist content the model cannot consume; never imply an audio/blob payload was rendered. */
|
|
57
|
+
export function renderMcpResultContent(result: Record<string, unknown>, payloadFiles: McpPayloadFile[] = []): ContentBlock[] {
|
|
58
|
+
const content: McpContent[] = Array.isArray(result.contents)
|
|
59
|
+
? result.contents.map(resource => typeof resource.text === "string" ? { type: "text", text: resource.text } : { type: "resource", resource })
|
|
60
|
+
: Array.isArray(result.content) ? result.content : [];
|
|
61
|
+
const rendered = content.map((block, index) => {
|
|
62
|
+
const file = payloadFiles.find(file => file.index === index);
|
|
63
|
+
return file ? { type: "text" as const, text: formatMcpPayloadFile(file) } : block;
|
|
64
|
+
});
|
|
65
|
+
return resolveMcpResultContent({ ...result, content: rendered });
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
function stringifyStructuredContent(value: unknown): string {
|
package/tool-result-renderer.ts
CHANGED
|
@@ -17,6 +17,9 @@ export interface McpProxyToolCallInput {
|
|
|
17
17
|
connect?: string;
|
|
18
18
|
describe?: string;
|
|
19
19
|
search?: string;
|
|
20
|
+
query?: string;
|
|
21
|
+
uri?: string;
|
|
22
|
+
ref?: string;
|
|
20
23
|
includeSchemas?: boolean;
|
|
21
24
|
server?: string;
|
|
22
25
|
action?: string;
|
|
@@ -112,7 +115,13 @@ export function formatMcpProxyToolCallLines(
|
|
|
112
115
|
args: McpProxyToolCallInput,
|
|
113
116
|
maxInputChars = DEFAULT_MAX_CALL_INPUT_CHARS,
|
|
114
117
|
): string[] {
|
|
115
|
-
if (args.action === "ui-messages") return [`mcp ${args.action}`];
|
|
118
|
+
if (args.action === "ui-messages" || args.action === "status") return [`mcp ${args.action}`];
|
|
119
|
+
if (args.action) {
|
|
120
|
+
const target = args.tool ?? args.query ?? args.uri ?? args.ref;
|
|
121
|
+
let line = `mcp ${args.action}${target ? ` ${target}` : ""}${args.server ? `${target ? " @" : ""} ${args.server}` : ""}`;
|
|
122
|
+
if (args.action === "search" && args.includeSchemas === false) line += " (schemas hidden)";
|
|
123
|
+
return args.action === "call" && args.args ? [line, formatJsonish(args.args, maxInputChars)] : [line];
|
|
124
|
+
}
|
|
116
125
|
|
|
117
126
|
if (args.tool) {
|
|
118
127
|
const target = args.server ? `${args.tool} @ ${args.server}` : args.tool;
|
|
@@ -132,7 +141,6 @@ export function formatMcpProxyToolCallLines(
|
|
|
132
141
|
}
|
|
133
142
|
|
|
134
143
|
if (args.server) return [`mcp list ${args.server}`];
|
|
135
|
-
if (args.action) return [`mcp ${args.action}`];
|
|
136
144
|
|
|
137
145
|
return ["mcp status"];
|
|
138
146
|
}
|
package/types.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface McpServerStatusSnapshot {
|
|
|
30
30
|
readonly name: string;
|
|
31
31
|
readonly status: McpServerRuntimeStatus;
|
|
32
32
|
readonly toolCount: number;
|
|
33
|
+
/** False means the server has not been discovered, rather than an empty inventory. */
|
|
34
|
+
readonly catalogKnown?: boolean;
|
|
33
35
|
readonly resourceCount?: number;
|
|
34
36
|
readonly failedAgoSeconds?: number;
|
|
35
37
|
readonly disabled: boolean;
|
|
@@ -59,24 +61,12 @@ type SdkResource = ListResourcesResult["resources"][number];
|
|
|
59
61
|
type SdkPrompt = ListPromptsResult["prompts"][number];
|
|
60
62
|
type SdkPromptArgument = NonNullable<SdkPrompt["arguments"]>[number];
|
|
61
63
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
name: SdkTool["name"];
|
|
66
|
-
title?: SdkTool["title"];
|
|
67
|
-
description?: SdkTool["description"];
|
|
68
|
-
inputSchema?: SdkTool["inputSchema"]; // JSON Schema
|
|
69
|
-
annotations?: SdkTool["annotations"];
|
|
70
|
-
_meta?: SdkTool["_meta"];
|
|
64
|
+
// Legacy cache records may omit inputSchema; otherwise retain the native descriptor.
|
|
65
|
+
export interface McpTool extends Omit<SdkTool, "inputSchema"> {
|
|
66
|
+
inputSchema?: SdkTool["inputSchema"];
|
|
71
67
|
}
|
|
72
68
|
|
|
73
|
-
export
|
|
74
|
-
uri: SdkResource["uri"];
|
|
75
|
-
name: SdkResource["name"];
|
|
76
|
-
description?: SdkResource["description"];
|
|
77
|
-
mimeType?: SdkResource["mimeType"];
|
|
78
|
-
_meta?: SdkResource["_meta"];
|
|
79
|
-
}
|
|
69
|
+
export type McpResource = SdkResource;
|
|
80
70
|
|
|
81
71
|
export interface McpPromptArgument {
|
|
82
72
|
name: SdkPromptArgument["name"];
|
|
@@ -509,7 +499,7 @@ export interface McpSettings {
|
|
|
509
499
|
/** Freeze direct-tool registration after the initial sync. Automatic metadata updates
|
|
510
500
|
* (reconnects, lazy-connect, tool-list-changed) won't rebuild the system prompt,
|
|
511
501
|
* preserving the prompt-cache prefix. The agent rediscovers explicitly via
|
|
512
|
-
* mcp({ connect: "server" }). Default: false. */
|
|
502
|
+
* mcp({ action: "connect", server: "server" }). Default: false. */
|
|
513
503
|
freezeDirectTools?: boolean;
|
|
514
504
|
autoAuth?: boolean;
|
|
515
505
|
sampling?: boolean;
|
|
@@ -596,15 +586,14 @@ export interface McpAdapterOptions {
|
|
|
596
586
|
// Alias for clarity
|
|
597
587
|
export type ServerDefinition = ServerEntry;
|
|
598
588
|
|
|
599
|
-
export interface ToolMetadata {
|
|
589
|
+
export interface ToolMetadata extends Omit<McpTool, "name" | "description"> {
|
|
600
590
|
name: string; // Prefixed tool name (e.g., "xcodebuild_list_sims")
|
|
601
591
|
originalName: string; // Original MCP tool name (e.g., "list_sims")
|
|
602
592
|
description: string;
|
|
603
|
-
resourceUri?: string; //
|
|
593
|
+
resourceUri?: string; // Legacy resource alias; new calls address server + URI.
|
|
594
|
+
resourceDescriptor?: McpResource;
|
|
604
595
|
uiResourceUri?: string; // For app-enabled tools: the UI resource URI
|
|
605
596
|
uiVisibility?: UiToolVisibility[];
|
|
606
|
-
inputSchema?: unknown; // JSON Schema for parameters (stored for describe/errors)
|
|
607
|
-
annotations?: McpTool["annotations"];
|
|
608
597
|
uiStreamMode?: UiStreamMode;
|
|
609
598
|
}
|
|
610
599
|
|
|
@@ -617,16 +606,9 @@ export interface PromptMetadata {
|
|
|
617
606
|
arguments: McpPromptArgument[];
|
|
618
607
|
}
|
|
619
608
|
|
|
620
|
-
export interface DirectToolSpec {
|
|
609
|
+
export interface DirectToolSpec extends Omit<ToolMetadata, "name"> {
|
|
621
610
|
serverName: string;
|
|
622
|
-
originalName: string;
|
|
623
611
|
prefixedName: string;
|
|
624
|
-
description: string;
|
|
625
|
-
inputSchema?: unknown;
|
|
626
|
-
annotations?: McpTool["annotations"];
|
|
627
|
-
resourceUri?: string;
|
|
628
|
-
uiResourceUri?: string;
|
|
629
|
-
uiStreamMode?: UiStreamMode;
|
|
630
612
|
}
|
|
631
613
|
|
|
632
614
|
export interface ServerProvenance {
|
|
@@ -640,21 +622,14 @@ export interface McpAuthResult {
|
|
|
640
622
|
message?: string;
|
|
641
623
|
}
|
|
642
624
|
|
|
643
|
-
export interface CachedTool {
|
|
644
|
-
|
|
645
|
-
description?: string;
|
|
646
|
-
inputSchema?: unknown;
|
|
647
|
-
annotations?: McpTool["annotations"];
|
|
625
|
+
export interface CachedTool extends McpTool {
|
|
626
|
+
// Version-1 caches stored flattened UI fields instead of the raw _meta.
|
|
648
627
|
uiResourceUri?: string;
|
|
649
628
|
uiVisibility?: UiToolVisibility[];
|
|
650
|
-
uiStreamMode?:
|
|
629
|
+
uiStreamMode?: UiStreamMode;
|
|
651
630
|
}
|
|
652
631
|
|
|
653
|
-
export
|
|
654
|
-
uri: string;
|
|
655
|
-
name: string;
|
|
656
|
-
description?: string;
|
|
657
|
-
}
|
|
632
|
+
export type CachedResource = McpResource;
|
|
658
633
|
|
|
659
634
|
export interface CachedPrompt {
|
|
660
635
|
name: string;
|
package/utils.ts
CHANGED
|
@@ -274,11 +274,9 @@ export function truncateAtWord(text: string, target: number): string {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
export function normalizeDirectToolInputSchema(schema: unknown): Record<string, unknown> {
|
|
277
|
-
|
|
278
|
-
? schema as Record<string, unknown>
|
|
277
|
+
return schema && typeof schema === "object" && !Array.isArray(schema)
|
|
278
|
+
? structuredClone(schema) as Record<string, unknown>
|
|
279
279
|
: { type: "object", properties: {} };
|
|
280
|
-
const { $schema, additionalProperties, ...normalized } = inputSchema;
|
|
281
|
-
return normalized;
|
|
282
280
|
}
|
|
283
281
|
|
|
284
282
|
export function formatAuthRequiredMessage(
|
package/dist/ts-shape.d.ts
DELETED
package/dist/ts-shape.js
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
const UNSUPPORTED_KEYWORDS = ["if", "then", "else", "allOf", "not", "patternProperties", "additionalProperties"];
|
|
2
|
-
/** Renders the useful JSON Schema subset as TypeScript, or null for unsupported schemas. */
|
|
3
|
-
export function renderTsShape(inputSchema) {
|
|
4
|
-
try {
|
|
5
|
-
if (!isSchema(inputSchema))
|
|
6
|
-
return null;
|
|
7
|
-
const definitions = new Map();
|
|
8
|
-
for (const key of ["$defs", "definitions"]) {
|
|
9
|
-
const rawDefinitions = inputSchema[key];
|
|
10
|
-
if (rawDefinitions === undefined)
|
|
11
|
-
continue;
|
|
12
|
-
if (!isSchema(rawDefinitions))
|
|
13
|
-
return null;
|
|
14
|
-
for (const [name, definition] of Object.entries(rawDefinitions)) {
|
|
15
|
-
if (!isSchema(definition))
|
|
16
|
-
return null;
|
|
17
|
-
definitions.set(`${key}/${decodePointerToken(name)}`, definition);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
const aliases = new Map();
|
|
21
|
-
const usedAliases = new Set();
|
|
22
|
-
let aliasIndex = 0;
|
|
23
|
-
const aliasFor = (definitionKey) => {
|
|
24
|
-
let alias = aliases.get(definitionKey);
|
|
25
|
-
if (alias)
|
|
26
|
-
return alias;
|
|
27
|
-
const name = definitionKey.slice(definitionKey.indexOf("/") + 1);
|
|
28
|
-
alias = /^[A-Za-z_$][\w$]*$/.test(name) && !usedAliases.has(name) ? name : `Definition${++aliasIndex}`;
|
|
29
|
-
while (usedAliases.has(alias))
|
|
30
|
-
alias = `Definition${++aliasIndex}`;
|
|
31
|
-
aliases.set(definitionKey, alias);
|
|
32
|
-
usedAliases.add(alias);
|
|
33
|
-
return alias;
|
|
34
|
-
};
|
|
35
|
-
const render = (schema) => {
|
|
36
|
-
if (!isSchema(schema) || hasUnsupportedKeyword(schema))
|
|
37
|
-
return null;
|
|
38
|
-
if ("$ref" in schema) {
|
|
39
|
-
if (typeof schema.$ref !== "string")
|
|
40
|
-
return null;
|
|
41
|
-
const match = schema.$ref.match(/^#\/(\$defs|definitions)\/([^/]+)$/);
|
|
42
|
-
if (!match)
|
|
43
|
-
return null;
|
|
44
|
-
const definitionGroup = match[1];
|
|
45
|
-
const definitionName = match[2];
|
|
46
|
-
if (definitionGroup === undefined || definitionName === undefined)
|
|
47
|
-
return null;
|
|
48
|
-
const definitionKey = `${definitionGroup}/${decodePointerToken(definitionName)}`;
|
|
49
|
-
if (!definitions.has(definitionKey))
|
|
50
|
-
return null;
|
|
51
|
-
return aliasFor(definitionKey);
|
|
52
|
-
}
|
|
53
|
-
if (Array.isArray(schema.enum)) {
|
|
54
|
-
const values = schema.enum.map(renderLiteral);
|
|
55
|
-
return values.every((value) => value !== null) ? values.join(" | ") : null;
|
|
56
|
-
}
|
|
57
|
-
if (Object.hasOwn(schema, "const"))
|
|
58
|
-
return renderLiteral(schema.const);
|
|
59
|
-
if (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) {
|
|
60
|
-
const variants = (schema.anyOf ?? schema.oneOf);
|
|
61
|
-
if (variants.length === 0)
|
|
62
|
-
return null;
|
|
63
|
-
const rendered = variants.map(render);
|
|
64
|
-
return rendered.every((value) => value !== null) ? rendered.join(" | ") : null;
|
|
65
|
-
}
|
|
66
|
-
if (schema.type === "object" || schema.properties !== undefined) {
|
|
67
|
-
if (schema.properties === undefined)
|
|
68
|
-
return "{}";
|
|
69
|
-
if (!isSchema(schema.properties))
|
|
70
|
-
return null;
|
|
71
|
-
const required = new Set(Array.isArray(schema.required)
|
|
72
|
-
? schema.required.filter((name) => typeof name === "string")
|
|
73
|
-
: []);
|
|
74
|
-
const properties = [];
|
|
75
|
-
for (const [name, property] of Object.entries(schema.properties)) {
|
|
76
|
-
const rendered = render(property);
|
|
77
|
-
if (rendered === null)
|
|
78
|
-
return null;
|
|
79
|
-
properties.push(`${formatPropertyName(name)}${required.has(name) ? "" : "?"}: ${rendered};`);
|
|
80
|
-
}
|
|
81
|
-
return properties.length === 0 ? "{}" : `{ ${properties.join(" ")} }`;
|
|
82
|
-
}
|
|
83
|
-
if (schema.type === "array") {
|
|
84
|
-
if (schema.items === undefined)
|
|
85
|
-
return "unknown[]";
|
|
86
|
-
const item = render(schema.items);
|
|
87
|
-
return item === null ? null : `${needsParentheses(item) ? `(${item})` : item}[]`;
|
|
88
|
-
}
|
|
89
|
-
if (Array.isArray(schema.type)) {
|
|
90
|
-
const types = schema.type.map(renderType);
|
|
91
|
-
return types.every((type) => type !== null) ? types.join(" | ") : null;
|
|
92
|
-
}
|
|
93
|
-
if (typeof schema.type === "string")
|
|
94
|
-
return renderType(schema.type);
|
|
95
|
-
return "unknown";
|
|
96
|
-
};
|
|
97
|
-
const root = render(inputSchema);
|
|
98
|
-
if (root === null)
|
|
99
|
-
return null;
|
|
100
|
-
const definitionsText = [];
|
|
101
|
-
for (const [key, alias] of aliases) {
|
|
102
|
-
const definition = definitions.get(key);
|
|
103
|
-
if (!definition)
|
|
104
|
-
return null;
|
|
105
|
-
const rendered = render(definition);
|
|
106
|
-
if (rendered === null)
|
|
107
|
-
return null;
|
|
108
|
-
definitionsText.push(`type ${alias} = ${rendered};`);
|
|
109
|
-
}
|
|
110
|
-
return definitionsText.length > 0 ? `${definitionsText.join("\n")}\n\n${root}` : root;
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
function isSchema(value) {
|
|
117
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
118
|
-
}
|
|
119
|
-
function hasUnsupportedKeyword(schema) {
|
|
120
|
-
return UNSUPPORTED_KEYWORDS.some(keyword => Object.hasOwn(schema, keyword));
|
|
121
|
-
}
|
|
122
|
-
function decodePointerToken(token) {
|
|
123
|
-
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
124
|
-
}
|
|
125
|
-
function renderType(type) {
|
|
126
|
-
switch (type) {
|
|
127
|
-
case "string": return "string";
|
|
128
|
-
case "number":
|
|
129
|
-
case "integer": return "number";
|
|
130
|
-
case "boolean": return "boolean";
|
|
131
|
-
case "null": return "null";
|
|
132
|
-
case "object": return "{}";
|
|
133
|
-
case "array": return "unknown[]";
|
|
134
|
-
default: return null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
function renderLiteral(value) {
|
|
138
|
-
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
139
|
-
return JSON.stringify(value);
|
|
140
|
-
return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
|
|
141
|
-
}
|
|
142
|
-
function formatPropertyName(name) {
|
|
143
|
-
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
|
|
144
|
-
}
|
|
145
|
-
function needsParentheses(type) {
|
|
146
|
-
return type.includes(" | ");
|
|
147
|
-
}
|