pi-mcp-adapter 2.32.1 → 2.33.0
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 +47 -0
- package/OAUTH.md +367 -0
- package/README.md +111 -16
- package/claude-plugin-loader.ts +365 -0
- package/commands.ts +8 -21
- package/config.ts +79 -24
- package/consent-manager.ts +40 -9
- package/direct-tools.ts +29 -8
- package/dist/claude-plugin-loader.d.ts +9 -0
- package/dist/claude-plugin-loader.js +362 -0
- package/dist/claude-plugin-loader.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +66 -22
- package/dist/config.js.map +1 -1
- package/dist/metadata-cache.js +3 -13
- package/dist/metadata-cache.js.map +1 -1
- package/dist/package-mcp-loader.js +2 -2
- package/dist/package-mcp-loader.js.map +1 -1
- package/dist/types.d.ts +23 -3
- package/dist/types.js +26 -0
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +36 -5
- package/dist/utils.js.map +1 -1
- package/http-ca.ts +49 -0
- package/index.ts +403 -16
- package/init.ts +38 -3
- package/mcp-auth-fetch.ts +125 -0
- package/mcp-auth-flow.ts +147 -68
- package/mcp-auth.ts +22 -7
- package/mcp-code.ts +45 -18
- package/mcp-install.ts +60 -0
- package/mcp-oauth-provider.ts +82 -13
- package/mcp-output-guard.ts +53 -42
- package/mcp-panel-theme.ts +104 -0
- package/mcp-panel.ts +366 -332
- package/mcp-references.ts +9 -14
- package/mcp-refresh-lock.ts +62 -0
- package/mcp-script-worker.mjs +3 -1
- package/mcp-setup-panel.ts +381 -341
- package/mcp-status.ts +5 -0
- package/metadata-cache.ts +3 -13
- package/namespace-tools.ts +1 -1
- package/oauth-diagnostics.ts +31 -0
- package/oauth.ts +10 -3
- package/package-mcp-loader.ts +2 -2
- package/package.json +16 -5
- package/proxy-modes.ts +108 -7
- package/runtime-owner.ts +16 -1
- package/sampling-handler.ts +9 -32
- package/server-manager.ts +135 -23
- package/session-approvals.ts +187 -0
- package/skills/mcp-scripting/SKILL.md +9 -1
- package/state.ts +10 -1
- package/tool-approval.ts +10 -22
- package/tool-metadata.ts +14 -0
- package/types.ts +50 -3
- package/ui-server.ts +8 -1
- package/utils.ts +36 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.33.0] - 2026-09-10
|
|
11
|
+
|
|
12
|
+
### Highlights
|
|
13
|
+
- Install MCP servers from a URL and complete browser OAuth without leaving the scripting workflow.
|
|
14
|
+
- Keep large tool catalogs out of context until search activates the tools you need.
|
|
15
|
+
- Use OAuth more reliably across multiple Pi processes, private gateways, stalled providers, and dynamic loopback callbacks.
|
|
16
|
+
- Connect to private HTTPS servers with custom CA bundles and run stdio servers with tighter environment isolation.
|
|
17
|
+
- Inspect richer tool schemas and filter larger intermediate results safely in MCP scripts.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- HTTPS MCP servers can use an origin-scoped `caFile` PEM trust bundle for Streamable HTTP and SSE without disabling certificate verification. Thanks to [@desmonna](https://github.com/desmonna) for #527.
|
|
21
|
+
- `directTools: "search"` keeps direct tools inactive until `mcp({ search })` finds and activates them. Search-mode tools do not count toward the 75-tool advisory. Thanks to [@chiptoe-svg](https://github.com/chiptoe-svg) for PR #525.
|
|
22
|
+
- Install MCP servers from a URL with `mcp({ action: "install", url: "..." })`. Thanks to [@exoulster](https://github.com/exoulster) for PR #532.
|
|
23
|
+
- Script `tools.describe()` now exposes server-advertised output schemas for `data.structuredContent`, including after metadata refreshes. (#522)
|
|
24
|
+
- Stdio MCP servers can set `inheritEnv: false` while retaining platform defaults and explicit `env` values. Thanks to [@zenolam](https://github.com/zenolam) for #509.
|
|
25
|
+
- Local Claude plugin bundles can now be loaded from trusted configured directories, including bundled MCP servers and skills. Thanks to [@gugu91](https://github.com/gugu91) for PR #493.
|
|
26
|
+
- OAuth loopback redirects can use `{port}` with `localhost`, `127.0.0.1`, or `::1` when a provider permits RFC 8252 dynamic ports. Thanks to [@nrutman](https://github.com/nrutman) for PR #483.
|
|
27
|
+
- Runtime MCP status now includes each server's registered direct-tool count, including resource tools. Thanks to [@FischLu](https://github.com/FischLu) for #482.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- Approval brokers now observe and can gate cached MCP calls; session grants apply only on abstention or no claim. Thanks to [@yanqianglu](https://github.com/yanqianglu) for #534.
|
|
31
|
+
- MCP setup and server panels now follow Pi's active theme and TUI components. (#488)
|
|
32
|
+
- MCP sampling requests now route through Pi's `ModelRegistry.complete`, leaving provider authentication, environment, and base URL handling to the host.
|
|
33
|
+
- `mcp({ connect })` now activates newly discovered direct tools from the connection result, so they become available at the correct point in the transcript. (#490) Thanks to [@chiptoe-svg](https://github.com/chiptoe-svg) for PR #494.
|
|
34
|
+
- OAuth transaction support uses pinned MCP SDK preview builds until the supporting SDK release is available on npm.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- macOS HTTP connection failures to literal private/link-local IPs now retain network details and suggest Local Network Privacy checks without assuming a privacy denial. Thanks to [@tdhooghe](https://github.com/tdhooghe) for #543.
|
|
38
|
+
- OAuth credential updates now coordinate across Pi processes, release cleanly after failures and cancellation, and keep callback tokens bound to the client that requested them. Optional `PI_MCP_OAUTH_LOG` diagnostics identify transactions without logging credentials. Thanks to [@CharlesMcMillan](https://github.com/CharlesMcMillan) for PR #528.
|
|
39
|
+
- MCP runtime cancellation now works on Node 20.0.0 when `AbortSignal.any` is unavailable. (#537)
|
|
40
|
+
- OAuth now forwards configured service headers to same-origin discovery, registration, token exchange, and refresh requests, enabling authentication behind service-token gateways without leaking credentials to other origins. Thanks to [@ethanbrown3](https://github.com/ethanbrown3) for PR #533 and [@Davasny](https://github.com/Davasny) for reporting #530.
|
|
41
|
+
- Script `mcp({ action: "auth-start" })` now opens the authorization URL and watches the loopback callback to complete OAuth automatically, while retaining pasted `auth-complete` as a fallback. Thanks to [@exoulster](https://github.com/exoulster) for PR #532.
|
|
42
|
+
- Namespace proxy tool names now stay within provider limits without creating collisions for long server names. Thanks to [@saikpr](https://github.com/saikpr) for PR #529.
|
|
43
|
+
- Script calls now preserve intermediate data for filtering within a fixed 16 MiB cumulative transfer budget while retaining final-output limits. (#520)
|
|
44
|
+
- Namespace proxy argument guidance now uses exact search-result tool names for schema inspection. Thanks to [@r1ckyIn](https://github.com/r1ckyIn) for PR #519.
|
|
45
|
+
- Script `tools.describe()` now retains documented input field guidance alongside compact parameter shapes, including formats and units. (#521)
|
|
46
|
+
- Early MCP tool discovery now honors `--mcp-config=<path>`, including paths containing `=`. (#512)
|
|
47
|
+
- Updated `qs` to patched version 6.16.0. (#517)
|
|
48
|
+
- The optional `@earendil-works/pi-ai` peer now supports Pi 0.85 alongside 0.84.1, avoiding npm resolution conflicts. Thanks to [@dyld-w](https://github.com/dyld-w) for #507.
|
|
49
|
+
- Session-scoped MCP tool approvals and MCP App iframe consent now persist on and restore from the active Pi session branch. (#492)
|
|
50
|
+
- The `/mcp` panel no longer marks reconnects as cached when no cache entry was restored. Thanks to [@fyq163](https://github.com/fyq163) for #497.
|
|
51
|
+
- MCP output truncation now uses Pi host truncation semantics and formatting while preserving MCP artifact spill behavior.
|
|
52
|
+
- Updated Ajv's transitive `fast-uri` dependency to patched version 3.1.7. Thanks to [@escuelallenquen](https://github.com/escuelallenquen) for #513.
|
|
53
|
+
- Exclusive mode now honors an explicit `--mcp-config` override instead of always loading the agent-global configuration. Thanks to [@willem445](https://github.com/willem445) for #496.
|
|
54
|
+
- OAuth discovery, dynamic registration, token exchange, and token refresh requests now have a timeout and honor cancellation instead of hanging the agent on stalled providers. Thanks to [@west-david](https://github.com/west-david) for #485 and PR #486.
|
|
55
|
+
- OAuth redirect URI mismatches now preserve refreshable credentials and re-register stale dynamic clients after `invalid_grant`. Thanks to [@CharlesMcMillan](https://github.com/CharlesMcMillan) for PR #495.
|
|
56
|
+
|
|
10
57
|
## [2.32.1] - 2026-09-01
|
|
11
58
|
|
|
12
59
|
### Fixed
|
package/OAUTH.md
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# OAuth 2.1 Authentication for MCP
|
|
2
|
+
|
|
3
|
+
This document describes the OAuth 2.1 + PKCE authentication implementation for the Pi MCP Adapter using the official MCP SDK.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The Pi MCP Adapter uses the official MCP SDK's built-in OAuth implementation, which provides:
|
|
8
|
+
|
|
9
|
+
- **Automatic OAuth endpoint discovery** (RFC 9728) - No manual configuration needed
|
|
10
|
+
- **Dynamic Client Registration fallback** (RFC 7591) - Used when no pre-registered `clientId` is configured and the server supports registration
|
|
11
|
+
- **Automatic callback handling** - Built-in HTTP server handles callbacks automatically
|
|
12
|
+
- **Automatic token refresh** - SDK handles token refresh transparently
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- **PKCE (S256)** - Mandatory code challenge method for OAuth 2.1
|
|
17
|
+
- **Automatic Callback Server** - Local browser redirects automatically when available
|
|
18
|
+
- **Manual Remote Flow** - Copy auth URLs and pasted redirect URLs/codes for headless SSH sessions
|
|
19
|
+
- **Dynamic Client Registration fallback** - Registers when no pre-registered `clientId` is configured and the server supports registration
|
|
20
|
+
- **Auto-Discovery** - Discovers OAuth endpoints from server metadata
|
|
21
|
+
- **Automatic Token Refresh** - SDK handles expired tokens automatically
|
|
22
|
+
- **State Parameter Validation** - CSRF protection
|
|
23
|
+
- **Secure Token Storage** - Persistent OAuth entries are stored in the operating system credential store
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
### Minimal Configuration (Recommended)
|
|
28
|
+
|
|
29
|
+
For most MCP servers, you only need the URL:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"my-oauth-server": {
|
|
35
|
+
"url": "https://api.example.com/mcp"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
OAuth is automatically enabled for HTTP servers. The SDK will:
|
|
42
|
+
- Auto-detect if the server requires OAuth
|
|
43
|
+
- Discover OAuth endpoints from the server
|
|
44
|
+
- Use Dynamic Client Registration fallback when no pre-registered client is configured and the server supports it
|
|
45
|
+
- Handle the entire OAuth flow including callback
|
|
46
|
+
|
|
47
|
+
### Optional Configuration
|
|
48
|
+
|
|
49
|
+
You can optionally provide a pre-registered client:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"my-oauth-server": {
|
|
55
|
+
"url": "https://api.example.com/mcp",
|
|
56
|
+
"auth": "oauth",
|
|
57
|
+
"oauth": {
|
|
58
|
+
"clientId": "your-client-id",
|
|
59
|
+
"clientSecret": "your-client-secret",
|
|
60
|
+
"scope": "read write",
|
|
61
|
+
"authorizationParams": { "access_type": "offline", "prompt": "consent" },
|
|
62
|
+
"redirectUri": "http://localhost:3118/callback"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
### Configuration Options
|
|
72
|
+
|
|
73
|
+
- `url` - The MCP server URL (required)
|
|
74
|
+
- `auth` - Set to `"oauth"` to force OAuth, `false` to disable, or omit to auto-detect
|
|
75
|
+
- `oauth.grantType` - `"authorization_code"` (default, browser flow) or `"client_credentials"` (non-interactive)
|
|
76
|
+
- `oauth.clientId` - Pre-registered client ID. MCP 2026 prefers pre-registered clients or Client ID Metadata Documents; this adapter falls back to Dynamic Client Registration when the ID is omitted and the server supports it.
|
|
77
|
+
- `oauth.clientSecret` - Client secret for confidential clients (optional)
|
|
78
|
+
- `oauth.scope` - Requested OAuth scopes (optional)
|
|
79
|
+
- `oauth.authorizationParams` - Extra authorization URL parameters for provider-specific extensions, such as Google's `{ "access_type": "offline", "prompt": "consent" }`. Flow-owned parameters like `client_id`, `redirect_uri`, `scope`, `state`, `code_challenge`, `response_type`, and `resource` cannot be overridden.
|
|
80
|
+
- `oauth.redirectUri` - Browser callback URI to advertise and bind, such as `http://localhost:3118/callback`. Use `{port}` in a loopback URI when the provider permits an OS-assigned RFC 8252 port, for example `http://127.0.0.1:{port}/callback` (optional)
|
|
81
|
+
- `oauth.clientName` - Client display name used for Dynamic Client Registration fallback (optional, defaults to `Pi Coding Agent`)
|
|
82
|
+
- `oauth.clientUri` - Client homepage URI used for Dynamic Client Registration fallback (optional)
|
|
83
|
+
- `oauth.authServerMetadataUrl` - HTTPS OAuth/OIDC authorization-server metadata document to use authoritatively when MCP protected-resource discovery is unavailable (optional; issuer validation remains enabled)
|
|
84
|
+
- `oauth.skipIssuerMetadataValidation` - Set `true` only for a known-misconfigured authorization server whose metadata issuer cannot be fixed immediately. This weakens OAuth issuer validation.
|
|
85
|
+
|
|
86
|
+
Dynamic fallback clients normally omit `oauth.redirectUri`; the adapter starts the callback server lazily on the default loopback host (`localhost`) and asks the OS for an available local port when auth begins. Use `oauth.redirectUri` when the provider requires a pre-registered callback, such as Slack MCP's Claude-compatible `http://localhost:3118/callback`. A loopback URI must use `http://` with `localhost`, `127.0.0.1`, or `[::1]`. It may contain an explicit port, which is bound exactly, or `{port}`, which is replaced with the OS-assigned port in the authorization and token requests.
|
|
87
|
+
|
|
88
|
+
### Non-Interactive `client_credentials`
|
|
89
|
+
|
|
90
|
+
For machine-to-machine OAuth, configure `grantType: "client_credentials"`.
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"mcpServers": {
|
|
95
|
+
"my-service": {
|
|
96
|
+
"url": "https://api.example.com/mcp",
|
|
97
|
+
"auth": "oauth",
|
|
98
|
+
"oauth": {
|
|
99
|
+
"grantType": "client_credentials",
|
|
100
|
+
"clientId": "service-client-id",
|
|
101
|
+
"clientSecret": "service-client-secret",
|
|
102
|
+
"scope": "read write"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This flow does not open a browser or use callback handling. `oauth.redirectUri` is ignored for `client_credentials`; `oauth.clientName` and `oauth.clientUri` still apply to dynamic client registration metadata.
|
|
110
|
+
|
|
111
|
+
## Usage
|
|
112
|
+
|
|
113
|
+
### Step 1: Authenticate
|
|
114
|
+
|
|
115
|
+
Run the `/mcp-auth` command with the server name:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
/mcp-auth my-oauth-server
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Manual `/mcp-auth` is the default flow. If you set `settings.autoAuth: true`, proxy/direct tool execution will trigger OAuth automatically when a server returns `needs-auth`, then retry the original operation once.
|
|
122
|
+
|
|
123
|
+
This will:
|
|
124
|
+
1. Start the callback server lazily on an OS-assigned local port, on an OS-assigned host-specific `{port}` callback, or on the exact `oauth.redirectUri` port for fixed callbacks
|
|
125
|
+
2. Discover OAuth endpoints automatically
|
|
126
|
+
3. Use Dynamic Client Registration fallback when no `clientId` is configured and the server supports registration
|
|
127
|
+
4. Open your browser for authentication
|
|
128
|
+
5. Wait for the automatic callback
|
|
129
|
+
6. Complete the OAuth flow
|
|
130
|
+
7. Store tokens securely
|
|
131
|
+
|
|
132
|
+
### Remote/headless authentication
|
|
133
|
+
|
|
134
|
+
When Pi runs over SSH or in a headless environment, use the proxy tool to retrieve the authorization URL instead of relying on OS browser launch:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
mcp({ action: "auth-start", server: "my-oauth-server" })
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Open the returned URL in your local browser. After approval, copy the full redirected localhost URL from the browser address bar (the page may fail to load locally) and complete the same pending auth flow:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
mcp({
|
|
144
|
+
action: "auth-complete",
|
|
145
|
+
server: "my-oauth-server",
|
|
146
|
+
args: { redirectUrl: "http://localhost:19876/callback?code=...&state=..." }
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
You can also pass only the `code` query parameter with `args: { code: "..." }`. JSON-string args remain supported. Redirect URL completion validates the saved OAuth state; raw code completion is available for providers that display a code directly.
|
|
151
|
+
|
|
152
|
+
### Step 2: Use the Server
|
|
153
|
+
|
|
154
|
+
Once authenticated, use the server normally:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
mcp({ server: "my-oauth-server" })
|
|
158
|
+
mcp({ tool: "my-tool", args: { key: "value" } })
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The SDK automatically:
|
|
162
|
+
- Adds the access token to requests
|
|
163
|
+
- Refreshes expired tokens automatically
|
|
164
|
+
- Re-authenticates if tokens are invalid
|
|
165
|
+
|
|
166
|
+
To clear stored OAuth credentials and force a fresh authorization:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
/mcp logout my-oauth-server
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## How It Works
|
|
173
|
+
|
|
174
|
+
### Authentication Flow
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
┌─────────┐ ┌──────────────┐ ┌─────────────────┐
|
|
178
|
+
│ Pi │────▶│ MCP Server │────▶│ OAuth Server │
|
|
179
|
+
│ │ │ │ │ │
|
|
180
|
+
│ 1. Init │ │ 2. Discovery │ │ 3. Register │
|
|
181
|
+
│ │ │ │ │ │
|
|
182
|
+
│ │◀────│ │◀────│ 4. Auth URL │
|
|
183
|
+
│ │ │ │ │ │
|
|
184
|
+
│ │────▶│ Callback │◀────│ 5. Browser │
|
|
185
|
+
│ │ │ Server │ │ Redirect │
|
|
186
|
+
│ │ │ │ │ │
|
|
187
|
+
│ │◀────│ │◀────│ 6. Code │
|
|
188
|
+
│ │ │ │ │ │
|
|
189
|
+
│ │────▶│ │────▶│ 7. Exchange │
|
|
190
|
+
│ │ │ │ │ │
|
|
191
|
+
│ │◀────│ │◀────│ 8. Tokens │
|
|
192
|
+
└─────────┘ └──────────────┘ └─────────────────┘
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Auto-Discovery
|
|
196
|
+
|
|
197
|
+
The SDK attempts to discover OAuth endpoints using:
|
|
198
|
+
|
|
199
|
+
1. **RFC 9728 Metadata** - Fetches `/.well-known/oauth-protected-resource`
|
|
200
|
+
2. **WWW-Authenticate Header** - Parses `resource_metadata` from 401 responses
|
|
201
|
+
|
|
202
|
+
### Dynamic Client Registration fallback
|
|
203
|
+
|
|
204
|
+
MCP 2026 prefers pre-registered clients or Client ID Metadata Documents. The adapter exposes pre-registered `oauth.clientId` today. It does not publish an HTTPS Client ID Metadata Document yet, so when no `clientId` is provided the SDK uses Dynamic Client Registration as a fallback if the server supports it:
|
|
205
|
+
|
|
206
|
+
1. Discovers the registration endpoint from OAuth metadata
|
|
207
|
+
2. Registers a new client with:
|
|
208
|
+
- `client_name`: configured `oauth.clientName` or "Pi Coding Agent"
|
|
209
|
+
- `client_uri`: configured `oauth.clientUri` or the adapter repository URL
|
|
210
|
+
- `redirect_uris`: `["http://localhost:<active-callback-port>/callback"]`, or the configured `oauth.redirectUri`
|
|
211
|
+
- `grant_types`: `["authorization_code", "refresh_token"]`
|
|
212
|
+
3. Stores the registered client credentials and the redirect URIs returned by the authorization server
|
|
213
|
+
|
|
214
|
+
When a fresh browser auth starts, cached dynamic fallback client info with tokens is re-registered if its stored redirect URIs are missing or do not include the current redirect URI. Token refresh does not perform this redirect check, so existing refresh-token grants keep working even after a callback setting changes.
|
|
215
|
+
|
|
216
|
+
### Callback Server
|
|
217
|
+
|
|
218
|
+
A Node.js HTTP server runs on a loopback callback endpoint and handles the active callback path:
|
|
219
|
+
|
|
220
|
+
- Dynamic registration starts the callback server only when auth begins, binds the default host `localhost`, and asks the OS for an available local port
|
|
221
|
+
- Pre-registered clients (`oauth.clientId`) without `oauth.redirectUri` require the exact configured callback port from `MCP_OAUTH_CALLBACK_PORT` or the default `19876` on `localhost`
|
|
222
|
+
- `oauth.redirectUri` binds the exact loopback host and path. An explicit port is bound exactly; `{port}` asks the OS for a port and is replaced with that assigned value before the URI is advertised to the provider
|
|
223
|
+
|
|
224
|
+
- Handles `code`, `state`, and `error` parameters
|
|
225
|
+
- Displays success/error HTML pages
|
|
226
|
+
- Validates state parameter for CSRF protection
|
|
227
|
+
- Has a 5-minute timeout for pending authorizations
|
|
228
|
+
|
|
229
|
+
## Shared credential transactions
|
|
230
|
+
|
|
231
|
+
SDK auth invocations run inside a credential transaction, including discovery, refresh, invalidation/retry, and callback exchange. The transaction ends when auth returns `AUTHORIZED`, returns `REDIRECT`, or throws; it does not span browser consent. Startup cleanup and logout use short transactions against the same credential identity.
|
|
232
|
+
|
|
233
|
+
Ownership uses `fs-native-extensions` kernel advisory locks on permanent files under the OS user's home (`.pi-mcp-adapter/refresh-locks-v2`). Lock identity follows the credential store's server-name account, not the configurable legacy OAuth import directory. Files are never deleted or reclaimed; process death releases ownership in the kernel. Every process sharing credentials must use this implementation. Old directory-lock builds do not coordinate with it and must be restarted.
|
|
234
|
+
|
|
235
|
+
Waiting acquisition is cancellable. Deactivating a provider aborts its auth fetches but does not release ownership before the underlying operation settles. A rotated response already received is persisted before release. Auth fetches have the configured `PI_MCP_OAUTH_REQUEST_TIMEOUT_MS` deadline (30 seconds by default), including configured discovery; ordinary MCP streaming requests are not given that deadline.
|
|
236
|
+
|
|
237
|
+
Concurrent processes may still perform successive refreshes after rereading the latest token. This serializes redemption; it does not claim cross-process deduplication. A process crash after remote rotation but before local persistence can still require reauthorization. Native lock support and cooperative access by all credential writers are required; the public token writer participates in the same transaction boundary.
|
|
238
|
+
|
|
239
|
+
This development branch pins SDK client/core previews to one immutable commit because `withAuthTransaction` is not yet released. Replace both preview dependencies with the supporting SDK release before publishing a stable adapter release.
|
|
240
|
+
|
|
241
|
+
### Opt-in diagnostics
|
|
242
|
+
|
|
243
|
+
Set `PI_MCP_OAUTH_LOG` to an absolute JSONL file path before launching Pi. Records include PID, transaction ID, server name, wait/total duration, and terminal result. They exclude token values, client secrets, authorization codes, endpoint URLs, and raw error messages. Logging failure never changes authentication behavior.
|
|
244
|
+
|
|
245
|
+
Events are `oauth_transaction_waiting`, `oauth_transaction_acquired`, `oauth_transaction_completed`, and `oauth_transaction_failed`. Check the completed event's `result` for `AUTHORIZED` or `REDIRECT`.
|
|
246
|
+
|
|
247
|
+
## Token Storage
|
|
248
|
+
|
|
249
|
+
Persistent OAuth entries are stored per configured server name in the operating system credential store, using macOS Keychain, Windows Credential Manager, or Linux Secret Service/libsecret through `@napi-rs/keyring`. The stored entry contains tokens, dynamic client information, legacy verifier/state fields when present, and the server URL binding.
|
|
250
|
+
|
|
251
|
+
The adapter fails closed when the OS credential store is unavailable. On headless Linux, configure an unlocked Secret Service-compatible keyring before using persistent OAuth; the adapter does not silently fall back to plaintext token files.
|
|
252
|
+
|
|
253
|
+
On Linux, if credential access fails because Pi inherited a revoked session keyring, the adapter makes one best-effort retry through `keyctl session - node <packaged helper>`. This lets explicit re-authentication write fresh credentials from a new session keyring without restarting a long-lived tmux or server process. The recovery path requires `keyctl` and `node` on `PATH`; missing, locked, or otherwise unavailable credential stores still fail closed.
|
|
254
|
+
|
|
255
|
+
Complete credential entries are held in memory for the lifetime of the Pi process on every supported credential-store platform. The MCP SDK reads the access token before every outbound request, so caching avoids a credential-store lookup on each tool call; on Linux this specifically avoids overloading the Secret Service daemon. The cache is filled on the first read for a server and covers both present and absent entries. Authenticating, refreshing, and logging out all update it immediately, so credential changes made through Pi take effect at once. Status-panel inspection deliberately bypasses it and still reads the store directly.
|
|
256
|
+
|
|
257
|
+
A credential changed or deleted by another process while Pi is running is not observed immediately. The affected server picks it up after the first credential-backed authentication failure in that `needs-auth` episode: Pi discards the cached entry, and the following read reloads from the credential store. Restarting Pi also clears the cache. Set `PI_MCP_ADAPTER_DISABLE_AUTH_CACHE=1` to turn the cache off entirely and restore a credential-store read per request.
|
|
258
|
+
|
|
259
|
+
Older versions stored plaintext entries at `~/.pi/agent/mcp-oauth/sha256-<server-hash>/tokens.json`, or under `settings.oauthDir` / `MCP_OAUTH_DIR`. On first read after upgrade, a valid legacy entry is imported into the OS credential store and the plaintext `tokens.json` file is removed. These directories are now legacy import locations, not persistent credential stores or isolation namespaces.
|
|
260
|
+
|
|
261
|
+
The stored `serverUrl` field ensures credentials are invalidated if the server URL changes.
|
|
262
|
+
|
|
263
|
+
## Security Considerations
|
|
264
|
+
|
|
265
|
+
### PKCE
|
|
266
|
+
|
|
267
|
+
All OAuth flows use PKCE with the S256 method, preventing authorization code interception attacks.
|
|
268
|
+
|
|
269
|
+
### State Parameter
|
|
270
|
+
|
|
271
|
+
A cryptographically secure random state parameter is generated for each flow and validated on callback.
|
|
272
|
+
|
|
273
|
+
### Issuer Metadata Validation
|
|
274
|
+
|
|
275
|
+
OAuth authorization-server metadata normally must echo the expected issuer. This protects against authorization-server mix-up. The adapter keeps this check on by default.
|
|
276
|
+
|
|
277
|
+
For private servers with known-broken metadata, `oauth.skipIssuerMetadataValidation: true` forwards the SDK's issuer-validation opt-out for that server only. Use it only as a temporary workaround while the server metadata is fixed. Do not use it for public or untrusted servers.
|
|
278
|
+
|
|
279
|
+
When an MCP server does not publish usable protected-resource metadata, configure `oauth.authServerMetadataUrl` with the HTTPS URL of its OAuth/OIDC authorization-server metadata document. That document is authoritative instead of MCP protected-resource discovery, and its issuer is still checked by default. Treat this as trusted configuration and point it only at a metadata endpoint you explicitly trust.
|
|
280
|
+
|
|
281
|
+
### OS Credential Store
|
|
282
|
+
|
|
283
|
+
Persistent OAuth credentials are written to the OS credential store. Legacy plaintext files are read only for one-way migration and are removed after successful import. On Linux, revoked session-keyring errors can be retried once through a fresh `keyctl session` helper during explicit re-authentication.
|
|
284
|
+
|
|
285
|
+
Credential entries reside in process memory for the lifetime of the Pi process on every supported credential-store platform rather than being re-read per request. They are never written anywhere but the OS credential store, and the process-memory copy is discarded on exit.
|
|
286
|
+
|
|
287
|
+
### URL Validation
|
|
288
|
+
|
|
289
|
+
Credentials are tied to a specific server URL. If the URL changes, the credentials are invalidated and re-authentication is required.
|
|
290
|
+
|
|
291
|
+
## Troubleshooting
|
|
292
|
+
|
|
293
|
+
### "No OAuth tokens found"
|
|
294
|
+
|
|
295
|
+
Run `/mcp-auth <server>` to authenticate.
|
|
296
|
+
|
|
297
|
+
### "Failed to discover OAuth endpoints"
|
|
298
|
+
|
|
299
|
+
The SDK automatically discovers OAuth endpoints from the MCP server. If discovery fails, the server may require a pre-registered client ID:
|
|
300
|
+
|
|
301
|
+
```json
|
|
302
|
+
{
|
|
303
|
+
"mcpServers": {
|
|
304
|
+
"server": {
|
|
305
|
+
"url": "https://api.example.com/mcp",
|
|
306
|
+
"auth": "oauth",
|
|
307
|
+
"oauth": {
|
|
308
|
+
"clientId": "your-client-id",
|
|
309
|
+
"scope": "read"
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### "Dynamic client registration not supported"
|
|
317
|
+
|
|
318
|
+
Some servers require pre-registered clients. Obtain a client ID from your OAuth provider and add it to the config.
|
|
319
|
+
|
|
320
|
+
### Callback server already in use
|
|
321
|
+
|
|
322
|
+
Dynamic fallback browser OAuth uses a lazy OS-assigned port on the default loopback host (`localhost`), so the configured default port being busy should not block fallback registration.
|
|
323
|
+
|
|
324
|
+
For pre-registered OAuth clients (`oauth.clientId`), the callback redirect URI must match the provider registration policy. Set `oauth.redirectUri` to the full fixed callback, such as Slack MCP's Claude-compatible `http://localhost:3118/callback`, use a loopback `{port}` URI when the provider explicitly permits dynamic RFC 8252 ports, or free/set `MCP_OAUTH_CALLBACK_PORT` when you rely on the default `/callback` path without an explicit redirect URI.
|
|
325
|
+
|
|
326
|
+
### Browser doesn't open
|
|
327
|
+
|
|
328
|
+
If the browser fails to open (e.g., in SSH sessions), the authorization URL will be displayed. Copy it manually to your browser.
|
|
329
|
+
|
|
330
|
+
## Architecture
|
|
331
|
+
|
|
332
|
+
The OAuth implementation uses the following modules:
|
|
333
|
+
|
|
334
|
+
- `mcp-auth.ts` - Auth storage and retrieval through the OS credential store, with one-way legacy `tokens.json` import
|
|
335
|
+
- `mcp-oauth-provider.ts` - SDK OAuthClientProvider implementation
|
|
336
|
+
- `mcp-callback-server.ts` - Node.js HTTP callback server
|
|
337
|
+
- `mcp-auth-flow.ts` - High-level auth flow using SDK transport
|
|
338
|
+
|
|
339
|
+
## SDK Integration
|
|
340
|
+
|
|
341
|
+
The implementation uses the stable modular MCP client and OAuth APIs:
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
import {
|
|
345
|
+
auth,
|
|
346
|
+
StreamableHTTPClientTransport,
|
|
347
|
+
UnauthorizedError,
|
|
348
|
+
type OAuthClientProvider,
|
|
349
|
+
} from "@modelcontextprotocol/client"
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The `McpOAuthProvider` class implements `OAuthClientProvider` and is passed to `StreamableHTTPClientTransport`:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
const transport = new StreamableHTTPClientTransport(url, {
|
|
356
|
+
authProvider: new McpOAuthProvider(serverName, serverUrl, config, callbacks),
|
|
357
|
+
})
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## References
|
|
361
|
+
|
|
362
|
+
- [MCP SDK Documentation](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
363
|
+
- [MCP Authorization Specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)
|
|
364
|
+
- [OAuth 2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11)
|
|
365
|
+
- [PKCE (RFC 7636)](https://datatracker.ietf.org/doc/html/rfc7636)
|
|
366
|
+
- [Dynamic Client Registration (RFC 7591)](https://datatracker.ietf.org/doc/html/rfc7591)
|
|
367
|
+
- [OAuth Protected Resource Metadata (RFC 9728)](https://datatracker.ietf.org/doc/html/rfc9728)
|