@phi-code-admin/phi-code 0.86.0 → 0.88.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/docs/adr/0001-phase-contract.md +57 -0
  3. package/docs/adr/0002-independent-review.md +47 -0
  4. package/docs/fork-policy.md +18 -0
  5. package/extensions/phi/agents.ts +7 -4
  6. package/extensions/phi/benchmark.ts +129 -49
  7. package/extensions/phi/browser.ts +10 -37
  8. package/extensions/phi/btw/btw.ts +1 -7
  9. package/extensions/phi/chrome/index.ts +1283 -741
  10. package/extensions/phi/commit.ts +9 -14
  11. package/extensions/phi/goal/index.ts +10 -33
  12. package/extensions/phi/init.ts +37 -47
  13. package/extensions/phi/keys.ts +2 -7
  14. package/extensions/phi/mcp/callback-server.ts +162 -165
  15. package/extensions/phi/mcp/config.ts +122 -136
  16. package/extensions/phi/mcp/errors.ts +18 -23
  17. package/extensions/phi/mcp/index.ts +322 -355
  18. package/extensions/phi/mcp/oauth-provider.ts +289 -289
  19. package/extensions/phi/mcp/server-manager.ts +390 -413
  20. package/extensions/phi/mcp/tool-bridge.ts +381 -415
  21. package/extensions/phi/memory.ts +2 -2
  22. package/extensions/phi/models.ts +27 -26
  23. package/extensions/phi/orchestrator.ts +451 -237
  24. package/extensions/phi/productivity.ts +4 -2
  25. package/extensions/phi/providers/agent-def.ts +1 -1
  26. package/extensions/phi/providers/alibaba.ts +56 -7
  27. package/extensions/phi/providers/context-window.ts +4 -1
  28. package/extensions/phi/providers/live-models.ts +5 -20
  29. package/extensions/phi/providers/orchestrator-helpers.ts +31 -5
  30. package/extensions/phi/providers/phase-machine.ts +283 -0
  31. package/extensions/phi/setup.ts +196 -169
  32. package/extensions/phi/skill-loader.ts +14 -17
  33. package/extensions/phi/smart-router.ts +6 -6
  34. package/extensions/phi/web-search.ts +90 -50
  35. package/package.json +1 -1
@@ -11,36 +11,40 @@
11
11
  * constructs an OAuthClientProvider and the transport factory wires it in.
12
12
  */
13
13
 
14
- import type { OAuthClientProvider, OAuthDiscoveryState } from "@modelcontextprotocol/sdk/client/auth.js";
15
- import type { OAuthClientMetadata, OAuthClientInformationMixed, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
16
- import { readFile, writeFile, mkdir, unlink } from "node:fs/promises";
17
- import { join } from "node:path";
18
- import { homedir } from "node:os";
19
14
  import { createHash } from "node:crypto";
15
+ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
16
+ import { homedir } from "node:os";
17
+ import { join } from "node:path";
18
+ import type { OAuthClientProvider, OAuthDiscoveryState } from "@modelcontextprotocol/sdk/client/auth.js";
19
+ import type {
20
+ OAuthClientInformationMixed,
21
+ OAuthClientMetadata,
22
+ OAuthTokens,
23
+ } from "@modelcontextprotocol/sdk/shared/auth.js";
20
24
 
21
25
  // ─── Types ────────────────────────────────────────────────────────────────────
22
26
 
23
27
  /** Auth config in mcp.json server entry. Matches the Zod AuthConfigSchema in config.ts. */
24
28
  export interface AuthConfig {
25
- /** Auth type. Currently only "oauth" is supported. */
26
- type?: "oauth" | undefined;
27
- /**
28
- * Callback URL the OAuth server redirects to after authorization.
29
- * Default: auto-detected local callback server.
30
- */
31
- redirectUrl?: string | undefined;
32
- /**
33
- * Optional scope to request during authorization.
34
- */
35
- scope?: string | undefined;
36
- /**
37
- * Pre-registered client_id (skip dynamic client registration).
38
- */
39
- clientId?: string | undefined;
40
- /**
41
- * Pre-registered client_secret.
42
- */
43
- clientSecret?: string | undefined;
29
+ /** Auth type. Currently only "oauth" is supported. */
30
+ type?: "oauth" | undefined;
31
+ /**
32
+ * Callback URL the OAuth server redirects to after authorization.
33
+ * Default: auto-detected local callback server.
34
+ */
35
+ redirectUrl?: string | undefined;
36
+ /**
37
+ * Optional scope to request during authorization.
38
+ */
39
+ scope?: string | undefined;
40
+ /**
41
+ * Pre-registered client_id (skip dynamic client registration).
42
+ */
43
+ clientId?: string | undefined;
44
+ /**
45
+ * Pre-registered client_secret.
46
+ */
47
+ clientSecret?: string | undefined;
44
48
  }
45
49
 
46
50
  // ─── Callback Server Port ───────────────────────────────────────────────────────
@@ -49,36 +53,36 @@ let callbackPort = 19876;
49
53
 
50
54
  /** Set the callback server port. Called by the auth flow when the server starts. */
51
55
  export function setCallbackPort(port: number): void {
52
- callbackPort = port;
56
+ callbackPort = port;
53
57
  }
54
58
 
55
59
  /** Get the current callback server port. */
56
60
  export function getCallbackPort(): number {
57
- return callbackPort;
61
+ return callbackPort;
58
62
  }
59
63
 
60
64
  // ─── Persistent State Types ───────────────────────────────────────────────────
61
65
 
62
66
  interface StoredClientInfo {
63
- client_id: string;
64
- client_secret: string | undefined;
67
+ client_id: string;
68
+ client_secret: string | undefined;
65
69
  }
66
70
 
67
71
  interface StoredTokens {
68
- access_token: string;
69
- token_type: string | undefined;
70
- refresh_token: string | undefined;
71
- expires_in: number | undefined;
72
- scope: string | undefined;
73
- /** ISO timestamp when tokens were saved (for expiry estimation). */
74
- saved_at: string | undefined;
72
+ access_token: string;
73
+ token_type: string | undefined;
74
+ refresh_token: string | undefined;
75
+ expires_in: number | undefined;
76
+ scope: string | undefined;
77
+ /** ISO timestamp when tokens were saved (for expiry estimation). */
78
+ saved_at: string | undefined;
75
79
  }
76
80
 
77
81
  interface StoredState {
78
- clientInfo: StoredClientInfo | undefined;
79
- tokens: StoredTokens | undefined;
80
- codeVerifier: string | undefined;
81
- discoveryState: OAuthDiscoveryState | undefined;
82
+ clientInfo: StoredClientInfo | undefined;
83
+ tokens: StoredTokens | undefined;
84
+ codeVerifier: string | undefined;
85
+ discoveryState: OAuthDiscoveryState | undefined;
82
86
  }
83
87
 
84
88
  // ─── File helpers ─────────────────────────────────────────────────────────────
@@ -86,247 +90,243 @@ interface StoredState {
86
90
  const AUTH_DIR = join(homedir(), ".pi", "agent", "mcp-auth");
87
91
 
88
92
  function statePath(serverName: string): string {
89
- // Hash the server name to avoid filesystem issues with special chars
90
- const hash = createHash("sha256").update(serverName).digest("hex").slice(0, 16);
91
- return join(AUTH_DIR, `${hash}.json`);
93
+ // Hash the server name to avoid filesystem issues with special chars
94
+ const hash = createHash("sha256").update(serverName).digest("hex").slice(0, 16);
95
+ return join(AUTH_DIR, `${hash}.json`);
92
96
  }
93
97
 
94
98
  async function loadState(serverName: string): Promise<StoredState> {
95
- try {
96
- const raw = await readFile(statePath(serverName), "utf8");
97
- const parsed = JSON.parse(raw) as StoredState;
98
- return {
99
- clientInfo: parsed.clientInfo ?? undefined,
100
- tokens: parsed.tokens ?? undefined,
101
- codeVerifier: parsed.codeVerifier ?? undefined,
102
- discoveryState: parsed.discoveryState ?? undefined,
103
- };
104
- } catch {
105
- return {
106
- clientInfo: undefined,
107
- tokens: undefined,
108
- codeVerifier: undefined,
109
- discoveryState: undefined,
110
- };
111
- }
99
+ try {
100
+ const raw = await readFile(statePath(serverName), "utf8");
101
+ const parsed = JSON.parse(raw) as StoredState;
102
+ return {
103
+ clientInfo: parsed.clientInfo ?? undefined,
104
+ tokens: parsed.tokens ?? undefined,
105
+ codeVerifier: parsed.codeVerifier ?? undefined,
106
+ discoveryState: parsed.discoveryState ?? undefined,
107
+ };
108
+ } catch {
109
+ return {
110
+ clientInfo: undefined,
111
+ tokens: undefined,
112
+ codeVerifier: undefined,
113
+ discoveryState: undefined,
114
+ };
115
+ }
112
116
  }
113
117
 
114
118
  async function saveState(serverName: string, state: StoredState): Promise<void> {
115
- await mkdir(AUTH_DIR, { recursive: true });
116
- // Only write defined fields
117
- const toWrite: Record<string, unknown> = {};
118
- if (state.clientInfo !== undefined) toWrite.clientInfo = state.clientInfo;
119
- if (state.tokens !== undefined) toWrite.tokens = state.tokens;
120
- if (state.codeVerifier !== undefined) toWrite.codeVerifier = state.codeVerifier;
121
- if (state.discoveryState !== undefined) toWrite.discoveryState = state.discoveryState;
122
- await writeFile(statePath(serverName), JSON.stringify(toWrite, null, 2), "utf8");
119
+ await mkdir(AUTH_DIR, { recursive: true });
120
+ // Only write defined fields
121
+ const toWrite: Record<string, unknown> = {};
122
+ if (state.clientInfo !== undefined) toWrite.clientInfo = state.clientInfo;
123
+ if (state.tokens !== undefined) toWrite.tokens = state.tokens;
124
+ if (state.codeVerifier !== undefined) toWrite.codeVerifier = state.codeVerifier;
125
+ if (state.discoveryState !== undefined) toWrite.discoveryState = state.discoveryState;
126
+ await writeFile(statePath(serverName), JSON.stringify(toWrite, null, 2), "utf8");
123
127
  }
124
128
 
125
129
  // ─── OAuthClientProvider Implementation ────────────────────────────────────────
126
130
 
127
131
  export class McpOAuthProvider implements OAuthClientProvider {
128
- private serverName: string;
129
- private authConfig: AuthConfig;
130
- private _redirectUrl: string | undefined;
131
- private _onAuthRequired: ((url: URL) => void | Promise<void>) | undefined;
132
- private _oauthState: string | undefined;
133
-
134
- constructor(
135
- serverName: string,
136
- authConfig: AuthConfig,
137
- onAuthRequired?: (url: URL) => void | Promise<void>,
138
- ) {
139
- this.serverName = serverName;
140
- this.authConfig = authConfig;
141
- this._redirectUrl = authConfig.redirectUrl;
142
- this._onAuthRequired = onAuthRequired;
143
- }
144
-
145
- // --- redirectUrl ---
146
-
147
- get redirectUrl(): string | URL {
148
- // Use configured redirect URL if provided, otherwise use the callback server
149
- // Use 127.0.0.1 explicitly (IPv4) to match the callback server binding
150
- return this._redirectUrl || `http://127.0.0.1:${callbackPort}/callback`;
151
- }
152
-
153
- // --- clientMetadata ---
154
-
155
- get clientMetadata(): OAuthClientMetadata {
156
- const redirectUrl = String(this.redirectUrl);
157
- return {
158
- client_name: `pi-mcp/${this.serverName}`,
159
- redirect_uris: [redirectUrl],
160
- grant_types: ["authorization_code", "refresh_token"],
161
- response_types: ["code"],
162
- token_endpoint_auth_method: this.authConfig.clientSecret ? "client_secret_basic" : "none",
163
- };
164
- }
165
-
166
- // --- clientInformation ---
167
-
168
- async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {
169
- // If static credentials provided, use those
170
- if (this.authConfig.clientId) {
171
- return {
172
- client_id: this.authConfig.clientId,
173
- ...(this.authConfig.clientSecret && { client_secret: this.authConfig.clientSecret }),
174
- };
175
- }
176
- // Otherwise load from persisted DCR state
177
- const state = await loadState(this.serverName);
178
- if (state.clientInfo) {
179
- return {
180
- client_id: state.clientInfo.client_id,
181
- ...(state.clientInfo.client_secret && { client_secret: state.clientInfo.client_secret }),
182
- };
183
- }
184
- return undefined;
185
- }
186
-
187
- // --- saveClientInformation (DCR) ---
188
-
189
- async saveClientInformation(clientInformation: OAuthClientInformationMixed): Promise<void> {
190
- const state = await loadState(this.serverName);
191
- state.clientInfo = {
192
- client_id: clientInformation.client_id,
193
- client_secret: clientInformation.client_secret,
194
- };
195
- await saveState(this.serverName, state);
196
- }
197
-
198
- // --- tokens ---
199
-
200
- async tokens(): Promise<OAuthTokens | undefined> {
201
- const state = await loadState(this.serverName);
202
- if (!state.tokens) return undefined;
203
-
204
- // Always return stored tokens even if expired.
205
- // The SDK's auth() function checks tokens?.refresh_token and attempts
206
- // silent refresh before falling back to a new authorization flow.
207
- // Returning undefined for expired tokens would prevent that silent refresh
208
- // and force the user to re-authenticate via browser every time.
209
- //
210
- // Flow when we return expired tokens:
211
- // transport sends expired access_token401
212
- // → auth() sees refresh_token silent refresh → success → retry
213
- //
214
- // Flow when we return undefined (WRONG):
215
- // transport has no token auth() → no refresh possible → REDIRECT
216
- // → user must re-authenticate in browser
217
-
218
- // Build OAuthTokens only include defined fields
219
- const tokens: Record<string, string> = {
220
- access_token: state.tokens.access_token,
221
- };
222
- if (state.tokens.token_type !== undefined) tokens.token_type = state.tokens.token_type;
223
- if (state.tokens.refresh_token !== undefined) tokens.refresh_token = state.tokens.refresh_token;
224
- return tokens as unknown as OAuthTokens;
225
- }
226
-
227
- // --- saveTokens ---
228
-
229
- async saveTokens(tokens: OAuthTokens): Promise<void> {
230
- const state = await loadState(this.serverName);
231
- state.tokens = {
232
- access_token: tokens.access_token,
233
- token_type: tokens.token_type,
234
- refresh_token: tokens.refresh_token,
235
- expires_in: tokens.expires_in,
236
- scope: tokens.scope,
237
- saved_at: new Date().toISOString(),
238
- };
239
- await saveState(this.serverName, state);
240
- }
241
-
242
- // --- redirectToAuthorization ---
243
-
244
- async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
245
- if (this._onAuthRequired) {
246
- await this._onAuthRequired(authorizationUrl);
247
- } else {
248
- // Fallback: just log it
249
- console.error(
250
- `[pi-mcp] OAuth authorization required for "${this.serverName}".`,
251
- `\n Open this URL in your browser: ${authorizationUrl.toString()}`,
252
- );
253
- }
254
- }
255
-
256
- // --- PKCE code verifier ---
257
-
258
- async saveCodeVerifier(codeVerifier: string): Promise<void> {
259
- const state = await loadState(this.serverName);
260
- state.codeVerifier = codeVerifier;
261
- await saveState(this.serverName, state);
262
- }
263
-
264
- async codeVerifier(): Promise<string> {
265
- const state = await loadState(this.serverName);
266
- if (!state.codeVerifier) {
267
- throw new Error(`[pi-mcp] No PKCE code verifier found for "${this.serverName}"`);
268
- }
269
- return state.codeVerifier;
270
- }
271
-
272
- // --- Discovery state caching ---
273
-
274
- async saveDiscoveryState(discState: OAuthDiscoveryState): Promise<void> {
275
- const state = await loadState(this.serverName);
276
- state.discoveryState = discState;
277
- await saveState(this.serverName, state);
278
- }
279
-
280
- async discoveryState(): Promise<OAuthDiscoveryState | undefined> {
281
- const state = await loadState(this.serverName);
282
- return state.discoveryState;
283
- }
284
-
285
- // --- OAuth state parameter (CSRF protection) ---
286
-
287
- /**
288
- * Set the OAuth state parameter before calling auth().
289
- * This should be called with a cryptographically random value.
290
- */
291
- setState(state: string): void {
292
- this._oauthState = state;
293
- }
294
-
295
- /**
296
- * Returns the OAuth state parameter for CSRF protection.
297
- * This is called by the SDK's auth() function when building the authorization URL.
298
- * Returns empty string if no state has been set (no CSRF protection).
299
- */
300
- async state(): Promise<string> {
301
- return this._oauthState || "";
302
- }
303
-
304
- // --- Credential invalidation ---
305
-
306
- async invalidateCredentials(scope: "all" | "client" | "tokens" | "verifier" | "discovery"): Promise<void> {
307
- const state = await loadState(this.serverName);
308
- switch (scope) {
309
- case "all":
310
- state.clientInfo = undefined;
311
- state.tokens = undefined;
312
- state.codeVerifier = undefined;
313
- state.discoveryState = undefined;
314
- break;
315
- case "client":
316
- state.clientInfo = undefined;
317
- break;
318
- case "tokens":
319
- state.tokens = undefined;
320
- break;
321
- case "verifier":
322
- state.codeVerifier = undefined;
323
- break;
324
- case "discovery":
325
- state.discoveryState = undefined;
326
- break;
327
- }
328
- await saveState(this.serverName, state);
329
- }
132
+ private serverName: string;
133
+ private authConfig: AuthConfig;
134
+ private _redirectUrl: string | undefined;
135
+ private _onAuthRequired: ((url: URL) => void | Promise<void>) | undefined;
136
+ private _oauthState: string | undefined;
137
+
138
+ constructor(serverName: string, authConfig: AuthConfig, onAuthRequired?: (url: URL) => void | Promise<void>) {
139
+ this.serverName = serverName;
140
+ this.authConfig = authConfig;
141
+ this._redirectUrl = authConfig.redirectUrl;
142
+ this._onAuthRequired = onAuthRequired;
143
+ }
144
+
145
+ // --- redirectUrl ---
146
+
147
+ get redirectUrl(): string | URL {
148
+ // Use configured redirect URL if provided, otherwise use the callback server
149
+ // Use 127.0.0.1 explicitly (IPv4) to match the callback server binding
150
+ return this._redirectUrl || `http://127.0.0.1:${callbackPort}/callback`;
151
+ }
152
+
153
+ // --- clientMetadata ---
154
+
155
+ get clientMetadata(): OAuthClientMetadata {
156
+ const redirectUrl = String(this.redirectUrl);
157
+ return {
158
+ client_name: `pi-mcp/${this.serverName}`,
159
+ redirect_uris: [redirectUrl],
160
+ grant_types: ["authorization_code", "refresh_token"],
161
+ response_types: ["code"],
162
+ token_endpoint_auth_method: this.authConfig.clientSecret ? "client_secret_basic" : "none",
163
+ };
164
+ }
165
+
166
+ // --- clientInformation ---
167
+
168
+ async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {
169
+ // If static credentials provided, use those
170
+ if (this.authConfig.clientId) {
171
+ return {
172
+ client_id: this.authConfig.clientId,
173
+ ...(this.authConfig.clientSecret && { client_secret: this.authConfig.clientSecret }),
174
+ };
175
+ }
176
+ // Otherwise load from persisted DCR state
177
+ const state = await loadState(this.serverName);
178
+ if (state.clientInfo) {
179
+ return {
180
+ client_id: state.clientInfo.client_id,
181
+ ...(state.clientInfo.client_secret && { client_secret: state.clientInfo.client_secret }),
182
+ };
183
+ }
184
+ return undefined;
185
+ }
186
+
187
+ // --- saveClientInformation (DCR) ---
188
+
189
+ async saveClientInformation(clientInformation: OAuthClientInformationMixed): Promise<void> {
190
+ const state = await loadState(this.serverName);
191
+ state.clientInfo = {
192
+ client_id: clientInformation.client_id,
193
+ client_secret: clientInformation.client_secret,
194
+ };
195
+ await saveState(this.serverName, state);
196
+ }
197
+
198
+ // --- tokens ---
199
+
200
+ async tokens(): Promise<OAuthTokens | undefined> {
201
+ const state = await loadState(this.serverName);
202
+ if (!state.tokens) return undefined;
203
+
204
+ // Always return stored tokens even if expired.
205
+ // The SDK's auth() function checks tokens?.refresh_token and attempts
206
+ // silent refresh before falling back to a new authorization flow.
207
+ // Returning undefined for expired tokens would prevent that silent refresh
208
+ // and force the user to re-authenticate via browser every time.
209
+ //
210
+ // Flow when we return expired tokens:
211
+ // transport sends expired access_token 401
212
+ // auth() sees refresh_token silent refresh success retry
213
+ //
214
+ // Flow when we return undefined (WRONG):
215
+ // transport has no tokenauth() → no refresh possible → REDIRECT
216
+ // → user must re-authenticate in browser
217
+
218
+ // Build OAuthTokens only include defined fields
219
+ const tokens: Record<string, string> = {
220
+ access_token: state.tokens.access_token,
221
+ };
222
+ if (state.tokens.token_type !== undefined) tokens.token_type = state.tokens.token_type;
223
+ if (state.tokens.refresh_token !== undefined) tokens.refresh_token = state.tokens.refresh_token;
224
+ return tokens as unknown as OAuthTokens;
225
+ }
226
+
227
+ // --- saveTokens ---
228
+
229
+ async saveTokens(tokens: OAuthTokens): Promise<void> {
230
+ const state = await loadState(this.serverName);
231
+ state.tokens = {
232
+ access_token: tokens.access_token,
233
+ token_type: tokens.token_type,
234
+ refresh_token: tokens.refresh_token,
235
+ expires_in: tokens.expires_in,
236
+ scope: tokens.scope,
237
+ saved_at: new Date().toISOString(),
238
+ };
239
+ await saveState(this.serverName, state);
240
+ }
241
+
242
+ // --- redirectToAuthorization ---
243
+
244
+ async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
245
+ if (this._onAuthRequired) {
246
+ await this._onAuthRequired(authorizationUrl);
247
+ } else {
248
+ // Fallback: just log it
249
+ console.error(
250
+ `[pi-mcp] OAuth authorization required for "${this.serverName}".`,
251
+ `\n Open this URL in your browser: ${authorizationUrl.toString()}`,
252
+ );
253
+ }
254
+ }
255
+
256
+ // --- PKCE code verifier ---
257
+
258
+ async saveCodeVerifier(codeVerifier: string): Promise<void> {
259
+ const state = await loadState(this.serverName);
260
+ state.codeVerifier = codeVerifier;
261
+ await saveState(this.serverName, state);
262
+ }
263
+
264
+ async codeVerifier(): Promise<string> {
265
+ const state = await loadState(this.serverName);
266
+ if (!state.codeVerifier) {
267
+ throw new Error(`[pi-mcp] No PKCE code verifier found for "${this.serverName}"`);
268
+ }
269
+ return state.codeVerifier;
270
+ }
271
+
272
+ // --- Discovery state caching ---
273
+
274
+ async saveDiscoveryState(discState: OAuthDiscoveryState): Promise<void> {
275
+ const state = await loadState(this.serverName);
276
+ state.discoveryState = discState;
277
+ await saveState(this.serverName, state);
278
+ }
279
+
280
+ async discoveryState(): Promise<OAuthDiscoveryState | undefined> {
281
+ const state = await loadState(this.serverName);
282
+ return state.discoveryState;
283
+ }
284
+
285
+ // --- OAuth state parameter (CSRF protection) ---
286
+
287
+ /**
288
+ * Set the OAuth state parameter before calling auth().
289
+ * This should be called with a cryptographically random value.
290
+ */
291
+ setState(state: string): void {
292
+ this._oauthState = state;
293
+ }
294
+
295
+ /**
296
+ * Returns the OAuth state parameter for CSRF protection.
297
+ * This is called by the SDK's auth() function when building the authorization URL.
298
+ * Returns empty string if no state has been set (no CSRF protection).
299
+ */
300
+ async state(): Promise<string> {
301
+ return this._oauthState || "";
302
+ }
303
+
304
+ // --- Credential invalidation ---
305
+
306
+ async invalidateCredentials(scope: "all" | "client" | "tokens" | "verifier" | "discovery"): Promise<void> {
307
+ const state = await loadState(this.serverName);
308
+ switch (scope) {
309
+ case "all":
310
+ state.clientInfo = undefined;
311
+ state.tokens = undefined;
312
+ state.codeVerifier = undefined;
313
+ state.discoveryState = undefined;
314
+ break;
315
+ case "client":
316
+ state.clientInfo = undefined;
317
+ break;
318
+ case "tokens":
319
+ state.tokens = undefined;
320
+ break;
321
+ case "verifier":
322
+ state.codeVerifier = undefined;
323
+ break;
324
+ case "discovery":
325
+ state.discoveryState = undefined;
326
+ break;
327
+ }
328
+ await saveState(this.serverName, state);
329
+ }
330
330
  }
331
331
 
332
332
  // ─── Public Helpers ────────────────────────────────────────────────────────────
@@ -336,26 +336,26 @@ export class McpOAuthProvider implements OAuthClientProvider {
336
336
  * Returns null if no auth state file exists at all.
337
337
  */
338
338
  export async function getAuthStatus(serverName: string): Promise<{
339
- hasTokens: boolean;
340
- hasClientInfo: boolean;
341
- savedAt: string | undefined;
342
- scope: string | undefined;
339
+ hasTokens: boolean;
340
+ hasClientInfo: boolean;
341
+ savedAt: string | undefined;
342
+ scope: string | undefined;
343
343
  } | null> {
344
- const state = await loadState(serverName);
345
- if (
346
- state.clientInfo === undefined &&
347
- state.tokens === undefined &&
348
- state.codeVerifier === undefined &&
349
- state.discoveryState === undefined
350
- ) {
351
- return null;
352
- }
353
- return {
354
- hasTokens: state.tokens !== undefined,
355
- hasClientInfo: state.clientInfo !== undefined,
356
- savedAt: state.tokens?.saved_at,
357
- scope: state.tokens?.scope,
358
- };
344
+ const state = await loadState(serverName);
345
+ if (
346
+ state.clientInfo === undefined &&
347
+ state.tokens === undefined &&
348
+ state.codeVerifier === undefined &&
349
+ state.discoveryState === undefined
350
+ ) {
351
+ return null;
352
+ }
353
+ return {
354
+ hasTokens: state.tokens !== undefined,
355
+ hasClientInfo: state.clientInfo !== undefined,
356
+ savedAt: state.tokens?.saved_at,
357
+ scope: state.tokens?.scope,
358
+ };
359
359
  }
360
360
 
361
361
  /**
@@ -363,5 +363,5 @@ export async function getAuthStatus(serverName: string): Promise<{
363
363
  * Used to force re-authorization on next connection.
364
364
  */
365
365
  export async function resetAuth(serverName: string): Promise<void> {
366
- await unlink(statePath(serverName)).catch(() => {});
366
+ await unlink(statePath(serverName)).catch(() => {});
367
367
  }