pi-mcp-adapter 2.3.3 → 2.3.5
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 +15 -0
- package/index.ts +9 -3
- package/mcp-auth-flow.ts +3 -2
- package/mcp-callback-server.ts +73 -40
- package/mcp-oauth-provider.ts +26 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.3.5] - 2026-04-13
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Session lifecycle now always tears down OAuth callback state on restart and shutdown, preventing callback-server leaks across session transitions.
|
|
14
|
+
- OAuth callback server now calls `unref()` after successful bind so it no longer keeps sub-agent processes alive by itself.
|
|
15
|
+
- Strict OAuth port mode now rebinds to the configured callback port when safe, while refusing to switch ports when authorizations are still pending.
|
|
16
|
+
- Added focused lifecycle/callback-server regression coverage for teardown, `unref()`, strict rebinding, and pending-auth guardrails.
|
|
17
|
+
- Thanks @blai for the investigation and PR #43 that surfaced the sub-agent hang/root lifecycle issues.
|
|
18
|
+
|
|
19
|
+
## [2.3.4] - 2026-04-12
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- OAuth callback handling now allows dynamic-registration flows to fall back to a free local port when the preferred callback port is busy, while keeping pre-registered clients on their exact configured redirect port.
|
|
23
|
+
- Documented the new callback-port behavior and added focused auth-flow regression coverage.
|
|
24
|
+
|
|
10
25
|
## [2.3.3] - 2026-04-12
|
|
11
26
|
|
|
12
27
|
### Fixed
|
package/index.ts
CHANGED
|
@@ -8,7 +8,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
10
|
import { getConfigPathFromArgv, truncateAtWord } from "./utils.js";
|
|
11
|
-
import { initializeOAuth } from "./mcp-auth-flow.js";
|
|
11
|
+
import { initializeOAuth, shutdownOAuth } from "./mcp-auth-flow.js";
|
|
12
12
|
|
|
13
13
|
export default function mcpAdapter(pi: ExtensionAPI) {
|
|
14
14
|
let state: McpExtensionState | null = null;
|
|
@@ -85,7 +85,10 @@ export default function mcpAdapter(pi: ExtensionAPI) {
|
|
|
85
85
|
initPromise = null;
|
|
86
86
|
|
|
87
87
|
try {
|
|
88
|
-
await
|
|
88
|
+
await Promise.all([
|
|
89
|
+
shutdownState(previousState, "session_restart"),
|
|
90
|
+
shutdownOAuth(),
|
|
91
|
+
]);
|
|
89
92
|
} catch (error) {
|
|
90
93
|
console.error("MCP: failed to shut down previous session state", error);
|
|
91
94
|
}
|
|
@@ -133,7 +136,10 @@ export default function mcpAdapter(pi: ExtensionAPI) {
|
|
|
133
136
|
initPromise = null;
|
|
134
137
|
|
|
135
138
|
try {
|
|
136
|
-
await
|
|
139
|
+
await Promise.all([
|
|
140
|
+
shutdownState(currentState, "session_shutdown"),
|
|
141
|
+
shutdownOAuth(),
|
|
142
|
+
]);
|
|
137
143
|
} catch (error) {
|
|
138
144
|
console.error("MCP: session shutdown cleanup failed", error);
|
|
139
145
|
}
|
package/mcp-auth-flow.ts
CHANGED
|
@@ -100,8 +100,9 @@ export async function startAuth(
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
// Start the callback server
|
|
104
|
-
|
|
103
|
+
// Start the callback server.
|
|
104
|
+
// Pre-registered OAuth clients require an exact redirect URI, so enforce strict port binding.
|
|
105
|
+
await ensureCallbackServer({ strictPort: Boolean(config.clientId) })
|
|
105
106
|
|
|
106
107
|
// Generate and store OAuth state BEFORE creating the provider
|
|
107
108
|
// The SDK will call provider.state() to read this value
|
package/mcp-callback-server.ts
CHANGED
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { createServer, type Server, type IncomingMessage, type ServerResponse } from "http"
|
|
9
|
-
import {
|
|
10
|
-
|
|
9
|
+
import {
|
|
10
|
+
OAUTH_CALLBACK_PATH,
|
|
11
|
+
getConfiguredOAuthCallbackPort,
|
|
12
|
+
getOAuthCallbackPort,
|
|
13
|
+
setOAuthCallbackPort,
|
|
14
|
+
} from "./mcp-oauth-provider.js"
|
|
11
15
|
|
|
12
16
|
// HTML templates for callback responses
|
|
13
17
|
const HTML_SUCCESS = `<!DOCTYPE html>
|
|
@@ -65,29 +69,10 @@ const pendingAuths = new Map<string, PendingAuth>()
|
|
|
65
69
|
/** Timeout for callback completion (5 minutes) */
|
|
66
70
|
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
* Check if the callback port is already in use.
|
|
70
|
-
* Used to detect if another instance is running the server.
|
|
71
|
-
*/
|
|
72
|
-
async function isPortInUse(): Promise<boolean> {
|
|
73
|
-
return new Promise((resolve) => {
|
|
74
|
-
const socket = connect({ host: "127.0.0.1", port: OAUTH_CALLBACK_PORT })
|
|
75
|
-
socket.setTimeout(1000)
|
|
76
|
-
|
|
77
|
-
socket.on("connect", () => {
|
|
78
|
-
socket.end()
|
|
79
|
-
resolve(true)
|
|
80
|
-
})
|
|
72
|
+
const MAX_PORT_SCAN_ATTEMPTS = 25
|
|
81
73
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
socket.on("timeout", () => {
|
|
87
|
-
socket.destroy()
|
|
88
|
-
resolve(false)
|
|
89
|
-
})
|
|
90
|
-
})
|
|
74
|
+
interface EnsureCallbackServerOptions {
|
|
75
|
+
strictPort?: boolean
|
|
91
76
|
}
|
|
92
77
|
|
|
93
78
|
/**
|
|
@@ -160,27 +145,73 @@ function handleRequest(req: IncomingMessage, res: ServerResponse): void {
|
|
|
160
145
|
|
|
161
146
|
/**
|
|
162
147
|
* Ensure the callback server is running.
|
|
163
|
-
* If
|
|
148
|
+
* If strictPort is true, requires binding on the configured callback port.
|
|
149
|
+
* If strictPort is false, scans forward for an available local port.
|
|
164
150
|
*/
|
|
165
|
-
export async function ensureCallbackServer(): Promise<void> {
|
|
166
|
-
|
|
151
|
+
export async function ensureCallbackServer(options: EnsureCallbackServerOptions = {}): Promise<void> {
|
|
152
|
+
const configuredPort = getConfiguredOAuthCallbackPort()
|
|
153
|
+
const strictPort = options.strictPort === true
|
|
167
154
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
155
|
+
if (server) {
|
|
156
|
+
if (!strictPort || getOAuthCallbackPort() === configuredPort) return
|
|
157
|
+
|
|
158
|
+
if (pendingAuths.size > 0) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`OAuth callback server is running on port ${getOAuthCallbackPort()}, but strict callback port ${configuredPort} is required and cannot be switched while authorizations are pending`
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await stopCallbackServer()
|
|
171
165
|
}
|
|
172
166
|
|
|
173
|
-
|
|
167
|
+
const preferredPort = configuredPort
|
|
168
|
+
const maxAttempts = strictPort ? 1 : MAX_PORT_SCAN_ATTEMPTS
|
|
169
|
+
let lastError: Error | undefined
|
|
174
170
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
})
|
|
171
|
+
for (let offset = 0; offset < maxAttempts; offset++) {
|
|
172
|
+
const candidatePort = preferredPort + offset
|
|
173
|
+
const candidateServer = createServer(handleRequest)
|
|
179
174
|
|
|
180
|
-
|
|
181
|
-
resolve
|
|
182
|
-
|
|
183
|
-
|
|
175
|
+
try {
|
|
176
|
+
await new Promise<void>((resolve, reject) => {
|
|
177
|
+
candidateServer.once("error", (err) => {
|
|
178
|
+
reject(err)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
candidateServer.listen(candidatePort, "127.0.0.1", () => {
|
|
182
|
+
resolve()
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
server = candidateServer
|
|
187
|
+
server.unref()
|
|
188
|
+
setOAuthCallbackPort(candidatePort)
|
|
189
|
+
return
|
|
190
|
+
} catch (error) {
|
|
191
|
+
const nodeError = error as NodeJS.ErrnoException
|
|
192
|
+
await new Promise<void>((resolve) => {
|
|
193
|
+
candidateServer.close(() => resolve())
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
if (nodeError.code !== "EADDRINUSE") {
|
|
197
|
+
throw error
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
lastError = error instanceof Error ? error : new Error(String(error))
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (strictPort) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`OAuth callback port ${preferredPort} is already in use. Pre-registered OAuth clients require an exact redirect URI; set MCP_OAUTH_CALLBACK_PORT to your registered port or free port ${preferredPort}`,
|
|
207
|
+
{ cause: lastError }
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
throw new Error(
|
|
212
|
+
`OAuth callback port ${preferredPort} is already in use and no free port was found in range ${preferredPort}-${preferredPort + MAX_PORT_SCAN_ATTEMPTS - 1}`,
|
|
213
|
+
{ cause: lastError }
|
|
214
|
+
)
|
|
184
215
|
}
|
|
185
216
|
|
|
186
217
|
/**
|
|
@@ -225,11 +256,13 @@ export async function stopCallbackServer(): Promise<void> {
|
|
|
225
256
|
server = undefined
|
|
226
257
|
}
|
|
227
258
|
|
|
259
|
+
setOAuthCallbackPort(getConfiguredOAuthCallbackPort())
|
|
260
|
+
|
|
228
261
|
// Reject all pending auths (defer to allow any pending operations to complete)
|
|
229
262
|
const pendingList = Array.from(pendingAuths.entries())
|
|
230
263
|
pendingAuths.clear()
|
|
231
264
|
setTimeout(() => {
|
|
232
|
-
for (const [
|
|
265
|
+
for (const [, pending] of pendingList) {
|
|
233
266
|
clearTimeout(pending.timeout)
|
|
234
267
|
pending.reject(new Error("OAuth callback server stopped"))
|
|
235
268
|
}
|
package/mcp-oauth-provider.ts
CHANGED
|
@@ -27,9 +27,32 @@ import {
|
|
|
27
27
|
} from "./mcp-auth.js"
|
|
28
28
|
|
|
29
29
|
// Callback server configuration
|
|
30
|
-
const
|
|
30
|
+
const DEFAULT_OAUTH_CALLBACK_PORT = 19876
|
|
31
31
|
const OAUTH_CALLBACK_PATH = "/mcp/oauth/callback"
|
|
32
32
|
|
|
33
|
+
let configuredOAuthCallbackPort = DEFAULT_OAUTH_CALLBACK_PORT
|
|
34
|
+
|
|
35
|
+
if (process.env.MCP_OAUTH_CALLBACK_PORT) {
|
|
36
|
+
const parsedPort = Number.parseInt(process.env.MCP_OAUTH_CALLBACK_PORT, 10)
|
|
37
|
+
if (Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535) {
|
|
38
|
+
configuredOAuthCallbackPort = parsedPort
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let oauthCallbackPort = configuredOAuthCallbackPort
|
|
43
|
+
|
|
44
|
+
export function getConfiguredOAuthCallbackPort(): number {
|
|
45
|
+
return configuredOAuthCallbackPort
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getOAuthCallbackPort(): number {
|
|
49
|
+
return oauthCallbackPort
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function setOAuthCallbackPort(port: number): void {
|
|
53
|
+
oauthCallbackPort = port
|
|
54
|
+
}
|
|
55
|
+
|
|
33
56
|
/** Configuration options for OAuth */
|
|
34
57
|
export interface McpOAuthConfig {
|
|
35
58
|
grantType?: "authorization_code" | "client_credentials"
|
|
@@ -65,7 +88,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
65
88
|
*/
|
|
66
89
|
get redirectUrl(): string | undefined {
|
|
67
90
|
if (this.usesClientCredentials) return undefined
|
|
68
|
-
return `http://127.0.0.1:${
|
|
91
|
+
return `http://127.0.0.1:${getOAuthCallbackPort()}${OAUTH_CALLBACK_PATH}`
|
|
69
92
|
}
|
|
70
93
|
|
|
71
94
|
/**
|
|
@@ -262,4 +285,4 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
262
285
|
}
|
|
263
286
|
}
|
|
264
287
|
|
|
265
|
-
export {
|
|
288
|
+
export { DEFAULT_OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH }
|