pi-mcp-adapter 2.2.0 → 2.2.2

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 CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.2.2] - 2026-04-03
11
+
12
+ ### Fixed
13
+ - Session lifecycle teardown now handles repeated `session_start` transitions safely and prevents stale async init results from replacing newer state.
14
+ - Shutdown now still runs `gracefulShutdown()` even if metadata cache flushing throws, avoiding leaked MCP processes.
15
+ - Proxy/direct tool init error paths now preserve and surface underlying error messages instead of returning generic failures.
16
+ - Invalid `mcp` tool `args` now fail by throwing with parse/type context instead of returning non-failing tool payloads.
17
+ - Added focused lifecycle regressions tests for stale init cleanup and init-error visibility.
18
+
19
+ ## [2.2.1] - 2026-03-23
20
+
21
+ ### Fixed
22
+ - Added `promptSnippet` to MCP proxy tool and direct MCP tools so they appear in the system prompt's Available tools section (required since pi 0.59.0)
23
+
10
24
  ## [2.2.0] - 2026-03-16
11
25
 
12
26
  ### Added
package/direct-tools.ts CHANGED
@@ -181,10 +181,11 @@ export function createDirectToolExecutor(
181
181
  if (!state && initPromise) {
182
182
  try {
183
183
  state = await initPromise;
184
- } catch {
184
+ } catch (error) {
185
+ const message = error instanceof Error ? error.message : String(error);
185
186
  return {
186
- content: [{ type: "text" as const, text: "MCP initialization failed" }],
187
- details: { error: "init_failed" },
187
+ content: [{ type: "text" as const, text: `MCP initialization failed: ${message}` }],
188
+ details: { error: "init_failed", message },
188
189
  };
189
190
  }
190
191
  }
package/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI, ExtensionContext, ToolInfo } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI, ToolInfo } from "@mariozechner/pi-coding-agent";
2
2
  import type { McpExtensionState } from "./state.js";
3
3
  import { Type } from "@sinclair/typebox";
4
4
  import { showStatus, showTools, reconnectServers, authenticateServer, openMcpPanel } from "./commands.js";
@@ -7,11 +7,42 @@ import { buildProxyDescription, createDirectToolExecutor, resolveDirectTools } f
7
7
  import { flushMetadataCache, initializeMcp, updateStatusBar } from "./init.js";
8
8
  import { loadMetadataCache } from "./metadata-cache.js";
9
9
  import { executeCall, executeConnect, executeDescribe, executeList, executeSearch, executeStatus, executeUiMessages } from "./proxy-modes.js";
10
- import { getConfigPathFromArgv } from "./utils.js";
10
+ import { getConfigPathFromArgv, truncateAtWord } from "./utils.js";
11
11
 
12
12
  export default function mcpAdapter(pi: ExtensionAPI) {
13
13
  let state: McpExtensionState | null = null;
14
14
  let initPromise: Promise<McpExtensionState> | null = null;
15
+ let lifecycleGeneration = 0;
16
+
17
+ async function shutdownState(currentState: McpExtensionState | null, reason: string): Promise<void> {
18
+ if (!currentState) return;
19
+
20
+ if (currentState.uiServer) {
21
+ currentState.uiServer.close(reason);
22
+ currentState.uiServer = null;
23
+ }
24
+
25
+ let flushError: unknown;
26
+ try {
27
+ flushMetadataCache(currentState);
28
+ } catch (error) {
29
+ flushError = error;
30
+ }
31
+
32
+ try {
33
+ await currentState.lifecycle.gracefulShutdown();
34
+ } catch (error) {
35
+ if (flushError) {
36
+ console.error("MCP: graceful shutdown failed after metadata flush error", error);
37
+ } else {
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ if (flushError) {
43
+ throw flushError;
44
+ }
45
+ }
15
46
 
16
47
  const earlyConfigPath = getConfigPathFromArgv();
17
48
  const earlyConfig = loadMcpConfig(earlyConfigPath);
@@ -33,6 +64,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
33
64
  name: spec.prefixedName,
34
65
  label: `MCP: ${spec.originalName}`,
35
66
  description: spec.description || "(no description)",
67
+ promptSnippet: truncateAtWord(spec.description, 100) || `MCP tool from ${spec.serverName}`,
36
68
  parameters: Type.Unsafe<Record<string, unknown>>(spec.inputSchema || { type: "object", properties: {} }),
37
69
  execute: createDirectToolExecutor(() => state, () => initPromise, spec),
38
70
  });
@@ -46,35 +78,59 @@ export default function mcpAdapter(pi: ExtensionAPI) {
46
78
  });
47
79
 
48
80
  pi.on("session_start", async (_event, ctx) => {
49
- initPromise = initializeMcp(pi, ctx);
81
+ const generation = ++lifecycleGeneration;
82
+ const previousState = state;
83
+ state = null;
84
+ initPromise = null;
85
+
86
+ try {
87
+ await shutdownState(previousState, "session_restart");
88
+ } catch (error) {
89
+ console.error("MCP: failed to shut down previous session state", error);
90
+ }
91
+
92
+ if (generation !== lifecycleGeneration) {
93
+ return;
94
+ }
95
+
96
+ const promise = initializeMcp(pi, ctx);
97
+ initPromise = promise;
50
98
 
51
- initPromise.then(s => {
52
- state = s;
99
+ promise.then(async (nextState) => {
100
+ if (generation !== lifecycleGeneration || initPromise !== promise) {
101
+ try {
102
+ await shutdownState(nextState, "stale_session_start");
103
+ } catch (error) {
104
+ console.error("MCP: failed to clean stale session state", error);
105
+ }
106
+ return;
107
+ }
108
+
109
+ state = nextState;
110
+ updateStatusBar(nextState);
53
111
  initPromise = null;
54
- updateStatusBar(s);
55
112
  }).catch(err => {
113
+ if (generation !== lifecycleGeneration) {
114
+ return;
115
+ }
116
+ if (initPromise !== promise && initPromise !== null) {
117
+ return;
118
+ }
56
119
  console.error("MCP initialization failed:", err);
57
120
  initPromise = null;
58
121
  });
59
122
  });
60
123
 
61
124
  pi.on("session_shutdown", async () => {
62
- if (initPromise) {
63
- try {
64
- state = await initPromise;
65
- } catch {
66
- // Initialization failed, nothing to clean up
67
- }
68
- }
125
+ ++lifecycleGeneration;
126
+ const currentState = state;
127
+ state = null;
128
+ initPromise = null;
69
129
 
70
- if (state) {
71
- if (state.uiServer) {
72
- state.uiServer.close("session_shutdown");
73
- state.uiServer = null;
74
- }
75
- flushMetadataCache(state);
76
- await state.lifecycle.gracefulShutdown();
77
- state = null;
130
+ try {
131
+ await shutdownState(currentState, "session_shutdown");
132
+ } catch (error) {
133
+ console.error("MCP: session shutdown cleanup failed", error);
78
134
  }
79
135
  });
80
136
 
@@ -84,8 +140,9 @@ export default function mcpAdapter(pi: ExtensionAPI) {
84
140
  if (!state && initPromise) {
85
141
  try {
86
142
  state = await initPromise;
87
- } catch {
88
- if (ctx.hasUI) ctx.ui.notify("MCP initialization failed", "error");
143
+ } catch (error) {
144
+ const message = error instanceof Error ? error.message : String(error);
145
+ if (ctx.hasUI) ctx.ui.notify(`MCP initialization failed: ${message}`, "error");
89
146
  return;
90
147
  }
91
148
  }
@@ -130,8 +187,9 @@ export default function mcpAdapter(pi: ExtensionAPI) {
130
187
  if (!state && initPromise) {
131
188
  try {
132
189
  state = await initPromise;
133
- } catch {
134
- if (ctx.hasUI) ctx.ui.notify("MCP initialization failed", "error");
190
+ } catch (error) {
191
+ const message = error instanceof Error ? error.message : String(error);
192
+ if (ctx.hasUI) ctx.ui.notify(`MCP initialization failed: ${message}`, "error");
135
193
  return;
136
194
  }
137
195
  }
@@ -148,6 +206,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
148
206
  name: "mcp",
149
207
  label: "MCP",
150
208
  description: buildProxyDescription(earlyConfig, earlyCache, directSpecs),
209
+ promptSnippet: "MCP gateway - connect to MCP servers and call their tools",
151
210
  parameters: Type.Object({
152
211
  tool: Type.Optional(Type.String({ description: "Tool name to call (e.g., 'xcodebuild_list_sims')" })),
153
212
  args: Type.Optional(Type.String({ description: "Arguments as JSON string (e.g., '{\"key\": \"value\"}')" })),
@@ -176,28 +235,24 @@ export default function mcpAdapter(pi: ExtensionAPI) {
176
235
  parsedArgs = JSON.parse(params.args);
177
236
  if (typeof parsedArgs !== "object" || parsedArgs === null || Array.isArray(parsedArgs)) {
178
237
  const gotType = Array.isArray(parsedArgs) ? "array" : parsedArgs === null ? "null" : typeof parsedArgs;
179
- return {
180
- content: [{ type: "text" as const, text: `Invalid args: expected a JSON object, got ${gotType}` }],
181
- isError: true,
182
- details: { error: "invalid_args_type" },
183
- };
238
+ throw new Error(`Invalid args: expected a JSON object, got ${gotType}`);
184
239
  }
185
- } catch (e) {
186
- return {
187
- content: [{ type: "text" as const, text: `Invalid args JSON: ${e instanceof Error ? e.message : e}` }],
188
- isError: true,
189
- details: { error: "invalid_args" },
190
- };
240
+ } catch (error) {
241
+ if (error instanceof SyntaxError) {
242
+ throw new Error(`Invalid args JSON: ${error.message}`, { cause: error });
243
+ }
244
+ throw error;
191
245
  }
192
246
  }
193
247
 
194
248
  if (!state && initPromise) {
195
249
  try {
196
250
  state = await initPromise;
197
- } catch {
251
+ } catch (error) {
252
+ const message = error instanceof Error ? error.message : String(error);
198
253
  return {
199
- content: [{ type: "text" as const, text: "MCP initialization failed" }],
200
- details: { error: "init_failed" },
254
+ content: [{ type: "text" as const, text: `MCP initialization failed: ${message}` }],
255
+ details: { error: "init_failed", message },
201
256
  };
202
257
  }
203
258
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-adapter",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",