pi-mcp-extension 1.11.5 → 1.11.6

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/README.md CHANGED
@@ -163,7 +163,7 @@ For Pi 1.x compatibility, server entries also accept `type` as an alias for `tra
163
163
  | `cwd` | `string` | session working directory | Working directory for a `stdio` server (rejected on `streamable-http` / `sse`). A relative path resolves against the session working directory and a leading `~` expands to your home directory |
164
164
  | `env` | `Record<string, string>` | — | Extra environment variables for the child process (literal values, no `${VAR}` interpolation) |
165
165
  | `url` | `string` | — | Server URL (**required** for `streamable-http` and `sse`) |
166
- | `headers` | `Record<string, string>` | — | Static headers sent with `streamable-http` and `sse` requests |
166
+ | `headers` | `Record<string, string>` | — | Static headers sent with `streamable-http` and `sse` requests. When `auth` is set, OAuth owns `Authorization`, so an `Authorization` entry here is ignored (other headers still apply) |
167
167
  | `auth` (or `oauth`) | `object` | — | OAuth 2.1 configuration described below and in [OAUTH.md](./OAUTH.md) (`streamable-http` / `sse` only) |
168
168
  | `lifecycle` | `"eager" \| "lazy"` | `"lazy"` (`"eager"` for `pi.registerMcpServer()` unless `enabled: false`) | `eager` = auto-start on `session_start`, `lazy` = connect on request via `mcp_connect`, `/mcp:start`, or `/mcp:auth` |
169
169
  | `enabled` | `boolean` | `true` | Pi 1.3.0 switch: `false` keeps the entry configured but out of the eager set (normalized to `lifecycle: "lazy"`, even when `lifecycle: "eager"` is also set) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-extension",
3
- "version": "1.11.5",
3
+ "version": "1.11.6",
4
4
  "description": "MCP (Model Context Protocol) client extension for the Pi coding agent — connect Pi to any MCP server",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -20,6 +20,31 @@ export interface ManualAuthChallengeOptions {
20
20
  signal?: AbortSignal | undefined;
21
21
  }
22
22
 
23
+ /**
24
+ * Drop any `Authorization` header (any casing) from a configured static header set.
25
+ *
26
+ * WHY: `Authorization` is the one header an OAuth server owns. The MCP SDK builds it from the
27
+ * OAuth provider's access token and then spreads `requestInit.headers` on top, so an exact-case
28
+ * `Authorization` key silently replaces the token, while `authorization` in any other casing
29
+ * survives as a second entry that `new Headers()` comma-joins into one malformed value. The same
30
+ * static value would also ride along on the OAuth discovery, registration, and token requests
31
+ * made through `createHeadersFetch`, leaking a resource-server credential to the authorization
32
+ * server. Drop it (and only it) whenever OAuth is configured; every other static header
33
+ * (tenant keys, gateways) still applies.
34
+ */
35
+ export function stripAuthorizationHeader(
36
+ headers: Record<string, string> | undefined,
37
+ ): Record<string, string> | undefined {
38
+ if (!headers) return undefined;
39
+ const kept = Object.entries(headers).filter(([name]) => !isAuthorizationHeader(name));
40
+ return kept.length > 0 ? Object.fromEntries(kept) : undefined;
41
+ }
42
+
43
+ /** Case-insensitive test for the single header an OAuth server owns. */
44
+ export function isAuthorizationHeader(name: string): boolean {
45
+ return name.toLowerCase() === "authorization";
46
+ }
47
+
23
48
  /**
24
49
  * Wrap a fetch implementation so configured server headers are attached as
25
50
  * defaults on every request (e.g. during SDK OAuth metadata discovery and
@@ -9,6 +9,7 @@ import { isSafeAuthorizationUrl } from "./callback-url.js";
9
9
  import {
10
10
  discoverManualAuthChallenge,
11
11
  createHeadersFetch,
12
+ stripAuthorizationHeader,
12
13
  type ManualAuthChallenge,
13
14
  } from "./auth-challenge.js";
14
15
  import {
@@ -629,16 +630,22 @@ export async function authorize(
629
630
  // if empty, which the callback server then rejects as a CSRF error.
630
631
  authProvider.setState(oauthState);
631
632
 
633
+ // WHY: `Authorization` belongs to the OAuth flow here. Filter the same set the transport
634
+ // uses, so this flow's own challenge probe and its SDK `fetchFn` (metadata, registration,
635
+ // token) never carry a static credential that was configured for the MCP server — the
636
+ // authorization server is usually a different origin.
637
+ const staticHeaders = stripAuthorizationHeader(config.headers);
638
+
632
639
  const authChallenge = await discoverChallengeWithFallback({
633
640
  serverName,
634
641
  serverUrl,
635
- headers: config.headers,
642
+ headers: staticHeaders,
636
643
  signal,
637
644
  });
638
645
  assertActive();
639
646
 
640
647
  const callbackPromise = registerCallbackPromise(callbackServer, oauthState);
641
- const customFetch = config.headers ? createHeadersFetch(config.headers) : undefined;
648
+ const customFetch = staticHeaders ? createHeadersFetch(staticHeaders) : undefined;
642
649
 
643
650
  const authResult = await runInitialSdkAuth(
644
651
  authProvider,
package/src/config.ts CHANGED
@@ -187,7 +187,9 @@ export const ServerConfigSchema = z.preprocess(
187
187
  /**
188
188
  * Static HTTP headers to include with every request (streamable-http / sse only).
189
189
  * Useful for API-key-based auth (e.g. { "Authorization": "Bearer <key>" }).
190
- * For OAuth2, use the "auth" field instead.
190
+ * For OAuth2, use the "auth" field instead: when "auth" is set, OAuth owns the
191
+ * Authorization header and any Authorization entry here is ignored (other headers
192
+ * still apply).
191
193
  */
192
194
  headers: z.record(z.string(), z.string()).optional(),
193
195
  /**
@@ -16,6 +16,8 @@ import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.
16
16
  import { McpError } from "./errors.js";
17
17
  import type { ServerConfig } from "./config.js";
18
18
  import { McpOAuthProvider } from "./oauth-provider.js";
19
+ import { isAuthorizationHeader, stripAuthorizationHeader } from "./auth-challenge.js";
20
+ import { reportDiagnostic } from "./debug-log.js";
19
21
  import { createLineSplitter } from "./stdio-process.js";
20
22
  import { sanitizeTerminalText } from "./terminal-text.js";
21
23
 
@@ -32,9 +34,9 @@ function resolveEnv(extra?: Record<string, string>): Record<string, string> {
32
34
  return env;
33
35
  }
34
36
 
35
- function createRequestInit(config: ServerConfig): RequestInit | undefined {
37
+ function createRequestInit(headers: Record<string, string> | undefined): RequestInit | undefined {
36
38
  // Build requestInit for static headers (API keys, etc.)
37
- return config.headers ? { headers: config.headers } : undefined;
39
+ return headers ? { headers } : undefined;
38
40
  }
39
41
 
40
42
  function createOAuthProvider(
@@ -188,7 +190,18 @@ export function createTransport(
188
190
  ): Transport {
189
191
  const cwd = typeof cwdOrOnStderr === "string" ? cwdOrOnStderr : cwdArg;
190
192
  const onStderr = typeof cwdOrOnStderr === "function" ? cwdOrOnStderr : undefined;
191
- const requestInit = createRequestInit(config);
193
+ // WHY: with OAuth configured the SDK owns `Authorization` (see stripAuthorizationHeader);
194
+ // a leftover static header is a migration artifact, so drop it and record that we did.
195
+ const staticHeaders = config.auth ? stripAuthorizationHeader(config.headers) : config.headers;
196
+ if (config.auth && config.headers && Object.keys(config.headers).some(isAuthorizationHeader)) {
197
+ reportDiagnostic({
198
+ level: "info",
199
+ event: "config.static_authorization_ignored",
200
+ server: serverName,
201
+ message: `Ignoring the static Authorization header for "${serverName}": OAuth is configured, so requests carry the OAuth access token instead.`,
202
+ });
203
+ }
204
+ const requestInit = createRequestInit(staticHeaders);
192
205
  const authProvider = createOAuthProvider(serverName, config, storageDir);
193
206
 
194
207
  switch (config.transport) {