pi-mcp-adapter 2.3.3 → 2.3.4

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,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.3.4] - 2026-04-12
11
+
12
+ ### Fixed
13
+ - 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.
14
+ - Documented the new callback-port behavior and added focused auth-flow regression coverage.
15
+
10
16
  ## [2.3.3] - 2026-04-12
11
17
 
12
18
  ### Fixed
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
- await ensureCallbackServer()
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
@@ -6,8 +6,12 @@
6
6
  */
7
7
 
8
8
  import { createServer, type Server, type IncomingMessage, type ServerResponse } from "http"
9
- import { connect } from "net"
10
- import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./mcp-oauth-provider.js"
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
- socket.on("error", () => {
83
- resolve(false)
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,68 @@ function handleRequest(req: IncomingMessage, res: ServerResponse): void {
160
145
 
161
146
  /**
162
147
  * Ensure the callback server is running.
163
- * If the port is in use by another process, fail fast with a clear error.
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
- if (server) return
151
+ export async function ensureCallbackServer(options: EnsureCallbackServerOptions = {}): Promise<void> {
152
+ const configuredPort = getConfiguredOAuthCallbackPort()
153
+ const strictPort = options.strictPort === true
167
154
 
168
- const running = await isPortInUse()
169
- if (running) {
170
- throw new Error(`OAuth callback port ${OAUTH_CALLBACK_PORT} is already in use`)
155
+ if (server) {
156
+ if (!strictPort || getOAuthCallbackPort() === configuredPort) return
157
+
158
+ throw new Error(
159
+ `OAuth callback server is already running on port ${getOAuthCallbackPort()}, but strict callback port ${configuredPort} is required`
160
+ )
171
161
  }
172
162
 
173
- server = createServer(handleRequest)
163
+ const preferredPort = configuredPort
164
+ const maxAttempts = strictPort ? 1 : MAX_PORT_SCAN_ATTEMPTS
165
+ let lastError: Error | undefined
174
166
 
175
- await new Promise<void>((resolve, reject) => {
176
- server!.once("error", (err) => {
177
- reject(err)
178
- })
167
+ for (let offset = 0; offset < maxAttempts; offset++) {
168
+ const candidatePort = preferredPort + offset
169
+ const candidateServer = createServer(handleRequest)
179
170
 
180
- server!.listen(OAUTH_CALLBACK_PORT, "127.0.0.1", () => {
181
- resolve()
182
- })
183
- })
171
+ try {
172
+ await new Promise<void>((resolve, reject) => {
173
+ candidateServer.once("error", (err) => {
174
+ reject(err)
175
+ })
176
+
177
+ candidateServer.listen(candidatePort, "127.0.0.1", () => {
178
+ resolve()
179
+ })
180
+ })
181
+
182
+ server = candidateServer
183
+ setOAuthCallbackPort(candidatePort)
184
+ return
185
+ } catch (error) {
186
+ const nodeError = error as NodeJS.ErrnoException
187
+ await new Promise<void>((resolve) => {
188
+ candidateServer.close(() => resolve())
189
+ })
190
+
191
+ if (nodeError.code !== "EADDRINUSE") {
192
+ throw error
193
+ }
194
+
195
+ lastError = error instanceof Error ? error : new Error(String(error))
196
+ }
197
+ }
198
+
199
+ if (strictPort) {
200
+ throw new Error(
201
+ `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}`,
202
+ { cause: lastError }
203
+ )
204
+ }
205
+
206
+ throw new Error(
207
+ `OAuth callback port ${preferredPort} is already in use and no free port was found in range ${preferredPort}-${preferredPort + MAX_PORT_SCAN_ATTEMPTS - 1}`,
208
+ { cause: lastError }
209
+ )
184
210
  }
185
211
 
186
212
  /**
@@ -225,6 +251,8 @@ export async function stopCallbackServer(): Promise<void> {
225
251
  server = undefined
226
252
  }
227
253
 
254
+ setOAuthCallbackPort(getConfiguredOAuthCallbackPort())
255
+
228
256
  // Reject all pending auths (defer to allow any pending operations to complete)
229
257
  const pendingList = Array.from(pendingAuths.entries())
230
258
  pendingAuths.clear()
@@ -27,9 +27,32 @@ import {
27
27
  } from "./mcp-auth.js"
28
28
 
29
29
  // Callback server configuration
30
- const OAUTH_CALLBACK_PORT = 19876
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:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`
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 { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH }
288
+ export { DEFAULT_OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-adapter",
3
- "version": "2.3.3",
3
+ "version": "2.3.4",
4
4
  "description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",