pi-mcp-adapter 1.5.0 → 2.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/resource-tools.ts CHANGED
@@ -1,32 +1,4 @@
1
- // resource-tools.ts - MCP resource tool name collection
2
- // NOTE: Resources are NOT registered as Pi tools - they're called via the `mcp` proxy.
3
-
4
- import { formatToolName, type McpResource } from "./types.js";
5
-
6
- interface ResourceCollectionOptions {
7
- serverName: string;
8
- prefix: "server" | "none" | "short";
9
- }
10
-
11
- /**
12
- * Collect tool names for MCP resources.
13
- * Does NOT register with Pi - resources are called via the `mcp` proxy.
14
- */
15
- export function collectResourceToolNames(
16
- resources: McpResource[],
17
- options: ResourceCollectionOptions
18
- ): string[] {
19
- const collected: string[] = [];
20
- const { serverName, prefix } = options;
21
-
22
- for (const resource of resources) {
23
- const baseName = `get_${resourceNameToToolName(resource.name)}`;
24
- const toolName = formatToolName(baseName, serverName, prefix);
25
- collected.push(toolName);
26
- }
27
-
28
- return collected;
29
- }
1
+ // resource-tools.ts - MCP resource name utilities
30
2
 
31
3
  export function resourceNameToToolName(name: string): string {
32
4
  let result = name
package/server-manager.ts CHANGED
@@ -5,6 +5,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
5
5
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
6
6
  import type { McpTool, McpResource, ServerDefinition, Transport } from "./types.js";
7
7
  import { getStoredTokens } from "./oauth-handler.js";
8
+ import { resolveNpxBinary } from "./npx-resolver.js";
8
9
 
9
10
  interface ServerConnection {
10
11
  client: Client;
@@ -13,6 +14,7 @@ interface ServerConnection {
13
14
  tools: McpTool[];
14
15
  resources: McpResource[];
15
16
  lastUsedAt: number;
17
+ inFlight: number;
16
18
  status: "connected" | "closed";
17
19
  }
18
20
 
@@ -54,10 +56,21 @@ export class McpServerManager {
54
56
  let transport: Transport;
55
57
 
56
58
  if (definition.command) {
57
- // Stdio transport
59
+ let command = definition.command;
60
+ let args = definition.args ?? [];
61
+
62
+ if (command === "npx" || command === "npm") {
63
+ const resolved = await resolveNpxBinary(command, args);
64
+ if (resolved) {
65
+ command = resolved.isJs ? "node" : resolved.binPath;
66
+ args = resolved.isJs ? [resolved.binPath, ...resolved.extraArgs] : resolved.extraArgs;
67
+ console.log(`MCP: ${name} resolved to ${resolved.binPath} (skipping npm parent)`);
68
+ }
69
+ }
70
+
58
71
  transport = new StdioClientTransport({
59
- command: definition.command,
60
- args: definition.args ?? [],
72
+ command,
73
+ args,
61
74
  env: resolveEnv(definition.env),
62
75
  cwd: definition.cwd,
63
76
  stderr: definition.debug ? "inherit" : "ignore",
@@ -85,6 +98,7 @@ export class McpServerManager {
85
98
  tools,
86
99
  resources,
87
100
  lastUsedAt: Date.now(),
101
+ inFlight: 0,
88
102
  status: "connected",
89
103
  };
90
104
  } catch (error) {
@@ -181,11 +195,13 @@ export class McpServerManager {
181
195
  const connection = this.connections.get(name);
182
196
  if (!connection) return;
183
197
 
198
+ // Delete from map BEFORE async cleanup to prevent a race where a
199
+ // concurrent connect() creates a new connection that our deferred
200
+ // delete() would then remove, orphaning the new server process.
184
201
  connection.status = "closed";
185
- // Close both independently - don't let one failure prevent the other
202
+ this.connections.delete(name);
186
203
  await connection.client.close().catch(() => {});
187
204
  await connection.transport.close().catch(() => {});
188
- this.connections.delete(name);
189
205
  }
190
206
 
191
207
  async closeAll(): Promise<void> {
@@ -200,6 +216,34 @@ export class McpServerManager {
200
216
  getAllConnections(): Map<string, ServerConnection> {
201
217
  return new Map(this.connections);
202
218
  }
219
+
220
+ touch(name: string): void {
221
+ const connection = this.connections.get(name);
222
+ if (connection) {
223
+ connection.lastUsedAt = Date.now();
224
+ }
225
+ }
226
+
227
+ incrementInFlight(name: string): void {
228
+ const connection = this.connections.get(name);
229
+ if (connection) {
230
+ connection.inFlight = (connection.inFlight ?? 0) + 1;
231
+ }
232
+ }
233
+
234
+ decrementInFlight(name: string): void {
235
+ const connection = this.connections.get(name);
236
+ if (connection && connection.inFlight) {
237
+ connection.inFlight--;
238
+ }
239
+ }
240
+
241
+ isIdle(name: string, timeoutMs: number): boolean {
242
+ const connection = this.connections.get(name);
243
+ if (!connection || connection.status !== "connected") return false;
244
+ if (connection.inFlight && connection.inFlight > 0) return false;
245
+ return (Date.now() - connection.lastUsedAt) > timeoutMs;
246
+ }
203
247
  }
204
248
 
205
249
  /**
package/tool-registrar.ts CHANGED
@@ -1,39 +1,8 @@
1
- // tool-registrar.ts - MCP tool metadata and content transformation
1
+ // tool-registrar.ts - MCP content transformation
2
2
  // NOTE: Tools are NOT registered with Pi - only the unified `mcp` proxy tool is registered.
3
3
  // This keeps the LLM context small (1 tool instead of 100s).
4
4
 
5
- import type { McpTool, McpContent, ContentBlock } from "./types.js";
6
- import { formatToolName } from "./types.js";
7
-
8
- interface ToolCollectionOptions {
9
- serverName: string;
10
- prefix: "server" | "none" | "short";
11
- }
12
-
13
- /**
14
- * Collect tool names from MCP server tools.
15
- * Does NOT register with Pi - tools are called via the `mcp` proxy.
16
- */
17
- export function collectToolNames(
18
- tools: McpTool[],
19
- options: ToolCollectionOptions
20
- ): { collected: string[]; failed: string[] } {
21
- const collected: string[] = [];
22
- const failed: string[] = [];
23
-
24
- for (const tool of tools) {
25
- // Basic validation - tool must have a name
26
- if (!tool.name) {
27
- failed.push("(unnamed)");
28
- continue;
29
- }
30
-
31
- const toolName = formatToolName(tool.name, options.serverName, options.prefix);
32
- collected.push(toolName);
33
- }
34
-
35
- return { collected, failed };
36
- }
5
+ import type { McpContent, ContentBlock } from "./types.js";
37
6
 
38
7
  /**
39
8
  * Transform MCP content types to Pi content blocks.
package/types.ts CHANGED
@@ -66,7 +66,8 @@ export interface ServerEntry {
66
66
  auth?: "oauth" | "bearer";
67
67
  bearerToken?: string;
68
68
  bearerTokenEnv?: string;
69
- lifecycle?: "keep-alive" | "ephemeral";
69
+ lifecycle?: "keep-alive" | "lazy" | "eager";
70
+ idleTimeout?: number; // minutes, overrides global setting
70
71
  // Resource handling
71
72
  exposeResources?: boolean;
72
73
  // Debug
@@ -76,6 +77,7 @@ export interface ServerEntry {
76
77
  // Settings
77
78
  export interface McpSettings {
78
79
  toolPrefix?: "server" | "none" | "short";
80
+ idleTimeout?: number; // minutes, default 10, 0 to disable
79
81
  }
80
82
 
81
83
  // Root config
@@ -88,6 +90,30 @@ export interface McpConfig {
88
90
  // Alias for clarity
89
91
  export type ServerDefinition = ServerEntry;
90
92
 
93
+ export interface ToolMetadata {
94
+ name: string; // Prefixed tool name (e.g., "xcodebuild_list_sims")
95
+ originalName: string; // Original MCP tool name (e.g., "list_sims")
96
+ description: string;
97
+ resourceUri?: string; // For resource tools: the URI to read
98
+ inputSchema?: unknown; // JSON Schema for parameters (stored for describe/errors)
99
+ }
100
+
101
+ /**
102
+ * Get server prefix based on tool prefix mode.
103
+ */
104
+ export function getServerPrefix(
105
+ serverName: string,
106
+ mode: "server" | "none" | "short"
107
+ ): string {
108
+ if (mode === "none") return "";
109
+ if (mode === "short") {
110
+ let short = serverName.replace(/-?mcp$/i, "").replace(/-/g, "_");
111
+ if (!short) short = "mcp";
112
+ return short;
113
+ }
114
+ return serverName.replace(/-/g, "_");
115
+ }
116
+
91
117
  /**
92
118
  * Format a tool name with server prefix.
93
119
  */
@@ -96,17 +122,6 @@ export function formatToolName(
96
122
  serverName: string,
97
123
  prefix: "server" | "none" | "short"
98
124
  ): string {
99
- switch (prefix) {
100
- case "none":
101
- return toolName;
102
- case "short":
103
- let short = serverName.replace(/-?mcp$/i, "").replace(/-/g, "_");
104
- // Fallback if server name was just "mcp" or similar
105
- if (!short) short = "mcp";
106
- return `${short}_${toolName}`;
107
- case "server":
108
- default:
109
- const normalized = serverName.replace(/-/g, "_");
110
- return `${normalized}_${toolName}`;
111
- }
125
+ const p = getServerPrefix(serverName, prefix);
126
+ return p ? `${p}_${toolName}` : toolName;
112
127
  }