pi-mcp-adapter 1.5.1 → 2.0.1
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/ARCHITECTURE.md +91 -33
- package/CHANGELOG.md +38 -0
- package/README.md +60 -16
- package/cli.js +2 -0
- package/config.ts +1 -4
- package/index.ts +602 -256
- package/lifecycle.ts +34 -0
- package/metadata-cache.ts +175 -0
- package/npx-resolver.ts +419 -0
- package/package.json +3 -1
- package/resource-tools.ts +1 -29
- package/server-manager.ts +49 -5
- package/tool-registrar.ts +2 -33
- package/types.ts +29 -14
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
|
-
|
|
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
|
|
60
|
-
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
|
-
|
|
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
|
|
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 {
|
|
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" | "
|
|
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
|
-
|
|
100
|
-
|
|
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
|
}
|