@aexol/spectral 0.2.5 → 0.2.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.
Files changed (39) hide show
  1. package/dist/cli.js +10 -47
  2. package/dist/mcp/agent-dir.js +18 -0
  3. package/dist/mcp/app-bridge.bundle.js +67 -0
  4. package/dist/mcp/commands.js +263 -0
  5. package/dist/mcp/config.js +532 -0
  6. package/dist/mcp/consent-manager.js +59 -0
  7. package/dist/mcp/direct-tools.js +354 -0
  8. package/dist/mcp/errors.js +165 -0
  9. package/dist/mcp/glimpse-ui.js +67 -0
  10. package/dist/mcp/host-html-template.js +412 -0
  11. package/dist/mcp/index.js +291 -0
  12. package/dist/mcp/init.js +280 -0
  13. package/dist/mcp/lifecycle.js +79 -0
  14. package/dist/mcp/logger.js +130 -0
  15. package/dist/mcp/mcp-auth-flow.js +283 -0
  16. package/dist/mcp/mcp-auth.js +226 -0
  17. package/dist/mcp/mcp-callback-server.js +225 -0
  18. package/dist/mcp/mcp-oauth-provider.js +243 -0
  19. package/dist/mcp/mcp-panel.js +646 -0
  20. package/dist/mcp/mcp-setup-panel.js +485 -0
  21. package/dist/mcp/metadata-cache.js +158 -0
  22. package/dist/mcp/npx-resolver.js +385 -0
  23. package/dist/mcp/oauth-handler.js +54 -0
  24. package/dist/mcp/onboarding-state.js +56 -0
  25. package/dist/mcp/proxy-modes.js +714 -0
  26. package/dist/mcp/resource-tools.js +14 -0
  27. package/dist/mcp/sampling-handler.js +206 -0
  28. package/dist/mcp/server-manager.js +301 -0
  29. package/dist/mcp/state.js +1 -0
  30. package/dist/mcp/tool-metadata.js +128 -0
  31. package/dist/mcp/tool-registrar.js +43 -0
  32. package/dist/mcp/types.js +93 -0
  33. package/dist/mcp/ui-resource-handler.js +113 -0
  34. package/dist/mcp/ui-server.js +522 -0
  35. package/dist/mcp/ui-session.js +306 -0
  36. package/dist/mcp/ui-stream-types.js +58 -0
  37. package/dist/mcp/utils.js +104 -0
  38. package/dist/mcp/vitest.config.js +13 -0
  39. package/package.json +6 -3
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Centralized logging for MCP UI operations.
3
+ * Provides structured, contextual logs with levels.
4
+ */
5
+ const LEVEL_PRIORITY = {
6
+ debug: 0,
7
+ info: 1,
8
+ warn: 2,
9
+ error: 3,
10
+ };
11
+ const LEVEL_PREFIX = {
12
+ debug: "[MCP-UI:DEBUG]",
13
+ info: "[MCP-UI]",
14
+ warn: "[MCP-UI:WARN]",
15
+ error: "[MCP-UI:ERROR]",
16
+ };
17
+ class Logger {
18
+ minLevel = "info";
19
+ handlers = [];
20
+ defaultContext = {};
21
+ setLevel(level) {
22
+ this.minLevel = level;
23
+ }
24
+ setDefaultContext(context) {
25
+ this.defaultContext = context;
26
+ }
27
+ addHandler(handler) {
28
+ this.handlers.push(handler);
29
+ }
30
+ clearHandlers() {
31
+ this.handlers = [];
32
+ }
33
+ shouldLog(level) {
34
+ return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[this.minLevel];
35
+ }
36
+ emit(level, message, context, error) {
37
+ if (!this.shouldLog(level))
38
+ return;
39
+ const entry = {
40
+ level,
41
+ message,
42
+ context: { ...this.defaultContext, ...context },
43
+ error,
44
+ timestamp: new Date(),
45
+ };
46
+ // Default console output
47
+ const prefix = LEVEL_PREFIX[level];
48
+ const contextStr = formatContext(entry.context);
49
+ const fullMessage = contextStr ? `${prefix} ${message} ${contextStr}` : `${prefix} ${message}`;
50
+ if (level === "error") {
51
+ console.error(fullMessage, error ?? "");
52
+ }
53
+ else if (level === "warn") {
54
+ console.warn(fullMessage);
55
+ }
56
+ else if (level === "debug") {
57
+ console.debug(fullMessage);
58
+ }
59
+ else {
60
+ console.log(fullMessage);
61
+ }
62
+ // Custom handlers
63
+ for (const handler of this.handlers) {
64
+ try {
65
+ handler(entry);
66
+ }
67
+ catch {
68
+ // Ignore handler errors
69
+ }
70
+ }
71
+ }
72
+ debug(message, context) {
73
+ this.emit("debug", message, context);
74
+ }
75
+ info(message, context) {
76
+ this.emit("info", message, context);
77
+ }
78
+ warn(message, context) {
79
+ this.emit("warn", message, context);
80
+ }
81
+ error(message, error, context) {
82
+ this.emit("error", message, context, error);
83
+ }
84
+ /**
85
+ * Create a child logger with additional default context.
86
+ */
87
+ child(context) {
88
+ return new ChildLogger(this, context);
89
+ }
90
+ }
91
+ class ChildLogger {
92
+ parent;
93
+ context;
94
+ constructor(parent, context) {
95
+ this.parent = parent;
96
+ this.context = context;
97
+ }
98
+ debug(message, context) {
99
+ this.parent.debug(message, { ...this.context, ...context });
100
+ }
101
+ info(message, context) {
102
+ this.parent.info(message, { ...this.context, ...context });
103
+ }
104
+ warn(message, context) {
105
+ this.parent.warn(message, { ...this.context, ...context });
106
+ }
107
+ error(message, error, context) {
108
+ this.parent.error(message, error, { ...this.context, ...context });
109
+ }
110
+ child(context) {
111
+ return new ChildLogger(this.parent, { ...this.context, ...context });
112
+ }
113
+ }
114
+ function formatContext(context) {
115
+ if (!context || Object.keys(context).length === 0)
116
+ return "";
117
+ const parts = [];
118
+ for (const [key, value] of Object.entries(context)) {
119
+ if (value !== undefined && value !== null) {
120
+ parts.push(`${key}=${typeof value === "string" ? value : JSON.stringify(value)}`);
121
+ }
122
+ }
123
+ return parts.length > 0 ? `(${parts.join(", ")})` : "";
124
+ }
125
+ // Singleton instance
126
+ export const logger = new Logger();
127
+ // Enable debug mode via environment variable
128
+ if (process.env.MCP_UI_DEBUG === "1" || process.env.MCP_UI_DEBUG === "true") {
129
+ logger.setLevel("debug");
130
+ }
@@ -0,0 +1,283 @@
1
+ /**
2
+ * MCP Auth Flow
3
+ *
4
+ * High-level OAuth flow management using the MCP SDK's built-in auth functions.
5
+ */
6
+ import { auth as runSdkAuth, UnauthorizedError, } from "@modelcontextprotocol/sdk/client/auth.js";
7
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
8
+ import open from "open";
9
+ import { McpOAuthProvider } from "./mcp-oauth-provider.js";
10
+ import { ensureCallbackServer, waitForCallback, cancelPendingCallback, stopCallbackServer, } from "./mcp-callback-server.js";
11
+ import { getAuthForUrl, isTokenExpired, hasStoredTokens, clearAllCredentials, updateOAuthState, getOAuthState, clearOAuthState, } from "./mcp-auth.js";
12
+ // Track pending transports for auth completion
13
+ const pendingTransports = new Map();
14
+ // Deduplicate concurrent authenticate() calls per server.
15
+ const pendingAuthentications = new Map();
16
+ /**
17
+ * Generate a cryptographically secure random state parameter.
18
+ */
19
+ function generateState() {
20
+ return Array.from(crypto.getRandomValues(new Uint8Array(32)))
21
+ .map((b) => b.toString(16).padStart(2, "0"))
22
+ .join("");
23
+ }
24
+ /**
25
+ * Extract OAuth configuration from a ServerEntry.
26
+ */
27
+ function extractOAuthConfig(definition) {
28
+ // If oauth is explicitly false, return empty config
29
+ if (definition.oauth === false) {
30
+ return {};
31
+ }
32
+ return {
33
+ grantType: definition.oauth?.grantType,
34
+ clientId: definition.oauth?.clientId,
35
+ clientSecret: definition.oauth?.clientSecret,
36
+ scope: definition.oauth?.scope,
37
+ };
38
+ }
39
+ /**
40
+ * Start OAuth authentication flow for a server.
41
+ * Returns the authorization URL when browser authorization is required.
42
+ */
43
+ export async function startAuth(serverName, serverUrl, definition) {
44
+ const config = definition ? extractOAuthConfig(definition) : {};
45
+ if (config.grantType === "client_credentials") {
46
+ const authProvider = new McpOAuthProvider(serverName, serverUrl, config, {
47
+ onRedirect: async () => {
48
+ throw new Error("Browser redirect is not used for client_credentials flow");
49
+ },
50
+ });
51
+ const result = await runSdkAuth(authProvider, { serverUrl });
52
+ if (result !== "AUTHORIZED") {
53
+ throw new UnauthorizedError("Failed to authorize");
54
+ }
55
+ return { authorizationUrl: "" };
56
+ }
57
+ // Start the callback server.
58
+ // Pre-registered OAuth clients require an exact redirect URI, so enforce strict port binding.
59
+ await ensureCallbackServer({ strictPort: Boolean(config.clientId) });
60
+ const oauthState = generateState();
61
+ await updateOAuthState(serverName, oauthState);
62
+ let capturedUrl;
63
+ const authProvider = new McpOAuthProvider(serverName, serverUrl, config, {
64
+ onRedirect: async (url) => {
65
+ capturedUrl = url;
66
+ },
67
+ });
68
+ try {
69
+ const result = await runSdkAuth(authProvider, { serverUrl });
70
+ if (result === "AUTHORIZED") {
71
+ await clearOAuthState(serverName);
72
+ return { authorizationUrl: "" };
73
+ }
74
+ if (!capturedUrl) {
75
+ throw new UnauthorizedError("OAuth authorization URL was not provided");
76
+ }
77
+ pendingTransports.set(serverName, new StreamableHTTPClientTransport(new URL(serverUrl), { authProvider }));
78
+ return { authorizationUrl: capturedUrl.toString() };
79
+ }
80
+ catch (error) {
81
+ await clearOAuthState(serverName);
82
+ throw error;
83
+ }
84
+ }
85
+ /**
86
+ * Complete OAuth authentication with the authorization code.
87
+ */
88
+ export async function completeAuth(serverName, authorizationCode) {
89
+ const transport = pendingTransports.get(serverName);
90
+ if (!transport) {
91
+ throw new Error(`No pending OAuth flow for server: ${serverName}`);
92
+ }
93
+ try {
94
+ // Complete the auth using the transport's finishAuth method
95
+ await transport.finishAuth(authorizationCode);
96
+ return "authenticated";
97
+ }
98
+ finally {
99
+ pendingTransports.delete(serverName);
100
+ await transport.close().catch(() => { });
101
+ }
102
+ }
103
+ /**
104
+ * Perform the complete OAuth authentication flow for a server.
105
+ *
106
+ * @param serverName - The name of the MCP server
107
+ * @param serverUrl - The URL of the MCP server
108
+ * @param definition - The server definition (optional)
109
+ * @returns The final auth status
110
+ */
111
+ export async function authenticate(serverName, serverUrl, definition) {
112
+ const inFlight = pendingAuthentications.get(serverName);
113
+ if (inFlight) {
114
+ return inFlight;
115
+ }
116
+ const operation = (async () => {
117
+ // Start auth flow
118
+ const { authorizationUrl } = await startAuth(serverName, serverUrl, definition);
119
+ // If no auth URL needed, already authenticated
120
+ if (!authorizationUrl) {
121
+ return "authenticated";
122
+ }
123
+ // Get the state that was already generated and stored in startAuth()
124
+ const oauthState = await getOAuthState(serverName);
125
+ if (!oauthState) {
126
+ throw new Error("OAuth state not found - this should not happen");
127
+ }
128
+ // Register the callback BEFORE opening the browser
129
+ const callbackPromise = waitForCallback(oauthState);
130
+ try {
131
+ // Open browser
132
+ console.log(`MCP Auth: Opening browser for ${serverName}`);
133
+ try {
134
+ await open(authorizationUrl);
135
+ }
136
+ catch (error) {
137
+ console.warn(`MCP Auth: Failed to open browser for ${serverName}`, { error });
138
+ throw new Error(`Could not open browser. Please open this URL manually: ${authorizationUrl}`, { cause: error });
139
+ }
140
+ // Wait for callback
141
+ const code = await callbackPromise;
142
+ // Validate state
143
+ const storedState = await getOAuthState(serverName);
144
+ if (storedState !== oauthState) {
145
+ await clearOAuthState(serverName);
146
+ throw new Error("OAuth state mismatch - potential CSRF attack");
147
+ }
148
+ await clearOAuthState(serverName);
149
+ // Complete the auth
150
+ return await completeAuth(serverName, code);
151
+ }
152
+ catch (error) {
153
+ cancelPendingCallback(oauthState);
154
+ await clearOAuthState(serverName);
155
+ const pendingTransport = pendingTransports.get(serverName);
156
+ if (pendingTransport) {
157
+ pendingTransports.delete(serverName);
158
+ await pendingTransport.close().catch(() => { });
159
+ }
160
+ throw error;
161
+ }
162
+ })();
163
+ pendingAuthentications.set(serverName, operation);
164
+ try {
165
+ return await operation;
166
+ }
167
+ finally {
168
+ if (pendingAuthentications.get(serverName) === operation) {
169
+ pendingAuthentications.delete(serverName);
170
+ }
171
+ }
172
+ }
173
+ /**
174
+ * Get a valid access token for a server, refreshing if necessary.
175
+ *
176
+ * @param serverName - The name of the MCP server
177
+ * @param serverUrl - The URL of the MCP server
178
+ * @returns The valid tokens or null if not authenticated
179
+ */
180
+ export async function getValidToken(serverName, serverUrl) {
181
+ // Check if we have valid tokens
182
+ const entry = await getAuthForUrl(serverName, serverUrl);
183
+ if (!entry?.tokens) {
184
+ return null;
185
+ }
186
+ // Check expiration
187
+ const expired = await isTokenExpired(serverName);
188
+ if (expired === false) {
189
+ return entry.tokens;
190
+ }
191
+ if (expired === true && entry.tokens.refreshToken) {
192
+ // Token is expired, try to refresh
193
+ console.log(`MCP Auth: Token expired for ${serverName}, attempting refresh`);
194
+ try {
195
+ // Create auth provider for token refresh
196
+ const authProvider = new McpOAuthProvider(serverName, serverUrl, {}, {
197
+ onRedirect: async () => { },
198
+ });
199
+ const clientInfo = await authProvider.clientInformation();
200
+ if (!clientInfo) {
201
+ console.log(`MCP Auth: No client info for refresh for ${serverName}`);
202
+ return null;
203
+ }
204
+ const result = await runSdkAuth(authProvider, { serverUrl });
205
+ if (result !== "AUTHORIZED") {
206
+ return null;
207
+ }
208
+ const refreshed = await getAuthForUrl(serverName, serverUrl);
209
+ return refreshed?.tokens ?? null;
210
+ }
211
+ catch (error) {
212
+ console.error(`MCP Auth: Token refresh failed for ${serverName}`, { error });
213
+ return null;
214
+ }
215
+ }
216
+ // No expiration info or no refresh token, assume valid
217
+ return entry.tokens;
218
+ }
219
+ /**
220
+ * Check the authentication status for a server.
221
+ *
222
+ * @param serverName - The name of the MCP server
223
+ * @returns The current auth status
224
+ */
225
+ export async function getAuthStatus(serverName) {
226
+ const hasTokens = await hasStoredTokens(serverName);
227
+ if (!hasTokens)
228
+ return "not_authenticated";
229
+ const expired = await isTokenExpired(serverName);
230
+ return expired ? "expired" : "authenticated";
231
+ }
232
+ /**
233
+ * Remove all OAuth credentials for a server.
234
+ *
235
+ * @param serverName - The name of the MCP server
236
+ */
237
+ export async function removeAuth(serverName) {
238
+ const oauthState = await getOAuthState(serverName);
239
+ if (oauthState) {
240
+ cancelPendingCallback(oauthState);
241
+ }
242
+ const pendingTransport = pendingTransports.get(serverName);
243
+ if (pendingTransport) {
244
+ pendingTransports.delete(serverName);
245
+ await pendingTransport.close().catch(() => { });
246
+ }
247
+ clearAllCredentials(serverName);
248
+ await clearOAuthState(serverName);
249
+ console.log(`MCP Auth: Removed credentials for ${serverName}`);
250
+ }
251
+ /**
252
+ * Check if OAuth is supported for a server configuration.
253
+ * OAuth is supported for HTTP servers unless explicitly disabled.
254
+ *
255
+ * @param definition - The server definition
256
+ * @returns True if OAuth is supported
257
+ */
258
+ export function supportsOAuth(definition) {
259
+ // OAuth requires a URL
260
+ if (!definition.url)
261
+ return false;
262
+ // Explicitly disabled via auth: false or oauth: false
263
+ if (definition.auth === false)
264
+ return false;
265
+ if (definition.oauth === false)
266
+ return false;
267
+ // OAuth is enabled if auth is 'oauth' or not specified (auto-detect)
268
+ return definition.auth === "oauth" || definition.auth === undefined;
269
+ }
270
+ /**
271
+ * Initialize the OAuth system on startup.
272
+ * Starts the callback server if there are any OAuth servers configured.
273
+ */
274
+ export async function initializeOAuth() {
275
+ await ensureCallbackServer();
276
+ }
277
+ /**
278
+ * Shutdown the OAuth system.
279
+ * Stops the callback server and cancels pending auths.
280
+ */
281
+ export async function shutdownOAuth() {
282
+ await stopCallbackServer();
283
+ }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * MCP Auth Storage Module
3
+ *
4
+ * Handles secure storage of OAuth credentials, tokens, client information,
5
+ * and PKCE state for MCP servers. Maintains backward compatibility with
6
+ * per-server directory structure.
7
+ *
8
+ * Token storage location: $MCP_OAUTH_DIR/<server>/tokens.json when set,
9
+ * otherwise <Pi agent dir>/mcp-oauth/<server>/tokens.json
10
+ */
11
+ import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from 'fs';
12
+ import { join } from 'path';
13
+ import { getAgentPath } from './agent-dir.js';
14
+ // Base directory for auth storage - can be overridden via env var for testing
15
+ function getAuthBaseDir() {
16
+ const override = process.env.MCP_OAUTH_DIR?.trim();
17
+ return override ? override : getAgentPath('mcp-oauth');
18
+ }
19
+ /**
20
+ * Get the server-specific directory path.
21
+ */
22
+ function getServerDir(serverName) {
23
+ return join(getAuthBaseDir(), serverName);
24
+ }
25
+ /**
26
+ * Get the tokens file path for a server.
27
+ */
28
+ function getTokensFilePath(serverName) {
29
+ return join(getServerDir(serverName), 'tokens.json');
30
+ }
31
+ /**
32
+ * Ensure the server directory exists with secure permissions.
33
+ */
34
+ function ensureServerDir(serverName) {
35
+ const dir = getServerDir(serverName);
36
+ if (!existsSync(dir)) {
37
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
38
+ }
39
+ }
40
+ /**
41
+ * Read the auth entry for a server from disk.
42
+ * Returns undefined if file doesn't exist.
43
+ */
44
+ function readAuthEntry(serverName) {
45
+ try {
46
+ const filePath = getTokensFilePath(serverName);
47
+ if (!existsSync(filePath)) {
48
+ return undefined;
49
+ }
50
+ const data = readFileSync(filePath, 'utf-8');
51
+ return JSON.parse(data);
52
+ }
53
+ catch (error) {
54
+ console.error(`Failed to read auth entry for ${serverName}:`, error);
55
+ return undefined;
56
+ }
57
+ }
58
+ /**
59
+ * Write the auth entry for a server to disk with secure permissions.
60
+ */
61
+ function writeAuthEntry(serverName, entry) {
62
+ ensureServerDir(serverName);
63
+ const filePath = getTokensFilePath(serverName);
64
+ writeFileSync(filePath, JSON.stringify(entry, null, 2), { mode: 0o600 });
65
+ }
66
+ /**
67
+ * Get auth entry for a server.
68
+ */
69
+ export function getAuthEntry(serverName) {
70
+ return readAuthEntry(serverName);
71
+ }
72
+ /**
73
+ * Get auth entry and validate it's for the correct URL.
74
+ * Returns undefined if URL has changed (credentials are invalid).
75
+ */
76
+ export function getAuthForUrl(serverName, serverUrl) {
77
+ const entry = getAuthEntry(serverName);
78
+ if (!entry)
79
+ return undefined;
80
+ // If no serverUrl is stored, this is from an old version - consider it invalid
81
+ if (!entry.serverUrl)
82
+ return undefined;
83
+ // If URL has changed, credentials are invalid
84
+ if (entry.serverUrl !== serverUrl)
85
+ return undefined;
86
+ return entry;
87
+ }
88
+ /**
89
+ * Save auth entry for a server.
90
+ */
91
+ export function saveAuthEntry(serverName, entry, serverUrl) {
92
+ // Always update serverUrl if provided
93
+ if (serverUrl) {
94
+ entry.serverUrl = serverUrl;
95
+ }
96
+ writeAuthEntry(serverName, entry);
97
+ }
98
+ /**
99
+ * Remove auth entry for a server.
100
+ * Also removes the server directory if empty.
101
+ */
102
+ export function removeAuthEntry(serverName) {
103
+ try {
104
+ const filePath = getTokensFilePath(serverName);
105
+ if (existsSync(filePath)) {
106
+ writeFileSync(filePath, '{}', { mode: 0o600 });
107
+ }
108
+ // Try to remove the directory
109
+ const dir = getServerDir(serverName);
110
+ if (existsSync(dir)) {
111
+ try {
112
+ rmSync(dir, { recursive: true });
113
+ }
114
+ catch {
115
+ // Directory may not be empty, ignore
116
+ }
117
+ }
118
+ }
119
+ catch (error) {
120
+ console.error(`Failed to remove auth entry for ${serverName}:`, error);
121
+ }
122
+ }
123
+ /**
124
+ * Update tokens for a server.
125
+ */
126
+ export function updateTokens(serverName, tokens, serverUrl) {
127
+ const entry = getAuthEntry(serverName) ?? {};
128
+ entry.tokens = tokens;
129
+ saveAuthEntry(serverName, entry, serverUrl);
130
+ }
131
+ /**
132
+ * Update client info for a server.
133
+ */
134
+ export function updateClientInfo(serverName, clientInfo, serverUrl) {
135
+ const entry = getAuthEntry(serverName) ?? {};
136
+ entry.clientInfo = clientInfo;
137
+ saveAuthEntry(serverName, entry, serverUrl);
138
+ }
139
+ /**
140
+ * Update code verifier for a server.
141
+ */
142
+ export function updateCodeVerifier(serverName, codeVerifier) {
143
+ const entry = getAuthEntry(serverName) ?? {};
144
+ entry.codeVerifier = codeVerifier;
145
+ saveAuthEntry(serverName, entry);
146
+ }
147
+ /**
148
+ * Clear code verifier for a server.
149
+ */
150
+ export function clearCodeVerifier(serverName) {
151
+ const entry = getAuthEntry(serverName);
152
+ if (entry) {
153
+ delete entry.codeVerifier;
154
+ saveAuthEntry(serverName, entry);
155
+ }
156
+ }
157
+ /**
158
+ * Update OAuth state for a server.
159
+ */
160
+ export function updateOAuthState(serverName, state) {
161
+ const entry = getAuthEntry(serverName) ?? {};
162
+ entry.oauthState = state;
163
+ saveAuthEntry(serverName, entry);
164
+ }
165
+ /**
166
+ * Get OAuth state for a server.
167
+ */
168
+ export function getOAuthState(serverName) {
169
+ const entry = getAuthEntry(serverName);
170
+ return entry?.oauthState;
171
+ }
172
+ /**
173
+ * Clear OAuth state for a server.
174
+ */
175
+ export function clearOAuthState(serverName) {
176
+ const entry = getAuthEntry(serverName);
177
+ if (entry) {
178
+ delete entry.oauthState;
179
+ saveAuthEntry(serverName, entry);
180
+ }
181
+ }
182
+ /**
183
+ * Check if stored tokens are expired.
184
+ * Returns null if no tokens exist, false if no expiry or not expired, true if expired.
185
+ */
186
+ export function isTokenExpired(serverName) {
187
+ const entry = getAuthEntry(serverName);
188
+ if (!entry?.tokens)
189
+ return null;
190
+ if (!entry.tokens.expiresAt)
191
+ return false;
192
+ return entry.tokens.expiresAt < Date.now() / 1000;
193
+ }
194
+ /**
195
+ * Check if a server has stored tokens.
196
+ */
197
+ export function hasStoredTokens(serverName) {
198
+ const entry = getAuthEntry(serverName);
199
+ return !!entry?.tokens;
200
+ }
201
+ /**
202
+ * Clear all credentials for a server.
203
+ */
204
+ export function clearAllCredentials(serverName) {
205
+ removeAuthEntry(serverName);
206
+ }
207
+ /**
208
+ * Clear only client info for a server.
209
+ */
210
+ export function clearClientInfo(serverName) {
211
+ const entry = getAuthEntry(serverName);
212
+ if (entry) {
213
+ delete entry.clientInfo;
214
+ saveAuthEntry(serverName, entry);
215
+ }
216
+ }
217
+ /**
218
+ * Clear only tokens for a server.
219
+ */
220
+ export function clearTokens(serverName) {
221
+ const entry = getAuthEntry(serverName);
222
+ if (entry) {
223
+ delete entry.tokens;
224
+ saveAuthEntry(serverName, entry);
225
+ }
226
+ }