pi-mcp-adapter 2.2.1 → 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,15 @@ 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
+
10
19
  ## [2.2.1] - 2026-03-23
11
20
 
12
21
  ### Fixed
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";
@@ -12,6 +12,37 @@ import { getConfigPathFromArgv, truncateAtWord } from "./utils.js";
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);
@@ -47,35 +78,59 @@ export default function mcpAdapter(pi: ExtensionAPI) {
47
78
  });
48
79
 
49
80
  pi.on("session_start", async (_event, ctx) => {
50
- 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;
51
98
 
52
- initPromise.then(s => {
53
- 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);
54
111
  initPromise = null;
55
- updateStatusBar(s);
56
112
  }).catch(err => {
113
+ if (generation !== lifecycleGeneration) {
114
+ return;
115
+ }
116
+ if (initPromise !== promise && initPromise !== null) {
117
+ return;
118
+ }
57
119
  console.error("MCP initialization failed:", err);
58
120
  initPromise = null;
59
121
  });
60
122
  });
61
123
 
62
124
  pi.on("session_shutdown", async () => {
63
- if (initPromise) {
64
- try {
65
- state = await initPromise;
66
- } catch {
67
- // Initialization failed, nothing to clean up
68
- }
69
- }
125
+ ++lifecycleGeneration;
126
+ const currentState = state;
127
+ state = null;
128
+ initPromise = null;
70
129
 
71
- if (state) {
72
- if (state.uiServer) {
73
- state.uiServer.close("session_shutdown");
74
- state.uiServer = null;
75
- }
76
- flushMetadataCache(state);
77
- await state.lifecycle.gracefulShutdown();
78
- state = null;
130
+ try {
131
+ await shutdownState(currentState, "session_shutdown");
132
+ } catch (error) {
133
+ console.error("MCP: session shutdown cleanup failed", error);
79
134
  }
80
135
  });
81
136
 
@@ -85,8 +140,9 @@ export default function mcpAdapter(pi: ExtensionAPI) {
85
140
  if (!state && initPromise) {
86
141
  try {
87
142
  state = await initPromise;
88
- } catch {
89
- 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");
90
146
  return;
91
147
  }
92
148
  }
@@ -131,8 +187,9 @@ export default function mcpAdapter(pi: ExtensionAPI) {
131
187
  if (!state && initPromise) {
132
188
  try {
133
189
  state = await initPromise;
134
- } catch {
135
- 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");
136
193
  return;
137
194
  }
138
195
  }
@@ -178,28 +235,24 @@ export default function mcpAdapter(pi: ExtensionAPI) {
178
235
  parsedArgs = JSON.parse(params.args);
179
236
  if (typeof parsedArgs !== "object" || parsedArgs === null || Array.isArray(parsedArgs)) {
180
237
  const gotType = Array.isArray(parsedArgs) ? "array" : parsedArgs === null ? "null" : typeof parsedArgs;
181
- return {
182
- content: [{ type: "text" as const, text: `Invalid args: expected a JSON object, got ${gotType}` }],
183
- isError: true,
184
- details: { error: "invalid_args_type" },
185
- };
238
+ throw new Error(`Invalid args: expected a JSON object, got ${gotType}`);
186
239
  }
187
- } catch (e) {
188
- return {
189
- content: [{ type: "text" as const, text: `Invalid args JSON: ${e instanceof Error ? e.message : e}` }],
190
- isError: true,
191
- details: { error: "invalid_args" },
192
- };
240
+ } catch (error) {
241
+ if (error instanceof SyntaxError) {
242
+ throw new Error(`Invalid args JSON: ${error.message}`, { cause: error });
243
+ }
244
+ throw error;
193
245
  }
194
246
  }
195
247
 
196
248
  if (!state && initPromise) {
197
249
  try {
198
250
  state = await initPromise;
199
- } catch {
251
+ } catch (error) {
252
+ const message = error instanceof Error ? error.message : String(error);
200
253
  return {
201
- content: [{ type: "text" as const, text: "MCP initialization failed" }],
202
- details: { error: "init_failed" },
254
+ content: [{ type: "text" as const, text: `MCP initialization failed: ${message}` }],
255
+ details: { error: "init_failed", message },
203
256
  };
204
257
  }
205
258
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-adapter",
3
- "version": "2.2.1",
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",