pi-soly 1.9.3 → 1.11.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 (104) hide show
  1. package/ask/index.ts +12 -11
  2. package/ask/picker.ts +356 -76
  3. package/ask/prompt.ts +6 -2
  4. package/ask/tests/picker.test.ts +273 -82
  5. package/codemap.ts +276 -0
  6. package/hotreload.ts +239 -0
  7. package/init.ts +302 -0
  8. package/mcp/CHANGELOG.md +384 -0
  9. package/mcp/LICENSE +21 -0
  10. package/mcp/OAUTH.md +355 -0
  11. package/mcp/README.md +410 -0
  12. package/mcp/__tests__/agent-dir-paths.upstream-test.ts +80 -0
  13. package/mcp/__tests__/cli.upstream-test.ts +97 -0
  14. package/mcp/__tests__/commands-onboarding.upstream-test.ts +157 -0
  15. package/mcp/__tests__/config.upstream-test.ts +303 -0
  16. package/mcp/__tests__/consent-manager.upstream-test.ts +151 -0
  17. package/mcp/__tests__/direct-tools-auto-auth.upstream-test.ts +218 -0
  18. package/mcp/__tests__/direct-tools.upstream-test.ts +299 -0
  19. package/mcp/__tests__/elicitation-handler.upstream-test.ts +346 -0
  20. package/mcp/__tests__/elicitation-sdk-integration.upstream-test.ts +140 -0
  21. package/mcp/__tests__/errors.upstream-test.ts +218 -0
  22. package/mcp/__tests__/fixtures/elicitation-server.mjs +98 -0
  23. package/mcp/__tests__/host-html-template.upstream-test.ts +278 -0
  24. package/mcp/__tests__/index-lifecycle.upstream-test.ts +484 -0
  25. package/mcp/__tests__/init-elicitation.upstream-test.ts +86 -0
  26. package/mcp/__tests__/interactive-visualizer-server.upstream-test.ts +28 -0
  27. package/mcp/__tests__/logger.upstream-test.ts +175 -0
  28. package/mcp/__tests__/mcp-auth-flow-client-credentials.upstream-test.ts +612 -0
  29. package/mcp/__tests__/mcp-auth-storage.upstream-test.ts +47 -0
  30. package/mcp/__tests__/mcp-callback-server-unref.upstream-test.ts +264 -0
  31. package/mcp/__tests__/mcp-oauth-provider.upstream-test.ts +216 -0
  32. package/mcp/__tests__/mcp-panel-auth.upstream-test.ts +206 -0
  33. package/mcp/__tests__/mcp-panel-exclude-tools.upstream-test.ts +69 -0
  34. package/mcp/__tests__/mcp-panel-keybindings.upstream-test.ts +186 -0
  35. package/mcp/__tests__/npx-resolver.upstream-test.ts +54 -0
  36. package/mcp/__tests__/oauth-handler.upstream-test.ts +77 -0
  37. package/mcp/__tests__/onboarding-state.upstream-test.ts +52 -0
  38. package/mcp/__tests__/package-manifest.upstream-test.ts +22 -0
  39. package/mcp/__tests__/proxy-modes-auto-auth.upstream-test.ts +253 -0
  40. package/mcp/__tests__/proxy-modes-discovery.upstream-test.ts +84 -0
  41. package/mcp/__tests__/proxy-modes-manual-auth.upstream-test.ts +91 -0
  42. package/mcp/__tests__/proxy-modes-ui-messages.upstream-test.ts +72 -0
  43. package/mcp/__tests__/sampling-handler.upstream-test.ts +285 -0
  44. package/mcp/__tests__/server-manager-http-auth.upstream-test.ts +144 -0
  45. package/mcp/__tests__/server-manager-sampling.upstream-test.ts +236 -0
  46. package/mcp/__tests__/tool-metadata.upstream-test.ts +111 -0
  47. package/mcp/__tests__/tool-result-renderer.upstream-test.ts +147 -0
  48. package/mcp/__tests__/ui-integration.upstream-test.ts +551 -0
  49. package/mcp/__tests__/ui-resource-handler.upstream-test.ts +303 -0
  50. package/mcp/__tests__/ui-server.upstream-test.ts +967 -0
  51. package/mcp/__tests__/ui-session-messages.upstream-test.ts +72 -0
  52. package/mcp/__tests__/ui-streaming.upstream-test.ts +543 -0
  53. package/mcp/agent-dir.ts +20 -0
  54. package/mcp/app-bridge.bundle.js +67 -0
  55. package/mcp/cli.js +184 -0
  56. package/mcp/commands.ts +422 -0
  57. package/mcp/config.ts +666 -0
  58. package/mcp/consent-manager.ts +64 -0
  59. package/mcp/direct-tools.ts +439 -0
  60. package/mcp/elicitation-handler.ts +347 -0
  61. package/mcp/errors.ts +219 -0
  62. package/mcp/glimpse-ui.ts +80 -0
  63. package/mcp/host-html-template.ts +427 -0
  64. package/mcp/index.ts +362 -0
  65. package/mcp/init.ts +362 -0
  66. package/mcp/lifecycle.ts +93 -0
  67. package/mcp/logger.ts +169 -0
  68. package/mcp/mcp-auth-flow.ts +559 -0
  69. package/mcp/mcp-auth-flow.upstream-test.ts +259 -0
  70. package/mcp/mcp-auth.ts +302 -0
  71. package/mcp/mcp-auth.upstream-test.ts +373 -0
  72. package/mcp/mcp-callback-server.ts +372 -0
  73. package/mcp/mcp-callback-server.upstream-test.ts +416 -0
  74. package/mcp/mcp-oauth-provider.ts +369 -0
  75. package/mcp/mcp-oauth-provider.upstream-test.ts +518 -0
  76. package/mcp/mcp-panel.ts +829 -0
  77. package/mcp/mcp-setup-panel.ts +580 -0
  78. package/mcp/metadata-cache.ts +201 -0
  79. package/mcp/notify.ts +111 -0
  80. package/mcp/npx-resolver.ts +424 -0
  81. package/mcp/oauth-handler.ts +57 -0
  82. package/mcp/onboarding-state.ts +68 -0
  83. package/mcp/package.json +106 -0
  84. package/mcp/panel-keys.ts +37 -0
  85. package/mcp/proxy-modes.ts +949 -0
  86. package/mcp/resource-tools.ts +17 -0
  87. package/mcp/sampling-handler.ts +268 -0
  88. package/mcp/server-manager.ts +545 -0
  89. package/mcp/state.ts +41 -0
  90. package/mcp/tool-metadata.ts +216 -0
  91. package/mcp/tool-registrar.ts +46 -0
  92. package/mcp/tool-result-renderer.ts +161 -0
  93. package/mcp/types.ts +448 -0
  94. package/mcp/ui-resource-handler.ts +146 -0
  95. package/mcp/ui-server.ts +623 -0
  96. package/mcp/ui-session.ts +386 -0
  97. package/mcp/ui-stream-types.ts +89 -0
  98. package/mcp/utils.ts +129 -0
  99. package/mcp/vitest.config.ts +14 -0
  100. package/migrate.ts +258 -0
  101. package/notification.ts +218 -0
  102. package/notifications-log.ts +83 -0
  103. package/package.json +20 -3
  104. package/status.ts +140 -0
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Tests for mcp-auth-flow.ts - OAuth flow using MCP SDK
3
+ */
4
+
5
+ import { describe, it, before, after } from "node:test"
6
+ import assert from "node:assert"
7
+ import { existsSync, rmSync, mkdirSync } from "fs"
8
+ import { join } from "path"
9
+ import { tmpdir } from "os"
10
+ import { randomBytes } from "crypto"
11
+
12
+ // Set up isolated temp directory for tests
13
+ const TEST_DIR = join(tmpdir(), `mcp-oauth-test-${randomBytes(4).toString('hex')}`)
14
+ process.env.MCP_OAUTH_DIR = TEST_DIR
15
+
16
+ import {
17
+ authenticate,
18
+ startAuth,
19
+ completeAuth,
20
+ getAuthStatus,
21
+ removeAuth,
22
+ supportsOAuth,
23
+ extractOAuthConfig,
24
+ initializeOAuth,
25
+ shutdownOAuth,
26
+ type AuthStatus,
27
+ } from "./mcp-auth-flow.ts"
28
+ import { isCallbackServerRunning } from "./mcp-callback-server.ts"
29
+ import { updateTokens, clearAllCredentials } from "./mcp-auth.ts"
30
+ import type { ServerEntry } from "./types.ts"
31
+
32
+ describe("mcp-auth-flow", () => {
33
+ before(() => {
34
+ // Ensure clean state
35
+ try {
36
+ if (existsSync(TEST_DIR)) {
37
+ rmSync(TEST_DIR, { recursive: true, force: true })
38
+ }
39
+ mkdirSync(TEST_DIR, { recursive: true })
40
+ } catch {
41
+ // Ignore cleanup errors
42
+ }
43
+ })
44
+
45
+ after(async () => {
46
+ // Shutdown OAuth and clean up
47
+ await shutdownOAuth()
48
+ try {
49
+ if (existsSync(TEST_DIR)) {
50
+ rmSync(TEST_DIR, { recursive: true, force: true })
51
+ }
52
+ } catch {
53
+ // Ignore cleanup errors
54
+ }
55
+ })
56
+
57
+ describe("supportsOAuth", () => {
58
+ it("should return true for OAuth HTTP server", () => {
59
+ const definition: ServerEntry = {
60
+ url: "https://api.example.com/mcp",
61
+ }
62
+ assert.strictEqual(supportsOAuth(definition), true)
63
+ })
64
+
65
+ it("should return false for bearer auth", () => {
66
+ const definition: ServerEntry = {
67
+ url: "https://api.example.com/mcp",
68
+ auth: "bearer",
69
+ }
70
+ assert.strictEqual(supportsOAuth(definition), false)
71
+ })
72
+
73
+ it("should return false for stdio server", () => {
74
+ const definition: ServerEntry = {
75
+ command: "npx",
76
+ args: ["-y", "@example/mcp-server"],
77
+ }
78
+ assert.strictEqual(supportsOAuth(definition), false)
79
+ })
80
+
81
+ it("should return false when no URL", () => {
82
+ const definition: ServerEntry = {}
83
+ assert.strictEqual(supportsOAuth(definition), false)
84
+ })
85
+ })
86
+
87
+ describe("getAuthStatus", () => {
88
+ it("should return 'not_authenticated' when no tokens", async () => {
89
+ const status = await getAuthStatus("status-test-none")
90
+ assert.strictEqual(status, "not_authenticated")
91
+ })
92
+
93
+ it("should return 'authenticated' when tokens exist and not expired", async () => {
94
+ await updateTokens("status-test-ok", {
95
+ accessToken: "token",
96
+ expiresAt: Date.now() / 1000 + 3600, // 1 hour from now
97
+ })
98
+
99
+ const status = await getAuthStatus("status-test-ok")
100
+ assert.strictEqual(status, "authenticated")
101
+ })
102
+
103
+ it("should return 'expired' when tokens are expired", async () => {
104
+ await updateTokens("status-test-expired", {
105
+ accessToken: "token",
106
+ expiresAt: Date.now() / 1000 - 3600, // 1 hour ago
107
+ })
108
+
109
+ const status = await getAuthStatus("status-test-expired")
110
+ assert.strictEqual(status, "expired")
111
+ })
112
+ })
113
+
114
+ describe("removeAuth", () => {
115
+ it("should remove all credentials", async () => {
116
+ await updateTokens("remove-test", { accessToken: "token" })
117
+
118
+ await removeAuth("remove-test")
119
+
120
+ const status = await getAuthStatus("remove-test")
121
+ assert.strictEqual(status, "not_authenticated")
122
+ })
123
+ })
124
+
125
+ describe("initializeOAuth / shutdownOAuth", () => {
126
+ it("should not start callback server on initialize", async () => {
127
+ await shutdownOAuth()
128
+ await initializeOAuth()
129
+ assert.strictEqual(isCallbackServerRunning(), false)
130
+ })
131
+
132
+ it("should stop callback server on shutdown", async () => {
133
+ await initializeOAuth()
134
+ await shutdownOAuth()
135
+ assert.strictEqual(isCallbackServerRunning(), false)
136
+ })
137
+ })
138
+
139
+ describe("authenticate / completeAuth", () => {
140
+ it("should throw if no server URL provided", async () => {
141
+ await assert.rejects(
142
+ async () => await authenticate("no-url-test", ""),
143
+ /Invalid URL/
144
+ )
145
+ })
146
+
147
+ it("should reject malformed OAuth redirectUri values", async () => {
148
+ await assert.rejects(
149
+ async () => await startAuth("bad-redirect", "https://api.example.com/mcp", {
150
+ url: "https://api.example.com/mcp",
151
+ auth: "oauth",
152
+ oauth: { redirectUri: "not a url" },
153
+ }),
154
+ /Invalid OAuth redirectUri/
155
+ )
156
+ })
157
+
158
+ it("should reject non-local OAuth redirectUri values", async () => {
159
+ await assert.rejects(
160
+ async () => await startAuth("remote-redirect", "https://api.example.com/mcp", {
161
+ url: "https://api.example.com/mcp",
162
+ auth: "oauth",
163
+ oauth: { redirectUri: "https://example.com:3118/callback" },
164
+ }),
165
+ /localhost or loopback/
166
+ )
167
+ })
168
+
169
+ it("should reject OAuth redirectUri values without an explicit port", async () => {
170
+ await assert.rejects(
171
+ async () => await startAuth("no-port-redirect", "https://api.example.com/mcp", {
172
+ url: "https://api.example.com/mcp",
173
+ auth: "oauth",
174
+ oauth: { redirectUri: "http://localhost/callback" },
175
+ }),
176
+ /explicit numeric port/
177
+ )
178
+ })
179
+
180
+ it("should reject blank OAuth redirectUri values", async () => {
181
+ await assert.rejects(
182
+ async () => await startAuth("blank-redirect", "https://api.example.com/mcp", {
183
+ url: "https://api.example.com/mcp",
184
+ auth: "oauth",
185
+ oauth: { redirectUri: " " },
186
+ }),
187
+ /redirectUri must not be empty/
188
+ )
189
+ })
190
+
191
+ it("should reject non-string OAuth redirectUri values", async () => {
192
+ await assert.rejects(
193
+ async () => await startAuth("typed-redirect", "https://api.example.com/mcp", {
194
+ url: "https://api.example.com/mcp",
195
+ auth: "oauth",
196
+ oauth: { redirectUri: 3118 as unknown as string },
197
+ }),
198
+ /redirectUri must be a string/
199
+ )
200
+ })
201
+
202
+ it("should reject OAuth redirectUri values with fragments", async () => {
203
+ await assert.rejects(
204
+ async () => await startAuth("fragment-redirect", "https://api.example.com/mcp", {
205
+ url: "https://api.example.com/mcp",
206
+ auth: "oauth",
207
+ oauth: { redirectUri: "http://localhost:3118/callback#fragment" },
208
+ }),
209
+ /redirectUri must not include a fragment/
210
+ )
211
+ })
212
+
213
+ it("should reject OAuth redirectUri values with username or password", async () => {
214
+ await assert.rejects(
215
+ async () => await startAuth("credential-redirect", "https://api.example.com/mcp", {
216
+ url: "https://api.example.com/mcp",
217
+ auth: "oauth",
218
+ oauth: { redirectUri: "http://user:pass@localhost:3118/callback" },
219
+ }),
220
+ /redirectUri must not include username or password/
221
+ )
222
+ })
223
+
224
+ it("should reject non-string OAuth clientName and clientUri values", () => {
225
+ assert.throws(
226
+ () => extractOAuthConfig({
227
+ url: "https://api.example.com/mcp",
228
+ auth: "oauth",
229
+ oauth: { clientName: 123 as unknown as string },
230
+ }),
231
+ /clientName must be a string/
232
+ )
233
+ assert.throws(
234
+ () => extractOAuthConfig({
235
+ url: "https://api.example.com/mcp",
236
+ auth: "oauth",
237
+ oauth: { clientUri: 123 as unknown as string },
238
+ }),
239
+ /clientUri must be a string/
240
+ )
241
+ })
242
+
243
+ it("should trim OAuth redirectUri and client metadata values", () => {
244
+ const config = extractOAuthConfig({
245
+ url: "https://api.example.com/mcp",
246
+ auth: "oauth",
247
+ oauth: {
248
+ redirectUri: " http://localhost:3118/callback ",
249
+ clientName: " Custom MCP ",
250
+ clientUri: " https://example.com/custom ",
251
+ },
252
+ })
253
+
254
+ assert.strictEqual(config.redirectUri, "http://localhost:3118/callback")
255
+ assert.strictEqual(config.clientName, "Custom MCP")
256
+ assert.strictEqual(config.clientUri, "https://example.com/custom")
257
+ })
258
+ })
259
+ })
@@ -0,0 +1,302 @@
1
+ /**
2
+ * MCP Auth Storage Module
3
+ *
4
+ * Handles secure storage of OAuth credentials, tokens, client information,
5
+ * and PKCE state for MCP servers.
6
+ *
7
+ * Token storage location: $MCP_OAUTH_DIR/sha256-<server-hash>/tokens.json when set,
8
+ * otherwise <Pi agent dir>/mcp-oauth/sha256-<server-hash>/tokens.json
9
+ */
10
+
11
+ import { createHash } from 'crypto';
12
+ import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from 'fs';
13
+ import { join } from 'path';
14
+ import { getAgentPath } from './agent-dir.ts';
15
+
16
+ /** OAuth token storage format */
17
+ export interface StoredTokens {
18
+ accessToken: string;
19
+ refreshToken?: string;
20
+ expiresAt?: number; // Unix timestamp in seconds
21
+ scope?: string;
22
+ }
23
+
24
+ /** OAuth client information from dynamic or static registration */
25
+ export interface StoredClientInfo {
26
+ clientId: string;
27
+ clientSecret?: string;
28
+ clientIdIssuedAt?: number;
29
+ clientSecretExpiresAt?: number;
30
+ redirectUris?: string[];
31
+ }
32
+
33
+ /** Complete auth entry for a server */
34
+ export interface AuthEntry {
35
+ tokens?: StoredTokens;
36
+ clientInfo?: StoredClientInfo;
37
+ codeVerifier?: string;
38
+ oauthState?: string;
39
+ serverUrl?: string; // Track the URL these credentials are for
40
+ }
41
+
42
+ // Base directory for auth storage - can be overridden via env var for testing
43
+ function getAuthBaseDir(): string {
44
+ const override = process.env.MCP_OAUTH_DIR?.trim();
45
+ return override ? override : getAgentPath('mcp-oauth');
46
+ }
47
+
48
+ /**
49
+ * Get the server-specific directory path.
50
+ */
51
+ function getServerDir(serverName: string): string {
52
+ if (typeof serverName !== 'string') {
53
+ throw new Error(`Invalid MCP server name: ${JSON.stringify(serverName)}`);
54
+ }
55
+ const storageKey = createHash('sha256').update(serverName, 'utf8').digest('hex');
56
+ return join(getAuthBaseDir(), `sha256-${storageKey}`);
57
+ }
58
+
59
+ /**
60
+ * Get the tokens file path for a server.
61
+ */
62
+ export function getAuthEntryFilePath(serverName: string): string {
63
+ return join(getServerDir(serverName), 'tokens.json');
64
+ }
65
+
66
+ /**
67
+ * Ensure the server directory exists with secure permissions.
68
+ */
69
+ function ensureServerDir(serverName: string): void {
70
+ const dir = getServerDir(serverName);
71
+ if (!existsSync(dir)) {
72
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Read the auth entry for a server from disk.
78
+ * Returns undefined if file doesn't exist.
79
+ */
80
+ function readAuthEntry(serverName: string): AuthEntry | undefined {
81
+ const filePath = getAuthEntryFilePath(serverName);
82
+ try {
83
+ if (!existsSync(filePath)) {
84
+ return undefined;
85
+ }
86
+ const data = readFileSync(filePath, 'utf-8');
87
+ return JSON.parse(data) as AuthEntry;
88
+ } catch (error) {
89
+ console.error(`Failed to read auth entry for ${serverName}:`, error);
90
+ return undefined;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Write the auth entry for a server to disk with secure permissions.
96
+ */
97
+ function writeAuthEntry(serverName: string, entry: AuthEntry): void {
98
+ ensureServerDir(serverName);
99
+ const filePath = getAuthEntryFilePath(serverName);
100
+ writeFileSync(filePath, JSON.stringify(entry, null, 2), { mode: 0o600 });
101
+ }
102
+
103
+ /**
104
+ * Get auth entry for a server.
105
+ */
106
+ export function getAuthEntry(serverName: string): AuthEntry | undefined {
107
+ return readAuthEntry(serverName);
108
+ }
109
+
110
+ /**
111
+ * Get auth entry and validate it's for the correct URL.
112
+ * Returns undefined if URL has changed (credentials are invalid).
113
+ */
114
+ export function getAuthForUrl(serverName: string, serverUrl: string): AuthEntry | undefined {
115
+ const entry = getAuthEntry(serverName);
116
+ if (!entry) return undefined;
117
+
118
+ // If no serverUrl is stored, this is from an old version - consider it invalid
119
+ if (!entry.serverUrl) return undefined;
120
+
121
+ // If URL has changed, credentials are invalid
122
+ if (entry.serverUrl !== serverUrl) return undefined;
123
+
124
+ return entry;
125
+ }
126
+
127
+ /**
128
+ * Save auth entry for a server.
129
+ */
130
+ export function saveAuthEntry(serverName: string, entry: AuthEntry, serverUrl?: string): void {
131
+ // Always update serverUrl if provided
132
+ if (serverUrl) {
133
+ entry.serverUrl = serverUrl;
134
+ }
135
+ writeAuthEntry(serverName, entry);
136
+ }
137
+
138
+ /**
139
+ * Remove auth entry for a server.
140
+ * Also removes the server directory if empty.
141
+ */
142
+ export function removeAuthEntry(serverName: string): void {
143
+ try {
144
+ const filePath = getAuthEntryFilePath(serverName);
145
+ if (existsSync(filePath)) {
146
+ writeFileSync(filePath, '{}', { mode: 0o600 });
147
+ }
148
+ // Try to remove the directory
149
+ const dir = getServerDir(serverName);
150
+ if (existsSync(dir)) {
151
+ try {
152
+ rmSync(dir, { recursive: true });
153
+ } catch {
154
+ // Directory may not be empty, ignore
155
+ }
156
+ }
157
+ } catch (error) {
158
+ console.error(`Failed to remove auth entry for ${serverName}:`, error);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Update tokens for a server.
164
+ */
165
+ export function updateTokens(
166
+ serverName: string,
167
+ tokens: StoredTokens,
168
+ serverUrl?: string
169
+ ): void {
170
+ const entry = getAuthEntry(serverName) ?? {};
171
+ if (serverUrl && entry.serverUrl !== serverUrl) {
172
+ delete entry.clientInfo;
173
+ delete entry.codeVerifier;
174
+ delete entry.oauthState;
175
+ }
176
+ entry.tokens = tokens;
177
+ saveAuthEntry(serverName, entry, serverUrl);
178
+ }
179
+
180
+ /**
181
+ * Update client info for a server.
182
+ */
183
+ export function updateClientInfo(
184
+ serverName: string,
185
+ clientInfo: StoredClientInfo,
186
+ serverUrl?: string
187
+ ): void {
188
+ const entry = getAuthEntry(serverName) ?? {};
189
+ if (serverUrl && entry.serverUrl !== serverUrl) {
190
+ delete entry.tokens;
191
+ delete entry.codeVerifier;
192
+ delete entry.oauthState;
193
+ }
194
+ entry.clientInfo = clientInfo;
195
+ saveAuthEntry(serverName, entry, serverUrl);
196
+ }
197
+
198
+ /**
199
+ * Update code verifier for a server.
200
+ */
201
+ export function updateCodeVerifier(serverName: string, codeVerifier: string, serverUrl?: string): void {
202
+ const entry = getAuthEntry(serverName) ?? {};
203
+ if (serverUrl && entry.serverUrl !== serverUrl) {
204
+ delete entry.tokens;
205
+ delete entry.clientInfo;
206
+ delete entry.oauthState;
207
+ }
208
+ entry.codeVerifier = codeVerifier;
209
+ saveAuthEntry(serverName, entry, serverUrl);
210
+ }
211
+
212
+ /**
213
+ * Clear code verifier for a server.
214
+ */
215
+ export function clearCodeVerifier(serverName: string): void {
216
+ const entry = getAuthEntry(serverName);
217
+ if (entry) {
218
+ delete entry.codeVerifier;
219
+ saveAuthEntry(serverName, entry);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Update OAuth state for a server.
225
+ */
226
+ export function updateOAuthState(serverName: string, state: string, serverUrl?: string): void {
227
+ const entry = getAuthEntry(serverName) ?? {};
228
+ if (serverUrl && entry.serverUrl !== serverUrl) {
229
+ delete entry.tokens;
230
+ delete entry.clientInfo;
231
+ delete entry.codeVerifier;
232
+ }
233
+ entry.oauthState = state;
234
+ saveAuthEntry(serverName, entry, serverUrl);
235
+ }
236
+
237
+ /**
238
+ * Get OAuth state for a server.
239
+ */
240
+ export function getOAuthState(serverName: string): string | undefined {
241
+ const entry = getAuthEntry(serverName);
242
+ return entry?.oauthState;
243
+ }
244
+
245
+ /**
246
+ * Clear OAuth state for a server.
247
+ */
248
+ export function clearOAuthState(serverName: string): void {
249
+ const entry = getAuthEntry(serverName);
250
+ if (entry) {
251
+ delete entry.oauthState;
252
+ saveAuthEntry(serverName, entry);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Check if stored tokens are expired.
258
+ * Returns null if no tokens exist, false if no expiry or not expired, true if expired.
259
+ */
260
+ export function isTokenExpired(serverName: string): boolean | null {
261
+ const entry = getAuthEntry(serverName);
262
+ if (!entry?.tokens) return null;
263
+ if (!entry.tokens.expiresAt) return false;
264
+ return entry.tokens.expiresAt < Date.now() / 1000;
265
+ }
266
+
267
+ /**
268
+ * Check if a server has stored tokens.
269
+ */
270
+ export function hasStoredTokens(serverName: string): boolean {
271
+ const entry = getAuthEntry(serverName);
272
+ return !!entry?.tokens;
273
+ }
274
+
275
+ /**
276
+ * Clear all credentials for a server.
277
+ */
278
+ export function clearAllCredentials(serverName: string): void {
279
+ removeAuthEntry(serverName);
280
+ }
281
+
282
+ /**
283
+ * Clear only client info for a server.
284
+ */
285
+ export function clearClientInfo(serverName: string): void {
286
+ const entry = getAuthEntry(serverName);
287
+ if (entry) {
288
+ delete entry.clientInfo;
289
+ saveAuthEntry(serverName, entry);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Clear only tokens for a server.
295
+ */
296
+ export function clearTokens(serverName: string): void {
297
+ const entry = getAuthEntry(serverName);
298
+ if (entry) {
299
+ delete entry.tokens;
300
+ saveAuthEntry(serverName, entry);
301
+ }
302
+ }