pi-mcp-adapter 1.2.2 → 1.4.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 +16 -0
- package/README.md +0 -1
- package/index.ts +154 -81
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.4.0] - 2026-01-19
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Non-blocking startup** - Pi starts immediately, MCP servers connect in background. First MCP call waits only if init isn't done yet.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Tool metadata now includes `inputSchema` after `/mcp reconnect` (was missing, breaking describe and error hints)
|
|
17
|
+
|
|
18
|
+
## [1.3.0] - 2026-01-19
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **Parallel server connections** - All MCP servers now connect in parallel on startup instead of sequentially, significantly faster with many servers
|
|
23
|
+
|
|
8
24
|
## [1.2.2] - 2026-01-19
|
|
9
25
|
|
|
10
26
|
### Fixed
|
package/README.md
CHANGED
package/index.ts
CHANGED
|
@@ -24,6 +24,27 @@ interface McpExtensionState {
|
|
|
24
24
|
config: McpConfig;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** Run async tasks with concurrency limit */
|
|
28
|
+
async function parallelLimit<T, R>(
|
|
29
|
+
items: T[],
|
|
30
|
+
limit: number,
|
|
31
|
+
fn: (item: T) => Promise<R>
|
|
32
|
+
): Promise<R[]> {
|
|
33
|
+
const results: R[] = [];
|
|
34
|
+
let index = 0;
|
|
35
|
+
|
|
36
|
+
async function worker() {
|
|
37
|
+
while (index < items.length) {
|
|
38
|
+
const i = index++;
|
|
39
|
+
results[i] = await fn(items[i]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const workers = Array(Math.min(limit, items.length)).fill(null).map(() => worker());
|
|
44
|
+
await Promise.all(workers);
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
export default function mcpAdapter(pi: ExtensionAPI) {
|
|
28
49
|
let state: McpExtensionState | null = null;
|
|
29
50
|
let initPromise: Promise<McpExtensionState> | null = null;
|
|
@@ -34,25 +55,33 @@ export default function mcpAdapter(pi: ExtensionAPI) {
|
|
|
34
55
|
});
|
|
35
56
|
|
|
36
57
|
pi.on("session_start", async (_event, ctx) => {
|
|
58
|
+
// Non-blocking init - Pi starts immediately, MCP connects in background
|
|
37
59
|
initPromise = initializeMcp(pi, ctx);
|
|
38
|
-
state = await initPromise;
|
|
39
|
-
initPromise = null;
|
|
40
60
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
61
|
+
initPromise.then(s => {
|
|
62
|
+
state = s;
|
|
63
|
+
initPromise = null;
|
|
64
|
+
|
|
65
|
+
// Set up callback for auto-reconnect to update metadata
|
|
66
|
+
s.lifecycle.setReconnectCallback((serverName) => {
|
|
67
|
+
if (state) {
|
|
68
|
+
updateServerMetadata(state, serverName);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Update status bar when ready
|
|
73
|
+
if (ctx.hasUI) {
|
|
74
|
+
const totalTools = [...s.registeredTools.values()].flat().length;
|
|
75
|
+
if (totalTools > 0) {
|
|
76
|
+
ctx.ui.setStatus("mcp", ctx.ui.theme.fg("accent", `MCP: ${totalTools} tools`));
|
|
77
|
+
} else {
|
|
78
|
+
ctx.ui.setStatus("mcp", "");
|
|
79
|
+
}
|
|
45
80
|
}
|
|
81
|
+
}).catch(err => {
|
|
82
|
+
console.error("MCP initialization failed:", err);
|
|
83
|
+
initPromise = null;
|
|
46
84
|
});
|
|
47
|
-
|
|
48
|
-
if (ctx.hasUI) {
|
|
49
|
-
const totalTools = [...state.registeredTools.values()].flat().length;
|
|
50
|
-
if (totalTools > 0) {
|
|
51
|
-
ctx.ui.setStatus("mcp", ctx.ui.theme.fg("accent", `MCP: ${totalTools} tools`));
|
|
52
|
-
} else {
|
|
53
|
-
ctx.ui.setStatus("mcp", "");
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
85
|
});
|
|
57
86
|
|
|
58
87
|
pi.on("session_shutdown", async () => {
|
|
@@ -74,6 +103,15 @@ export default function mcpAdapter(pi: ExtensionAPI) {
|
|
|
74
103
|
pi.registerCommand("mcp", {
|
|
75
104
|
description: "Show MCP server status",
|
|
76
105
|
handler: async (args, ctx) => {
|
|
106
|
+
// Wait for init if still in progress
|
|
107
|
+
if (!state && initPromise) {
|
|
108
|
+
try {
|
|
109
|
+
state = await initPromise;
|
|
110
|
+
} catch {
|
|
111
|
+
if (ctx.hasUI) ctx.ui.notify("MCP initialization failed", "error");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
77
115
|
if (!state) {
|
|
78
116
|
if (ctx.hasUI) ctx.ui.notify("MCP not initialized", "error");
|
|
79
117
|
return;
|
|
@@ -107,6 +145,15 @@ export default function mcpAdapter(pi: ExtensionAPI) {
|
|
|
107
145
|
return;
|
|
108
146
|
}
|
|
109
147
|
|
|
148
|
+
// Wait for init if still in progress
|
|
149
|
+
if (!state && initPromise) {
|
|
150
|
+
try {
|
|
151
|
+
state = await initPromise;
|
|
152
|
+
} catch {
|
|
153
|
+
if (ctx.hasUI) ctx.ui.notify("MCP initialization failed", "error");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
110
157
|
if (!state) {
|
|
111
158
|
if (ctx.hasUI) ctx.ui.notify("MCP not initialized", "error");
|
|
112
159
|
return;
|
|
@@ -152,6 +199,17 @@ Mode: tool (call) > describe > search > server (list) > nothing (status)`,
|
|
|
152
199
|
includeSchemas?: boolean;
|
|
153
200
|
server?: string;
|
|
154
201
|
}) {
|
|
202
|
+
// Wait for init if still in progress
|
|
203
|
+
if (!state && initPromise) {
|
|
204
|
+
try {
|
|
205
|
+
state = await initPromise;
|
|
206
|
+
} catch {
|
|
207
|
+
return {
|
|
208
|
+
content: [{ type: "text", text: "MCP initialization failed" }],
|
|
209
|
+
details: { error: "init_failed" },
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
155
213
|
if (!state) {
|
|
156
214
|
return {
|
|
157
215
|
content: [{ type: "text", text: "MCP not initialized" }],
|
|
@@ -586,80 +644,94 @@ async function initializeMcp(
|
|
|
586
644
|
return { manager, lifecycle, registeredTools, toolMetadata, config };
|
|
587
645
|
}
|
|
588
646
|
|
|
589
|
-
|
|
647
|
+
if (ctx.hasUI) {
|
|
648
|
+
ctx.ui.setStatus("mcp", `Connecting to ${serverEntries.length} servers...`);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Connect to all servers in parallel (max 10 concurrent)
|
|
652
|
+
const results = await parallelLimit(serverEntries, 10, async ([name, definition]) => {
|
|
590
653
|
try {
|
|
591
|
-
if (ctx.hasUI) {
|
|
592
|
-
ctx.ui.setStatus("mcp", `Connecting to ${name}...`);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
654
|
const connection = await manager.connect(name, definition);
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
// Collect tool names (NOT registered with Pi - only mcp proxy is registered)
|
|
599
|
-
const { collected: toolNames, failed: failedTools } = collectToolNames(
|
|
600
|
-
connection.tools,
|
|
601
|
-
{ serverName: name, prefix }
|
|
602
|
-
);
|
|
603
|
-
|
|
604
|
-
// Collect resource tool names (if enabled)
|
|
605
|
-
if (definition.exposeResources !== false && connection.resources.length > 0) {
|
|
606
|
-
const resourceToolNames = collectResourceToolNames(
|
|
607
|
-
connection.resources,
|
|
608
|
-
{ serverName: name, prefix }
|
|
609
|
-
);
|
|
610
|
-
toolNames.push(...resourceToolNames);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
registeredTools.set(name, toolNames);
|
|
614
|
-
|
|
615
|
-
// Build tool metadata for searching (include inputSchema for describe/errors)
|
|
616
|
-
const metadata: ToolMetadata[] = connection.tools.map(tool => ({
|
|
617
|
-
name: formatToolName(tool.name, name, prefix),
|
|
618
|
-
originalName: tool.name,
|
|
619
|
-
description: tool.description ?? "",
|
|
620
|
-
inputSchema: tool.inputSchema,
|
|
621
|
-
}));
|
|
622
|
-
// Add resource tools to metadata
|
|
623
|
-
for (const resource of connection.resources) {
|
|
624
|
-
if (definition.exposeResources !== false) {
|
|
625
|
-
const baseName = `get_${resourceNameToToolName(resource.name)}`;
|
|
626
|
-
metadata.push({
|
|
627
|
-
name: formatToolName(baseName, name, prefix),
|
|
628
|
-
originalName: baseName,
|
|
629
|
-
description: resource.description ?? `Read resource: ${resource.uri}`,
|
|
630
|
-
resourceUri: resource.uri,
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
toolMetadata.set(name, metadata);
|
|
635
|
-
|
|
636
|
-
// Mark keep-alive servers
|
|
637
|
-
if (definition.lifecycle === "keep-alive") {
|
|
638
|
-
lifecycle.markKeepAlive(name, definition);
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
if (failedTools.length > 0 && ctx.hasUI) {
|
|
642
|
-
ctx.ui.notify(
|
|
643
|
-
`MCP: ${name} - ${failedTools.length} tools skipped`,
|
|
644
|
-
"warning"
|
|
645
|
-
);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
if (ctx.hasUI) {
|
|
649
|
-
ctx.ui.notify(
|
|
650
|
-
`MCP: ${name} connected (${connection.tools.length} tools, ${connection.resources.length} resources)`,
|
|
651
|
-
"info"
|
|
652
|
-
);
|
|
653
|
-
}
|
|
655
|
+
return { name, definition, connection, error: null };
|
|
654
656
|
} catch (error) {
|
|
655
657
|
const message = error instanceof Error ? error.message : String(error);
|
|
658
|
+
return { name, definition, connection: null, error: message };
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
const prefix = config.settings?.toolPrefix ?? "server";
|
|
662
|
+
|
|
663
|
+
// Process results
|
|
664
|
+
for (const { name, definition, connection, error } of results) {
|
|
665
|
+
if (error || !connection) {
|
|
656
666
|
if (ctx.hasUI) {
|
|
657
|
-
ctx.ui.notify(`MCP: Failed to connect to ${name}: ${
|
|
667
|
+
ctx.ui.notify(`MCP: Failed to connect to ${name}: ${error}`, "error");
|
|
658
668
|
}
|
|
659
|
-
console.error(`MCP: Failed to connect to ${name}: ${
|
|
669
|
+
console.error(`MCP: Failed to connect to ${name}: ${error}`);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Collect tool names (NOT registered with Pi - only mcp proxy is registered)
|
|
674
|
+
const { collected: toolNames, failed: failedTools } = collectToolNames(
|
|
675
|
+
connection.tools,
|
|
676
|
+
{ serverName: name, prefix }
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
// Collect resource tool names (if enabled)
|
|
680
|
+
if (definition.exposeResources !== false && connection.resources.length > 0) {
|
|
681
|
+
const resourceToolNames = collectResourceToolNames(
|
|
682
|
+
connection.resources,
|
|
683
|
+
{ serverName: name, prefix }
|
|
684
|
+
);
|
|
685
|
+
toolNames.push(...resourceToolNames);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
registeredTools.set(name, toolNames);
|
|
689
|
+
|
|
690
|
+
// Build tool metadata for searching (include inputSchema for describe/errors)
|
|
691
|
+
const metadata: ToolMetadata[] = connection.tools.map(tool => ({
|
|
692
|
+
name: formatToolName(tool.name, name, prefix),
|
|
693
|
+
originalName: tool.name,
|
|
694
|
+
description: tool.description ?? "",
|
|
695
|
+
inputSchema: tool.inputSchema,
|
|
696
|
+
}));
|
|
697
|
+
// Add resource tools to metadata
|
|
698
|
+
for (const resource of connection.resources) {
|
|
699
|
+
if (definition.exposeResources !== false) {
|
|
700
|
+
const baseName = `get_${resourceNameToToolName(resource.name)}`;
|
|
701
|
+
metadata.push({
|
|
702
|
+
name: formatToolName(baseName, name, prefix),
|
|
703
|
+
originalName: baseName,
|
|
704
|
+
description: resource.description ?? `Read resource: ${resource.uri}`,
|
|
705
|
+
resourceUri: resource.uri,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
toolMetadata.set(name, metadata);
|
|
710
|
+
|
|
711
|
+
// Mark keep-alive servers
|
|
712
|
+
if (definition.lifecycle === "keep-alive") {
|
|
713
|
+
lifecycle.markKeepAlive(name, definition);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (failedTools.length > 0 && ctx.hasUI) {
|
|
717
|
+
ctx.ui.notify(
|
|
718
|
+
`MCP: ${name} - ${failedTools.length} tools skipped`,
|
|
719
|
+
"warning"
|
|
720
|
+
);
|
|
660
721
|
}
|
|
661
722
|
}
|
|
662
723
|
|
|
724
|
+
// Summary notification
|
|
725
|
+
const connectedCount = results.filter(r => r.connection).length;
|
|
726
|
+
const failedCount = results.filter(r => r.error).length;
|
|
727
|
+
if (ctx.hasUI && connectedCount > 0) {
|
|
728
|
+
const totalTools = [...registeredTools.values()].flat().length;
|
|
729
|
+
const msg = failedCount > 0
|
|
730
|
+
? `MCP: ${connectedCount}/${serverEntries.length} servers connected (${totalTools} tools)`
|
|
731
|
+
: `MCP: ${connectedCount} servers connected (${totalTools} tools)`;
|
|
732
|
+
ctx.ui.notify(msg, "info");
|
|
733
|
+
}
|
|
734
|
+
|
|
663
735
|
// Start health checks for keep-alive servers
|
|
664
736
|
lifecycle.startHealthChecks();
|
|
665
737
|
|
|
@@ -792,11 +864,12 @@ async function reconnectServers(
|
|
|
792
864
|
|
|
793
865
|
state.registeredTools.set(name, toolNames);
|
|
794
866
|
|
|
795
|
-
// Update tool metadata for searching
|
|
867
|
+
// Update tool metadata for searching (include inputSchema for describe/errors)
|
|
796
868
|
const metadata: ToolMetadata[] = connection.tools.map(tool => ({
|
|
797
869
|
name: formatToolName(tool.name, name, prefix),
|
|
798
870
|
originalName: tool.name,
|
|
799
871
|
description: tool.description ?? "",
|
|
872
|
+
inputSchema: tool.inputSchema,
|
|
800
873
|
}));
|
|
801
874
|
for (const resource of connection.resources) {
|
|
802
875
|
if (definition.exposeResources !== false) {
|