@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
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
- const seenNames = new Set<string>();
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.find(m => m.name === toolName);
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
- return metadata.find(m => m.name.replace(/-/g, "_") === normalized);
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
- // 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.
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 ? JSON.stringify(c.resource) : "(no content)");
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
- return [{ type: "text" as const, text: stringifyStructuredContent(result.structuredContent) }];
51
+ blocks.push({ type: "text", text: stringifyStructuredContent(result.structuredContent) });
58
52
  }
53
+ return blocks;
54
+ }
59
55
 
60
- return [];
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 {
@@ -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
- // MCP wire definitions derive their field types from the installed SDK while
63
- // retaining the adapter's deliberately smaller public surface.
64
- export interface McpTool {
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 interface McpResource {
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; // For resource tools: the URI to read
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
- name: string;
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?: "eager" | "stream-first";
629
+ uiStreamMode?: UiStreamMode;
651
630
  }
652
631
 
653
- export interface CachedResource {
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
- const inputSchema = schema && typeof schema === "object" && !Array.isArray(schema)
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(
@@ -1,2 +0,0 @@
1
- /** Renders the useful JSON Schema subset as TypeScript, or null for unsupported schemas. */
2
- export declare function renderTsShape(inputSchema: unknown): string | null;
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
- }