pi-mcp-adapter 2.21.1 → 2.21.2
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 +7 -0
- package/config.ts +24 -13
- package/mcp-auth-flow.ts +0 -8
- package/mcp-auth.ts +120 -2
- package/package.json +1 -1
- package/proxy-modes.ts +20 -2
- package/server-manager.ts +4 -0
- package/ui-resource-handler.ts +3 -7
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.21.2] - 2026-08-09
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Reported MCP servers still connecting after a zero-result tool search, so agents retry instead of treating the result as definitive. Thanks @Leon69924 for issue #316.
|
|
14
|
+
- Rejected malformed MCP config server entries and persisted OAuth credential records at their trust boundaries, so invalid local state fails before it reaches runtime connection or token code.
|
|
15
|
+
- Sized OAuth credential chunks below the Windows Credential Manager per-value limit, so oversized OAuth records persist on Windows instead of failing at every payload size. The previous 1800-character chunk size exceeded the 1280-character ceiling, which left the chunking added in #246 ineffective on Windows. Thanks @CrazyCoder for PR #318.
|
|
16
|
+
|
|
10
17
|
## [2.21.1] - 2026-08-08
|
|
11
18
|
|
|
12
19
|
### Changed
|
package/config.ts
CHANGED
|
@@ -638,22 +638,32 @@ function readValidatedConfig(path: string, label: string): McpConfig | null {
|
|
|
638
638
|
}
|
|
639
639
|
|
|
640
640
|
function validateConfig(raw: unknown): McpConfig {
|
|
641
|
-
if (!raw
|
|
641
|
+
if (!isRecord(raw)) {
|
|
642
642
|
return { mcpServers: {} };
|
|
643
643
|
}
|
|
644
644
|
|
|
645
|
-
|
|
646
|
-
|
|
645
|
+
return {
|
|
646
|
+
mcpServers: toServerEntries(raw.mcpServers ?? raw["mcp-servers"]),
|
|
647
|
+
...(Array.isArray(raw.imports) ? { imports: raw.imports as ImportKind[] } : {}),
|
|
648
|
+
...(raw.settings !== undefined ? { settings: raw.settings as McpSettings } : {}),
|
|
649
|
+
};
|
|
650
|
+
}
|
|
647
651
|
|
|
648
|
-
|
|
649
|
-
|
|
652
|
+
function toServerEntries(servers: unknown): Record<string, ServerEntry> {
|
|
653
|
+
if (!isRecord(servers)) return {};
|
|
654
|
+
const entries: Record<string, ServerEntry> = {};
|
|
655
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
656
|
+
if (isServerEntry(entry)) entries[name] = entry;
|
|
650
657
|
}
|
|
658
|
+
return entries;
|
|
659
|
+
}
|
|
651
660
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
661
|
+
function isServerEntry(value: unknown): value is ServerEntry {
|
|
662
|
+
return isRecord(value);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
666
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
657
667
|
}
|
|
658
668
|
|
|
659
669
|
function mergeOpenCodeConfigs(base: Record<string, unknown>, next: Record<string, unknown>): Record<string, unknown> {
|
|
@@ -787,12 +797,13 @@ function extractServers(config: unknown, kind: ImportKind): Record<string, Serve
|
|
|
787
797
|
continue;
|
|
788
798
|
}
|
|
789
799
|
|
|
790
|
-
if (
|
|
791
|
-
|
|
800
|
+
if (!isRecord(entry)) continue;
|
|
801
|
+
if (kind !== "codex") {
|
|
802
|
+
mappedServers[name] = entry;
|
|
792
803
|
continue;
|
|
793
804
|
}
|
|
794
805
|
|
|
795
|
-
const mapped = { ...
|
|
806
|
+
const mapped = { ...entry };
|
|
796
807
|
const bearerTokenEnv = mapped.bearer_token_env_var;
|
|
797
808
|
const httpHeaders = mapped.http_headers;
|
|
798
809
|
const envHttpHeaders = mapped.env_http_headers;
|
package/mcp-auth-flow.ts
CHANGED
|
@@ -667,14 +667,12 @@ export async function authenticate(
|
|
|
667
667
|
}
|
|
668
668
|
|
|
669
669
|
const operation = (async (): Promise<AuthStatus> => {
|
|
670
|
-
// Start auth flow
|
|
671
670
|
const { authorizationUrl } = await startAuth(serverName, serverUrl, definition, {
|
|
672
671
|
...options,
|
|
673
672
|
...(signal ? { signal } : {}),
|
|
674
673
|
runtime,
|
|
675
674
|
})
|
|
676
675
|
|
|
677
|
-
// If no auth URL needed, already authenticated
|
|
678
676
|
if (!authorizationUrl) {
|
|
679
677
|
return "authenticated"
|
|
680
678
|
}
|
|
@@ -708,13 +706,11 @@ export async function authenticate(
|
|
|
708
706
|
console.warn(`MCP Auth: Failed to open browser for ${serverName}; waiting for manual callback`, { error })
|
|
709
707
|
}
|
|
710
708
|
|
|
711
|
-
// Wait for callback
|
|
712
709
|
const callbackResult = await abortable(callbackPromise, signal)
|
|
713
710
|
|
|
714
711
|
// The callback server accepted only the flow-local reserved state.
|
|
715
712
|
throwIfAborted(signal)
|
|
716
713
|
|
|
717
|
-
// Complete the auth
|
|
718
714
|
return await completeAuth(serverName, callbackResult, {
|
|
719
715
|
...options,
|
|
720
716
|
...(signal ? { signal } : {}),
|
|
@@ -758,25 +754,21 @@ export async function getValidToken(
|
|
|
758
754
|
const authStorageOptions = options.authStorageOptions ?? {}
|
|
759
755
|
const signal = combineAbortSignals(runtime.signal, options.signal)
|
|
760
756
|
throwIfAborted(signal)
|
|
761
|
-
// Check if we have valid tokens
|
|
762
757
|
const entry = await getAuthForUrl(serverName, serverUrl, authStorageOptions)
|
|
763
758
|
throwIfAborted(signal)
|
|
764
759
|
if (!entry?.tokens) {
|
|
765
760
|
return null
|
|
766
761
|
}
|
|
767
762
|
|
|
768
|
-
// Check expiration
|
|
769
763
|
const expired = await isTokenExpired(serverName, authStorageOptions)
|
|
770
764
|
if (expired === false) {
|
|
771
765
|
return entry.tokens
|
|
772
766
|
}
|
|
773
767
|
|
|
774
768
|
if (expired === true && entry.tokens.refreshToken) {
|
|
775
|
-
// Token is expired, try to refresh
|
|
776
769
|
console.log(`MCP Auth: Token expired for ${serverName}, attempting refresh`)
|
|
777
770
|
|
|
778
771
|
try {
|
|
779
|
-
// Create auth provider for token refresh
|
|
780
772
|
const authProvider = new McpOAuthProvider(serverName, serverUrl, {}, {
|
|
781
773
|
onRedirect: async () => {},
|
|
782
774
|
}, authStorageOptions, runtime.signal)
|
package/mcp-auth.ts
CHANGED
|
@@ -22,7 +22,16 @@ import { resolveConfiguredOAuthDir } from './config.ts';
|
|
|
22
22
|
const require = createRequire(import.meta.url);
|
|
23
23
|
const AUTH_SECRET_SERVICE = 'pi-mcp-adapter.oauth';
|
|
24
24
|
const TEST_AUTH_STORE_ENV = 'PI_MCP_ADAPTER_TEST_AUTH_STORE';
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Windows Credential Manager caps one value at CRED_MAX_CREDENTIAL_BLOB_SIZE
|
|
27
|
+
* (2560 bytes) and stores it as UTF-16, so the real ceiling is
|
|
28
|
+
* AUTH_SECRET_VALUE_LIMIT characters. Chunks must stay below that, and so must
|
|
29
|
+
* the threshold that decides whether to chunk at all, or oversized records still
|
|
30
|
+
* fail to persist on Windows.
|
|
31
|
+
*/
|
|
32
|
+
const AUTH_SECRET_CHUNK_SIZE = 1000;
|
|
33
|
+
/** Largest single value the strictest supported credential store accepts. */
|
|
34
|
+
const AUTH_SECRET_VALUE_LIMIT = 1280;
|
|
26
35
|
const KEYRING_RECOVERY_DISABLED_ENV = 'PI_MCP_ADAPTER_DISABLE_KEYRING_RECOVERY';
|
|
27
36
|
const KEYRING_RECOVERY_KEYCTL_ENV = 'PI_MCP_ADAPTER_KEYRING_RECOVERY_KEYCTL';
|
|
28
37
|
const KEYRING_RECOVERY_NODE_ENV = 'PI_MCP_ADAPTER_KEYRING_RECOVERY_NODE';
|
|
@@ -163,6 +172,22 @@ const keyringAuthSecretStore: AuthSecretStore = {
|
|
|
163
172
|
},
|
|
164
173
|
};
|
|
165
174
|
|
|
175
|
+
/** Mimics the Windows Credential Manager per-value ceiling for tests. */
|
|
176
|
+
const sizeLimitedAuthSecretStore: AuthSecretStore = {
|
|
177
|
+
read(account) {
|
|
178
|
+
return memoryAuthEntries.get(account);
|
|
179
|
+
},
|
|
180
|
+
write(account, payload) {
|
|
181
|
+
if (payload.length > AUTH_SECRET_VALUE_LIMIT) {
|
|
182
|
+
throw new Error(`Value of 'password encoded as UTF-16' is longer than the platform limit of ${AUTH_SECRET_VALUE_LIMIT * 2} chars`);
|
|
183
|
+
}
|
|
184
|
+
memoryAuthEntries.set(account, payload);
|
|
185
|
+
},
|
|
186
|
+
remove(account) {
|
|
187
|
+
memoryAuthEntries.delete(account);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
166
191
|
const unavailableAuthSecretStore: AuthSecretStore = {
|
|
167
192
|
read() {
|
|
168
193
|
throw new Error('simulated secure credential store unavailable');
|
|
@@ -205,6 +230,7 @@ export function removeTestAuthSecretStoreEntry(account: string): void {
|
|
|
205
230
|
|
|
206
231
|
function getAuthSecretStore(): AuthSecretStore {
|
|
207
232
|
if (process.env[TEST_AUTH_STORE_ENV] === 'memory') return memoryAuthSecretStore;
|
|
233
|
+
if (process.env[TEST_AUTH_STORE_ENV] === 'sizelimited') return sizeLimitedAuthSecretStore;
|
|
208
234
|
if (process.env[TEST_AUTH_STORE_ENV] === 'unavailable') return unavailableAuthSecretStore;
|
|
209
235
|
if (process.env[TEST_AUTH_STORE_ENV] === 'keyrevoked') return keyRevokedAuthSecretStore;
|
|
210
236
|
return keyringAuthSecretStore;
|
|
@@ -401,7 +427,99 @@ function parseJsonPayload(serverName: string, payload: string, source: string):
|
|
|
401
427
|
}
|
|
402
428
|
|
|
403
429
|
function parseAuthEntryPayload(serverName: string, payload: string, source: string): AuthEntry {
|
|
404
|
-
|
|
430
|
+
const parsed = parseJsonPayload(serverName, payload, source);
|
|
431
|
+
const entry = toAuthEntry(parsed);
|
|
432
|
+
if (!entry) {
|
|
433
|
+
throw new Error(`Failed to parse OAuth credentials for ${serverName} from ${source}: invalid credential shape`);
|
|
434
|
+
}
|
|
435
|
+
return entry;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function toAuthEntry(value: unknown): AuthEntry | undefined {
|
|
439
|
+
const entry = toRecord(value);
|
|
440
|
+
if (!entry) return undefined;
|
|
441
|
+
|
|
442
|
+
const codeVerifier = optionalString(entry.codeVerifier);
|
|
443
|
+
const oauthState = optionalString(entry.oauthState);
|
|
444
|
+
const serverUrl = optionalString(entry.serverUrl);
|
|
445
|
+
if (codeVerifier === null || oauthState === null || serverUrl === null) return undefined;
|
|
446
|
+
|
|
447
|
+
const tokens = entry.tokens === undefined ? undefined : toStoredTokens(entry.tokens);
|
|
448
|
+
const clientInfo = entry.clientInfo === undefined ? undefined : toStoredClientInfo(entry.clientInfo);
|
|
449
|
+
if ((entry.tokens !== undefined && !tokens) || (entry.clientInfo !== undefined && !clientInfo)) return undefined;
|
|
450
|
+
|
|
451
|
+
const authEntry: AuthEntry = {};
|
|
452
|
+
if (tokens) authEntry.tokens = tokens;
|
|
453
|
+
if (clientInfo) authEntry.clientInfo = clientInfo;
|
|
454
|
+
if (codeVerifier !== undefined) authEntry.codeVerifier = codeVerifier;
|
|
455
|
+
if (oauthState !== undefined) authEntry.oauthState = oauthState;
|
|
456
|
+
if (serverUrl !== undefined) authEntry.serverUrl = serverUrl;
|
|
457
|
+
return authEntry;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function toStoredTokens(value: unknown): StoredTokens | undefined {
|
|
461
|
+
const tokens = toRecord(value);
|
|
462
|
+
if (!tokens || typeof tokens.accessToken !== 'string') return undefined;
|
|
463
|
+
|
|
464
|
+
const refreshToken = optionalString(tokens.refreshToken);
|
|
465
|
+
const scope = optionalString(tokens.scope);
|
|
466
|
+
const issuer = optionalString(tokens.issuer);
|
|
467
|
+
const expiresAt = optionalNumber(tokens.expiresAt);
|
|
468
|
+
if (refreshToken === null || scope === null || issuer === null || expiresAt === null) return undefined;
|
|
469
|
+
|
|
470
|
+
const storedTokens: StoredTokens = { accessToken: tokens.accessToken };
|
|
471
|
+
if (refreshToken !== undefined) storedTokens.refreshToken = refreshToken;
|
|
472
|
+
if (expiresAt !== undefined) storedTokens.expiresAt = expiresAt;
|
|
473
|
+
if (scope !== undefined) storedTokens.scope = scope;
|
|
474
|
+
if (issuer !== undefined) storedTokens.issuer = issuer;
|
|
475
|
+
return storedTokens;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function toStoredClientInfo(value: unknown): StoredClientInfo | undefined {
|
|
479
|
+
const clientInfo = toRecord(value);
|
|
480
|
+
if (!clientInfo || typeof clientInfo.clientId !== 'string') return undefined;
|
|
481
|
+
|
|
482
|
+
const clientSecret = optionalString(clientInfo.clientSecret);
|
|
483
|
+
const issuer = optionalString(clientInfo.issuer);
|
|
484
|
+
const clientIdIssuedAt = optionalNumber(clientInfo.clientIdIssuedAt);
|
|
485
|
+
const clientSecretExpiresAt = optionalNumber(clientInfo.clientSecretExpiresAt);
|
|
486
|
+
const configPreRegistered = optionalBoolean(clientInfo.configPreRegistered);
|
|
487
|
+
if (clientSecret === null || issuer === null || clientIdIssuedAt === null || clientSecretExpiresAt === null || configPreRegistered === null) return undefined;
|
|
488
|
+
|
|
489
|
+
const storedClient: StoredClientInfo = { clientId: clientInfo.clientId };
|
|
490
|
+
const redirectUris = stringArray(clientInfo.redirectUris);
|
|
491
|
+
if (clientSecret !== undefined) storedClient.clientSecret = clientSecret;
|
|
492
|
+
if (clientIdIssuedAt !== undefined) storedClient.clientIdIssuedAt = clientIdIssuedAt;
|
|
493
|
+
if (clientSecretExpiresAt !== undefined) storedClient.clientSecretExpiresAt = clientSecretExpiresAt;
|
|
494
|
+
if (redirectUris !== undefined) storedClient.redirectUris = redirectUris;
|
|
495
|
+
if (issuer !== undefined) storedClient.issuer = issuer;
|
|
496
|
+
if (configPreRegistered !== undefined) storedClient.configPreRegistered = configPreRegistered;
|
|
497
|
+
return storedClient;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function toRecord(value: unknown): Record<string, unknown> | undefined {
|
|
501
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
502
|
+
? value as Record<string, unknown>
|
|
503
|
+
: undefined;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function optionalString(value: unknown): string | null | undefined {
|
|
507
|
+
if (value === undefined) return undefined;
|
|
508
|
+
return typeof value === 'string' ? value : null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function optionalNumber(value: unknown): number | null | undefined {
|
|
512
|
+
if (value === undefined) return undefined;
|
|
513
|
+
return typeof value === 'number' ? value : null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function optionalBoolean(value: unknown): boolean | null | undefined {
|
|
517
|
+
if (value === undefined) return undefined;
|
|
518
|
+
return typeof value === 'boolean' ? value : null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function stringArray(value: unknown): string[] | undefined {
|
|
522
|
+
return Array.isArray(value) && value.every(uri => typeof uri === 'string') ? value : undefined;
|
|
405
523
|
}
|
|
406
524
|
|
|
407
525
|
function isAuthEntryChunkManifest(value: unknown): value is AuthEntryChunkManifest {
|
package/package.json
CHANGED
package/proxy-modes.ts
CHANGED
|
@@ -525,10 +525,28 @@ export function executeSearch(
|
|
|
525
525
|
|
|
526
526
|
const page = paginate(matches, offset, limit);
|
|
527
527
|
if (page.total === 0) {
|
|
528
|
+
const connectingServers = server
|
|
529
|
+
? state.config.mcpServers[server] && state.manager.isConnecting(server) ? [server] : []
|
|
530
|
+
: Object.keys(state.config.mcpServers)
|
|
531
|
+
.filter(name => !isServerDisabled(state.config.mcpServers[name]) && state.manager.isConnecting(name))
|
|
532
|
+
.sort((a, b) => a.localeCompare(b));
|
|
528
533
|
const msg = server ? `No tools matching "${query}" in "${server}"` : `No tools matching "${query}"`;
|
|
534
|
+
const connectingMessage = connectingServers.length === 1
|
|
535
|
+
? ` Server "${connectingServers[0]}" is still connecting; retry in a moment.`
|
|
536
|
+
: connectingServers.length > 1
|
|
537
|
+
? ` Servers ${connectingServers.map(name => `"${name}"`).join(", ")} are still connecting; retry in a moment.`
|
|
538
|
+
: "";
|
|
529
539
|
return {
|
|
530
|
-
content: [{ type: "text" as const, text: msg }],
|
|
531
|
-
details: {
|
|
540
|
+
content: [{ type: "text" as const, text: `${msg}${connectingMessage}` }],
|
|
541
|
+
details: {
|
|
542
|
+
mode: "search",
|
|
543
|
+
matches: [],
|
|
544
|
+
count: 0,
|
|
545
|
+
hasMore: false,
|
|
546
|
+
nextOffset: null,
|
|
547
|
+
query,
|
|
548
|
+
...(connectingServers.length > 0 ? { connectingServers } : {}),
|
|
549
|
+
},
|
|
532
550
|
};
|
|
533
551
|
}
|
|
534
552
|
|
package/server-manager.ts
CHANGED
|
@@ -1030,6 +1030,10 @@ export class McpServerManager {
|
|
|
1030
1030
|
return false;
|
|
1031
1031
|
}
|
|
1032
1032
|
|
|
1033
|
+
isConnecting(name: string): boolean {
|
|
1034
|
+
return this.connectPromises.has(name);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1033
1037
|
getConnection(name: string): ServerConnection | undefined {
|
|
1034
1038
|
return this.connections.get(name);
|
|
1035
1039
|
}
|
package/ui-resource-handler.ts
CHANGED
|
@@ -179,10 +179,10 @@ function extractUiMeta(meta: Record<string, unknown> | undefined): UiResourceMet
|
|
|
179
179
|
|
|
180
180
|
const ui = isRecord(meta.ui) ? meta.ui : undefined;
|
|
181
181
|
const out: UiResourceMeta = {};
|
|
182
|
-
const openAiCsp =
|
|
182
|
+
const openAiCsp = Object.hasOwn(meta, "openai/widgetCSP")
|
|
183
183
|
? normalizeOpenAiWidgetCsp(meta["openai/widgetCSP"])
|
|
184
184
|
: undefined;
|
|
185
|
-
const hasStandardCsp = !!ui &&
|
|
185
|
+
const hasStandardCsp = !!ui && Object.hasOwn(ui, "csp");
|
|
186
186
|
const standardCspValue = hasStandardCsp ? ui.csp : undefined;
|
|
187
187
|
|
|
188
188
|
if (hasStandardCsp && !isRecord(standardCspValue)) {
|
|
@@ -196,7 +196,7 @@ function extractUiMeta(meta: Record<string, unknown> | undefined): UiResourceMet
|
|
|
196
196
|
out.csp = { ...openAiCsp, ...standardCsp };
|
|
197
197
|
if (isRecord(standardCspValue)) {
|
|
198
198
|
for (const [, standardField] of OPENAI_CSP_FIELD_MAPPINGS) {
|
|
199
|
-
if (
|
|
199
|
+
if (Object.hasOwn(standardCspValue, standardField) && !copyStringArray(standardCspValue[standardField])) {
|
|
200
200
|
delete out.csp[standardField];
|
|
201
201
|
}
|
|
202
202
|
}
|
|
@@ -245,10 +245,6 @@ function copyStringArray(value: unknown): string[] | undefined {
|
|
|
245
245
|
: undefined;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
function hasOwnProperty(record: Record<string, unknown>, property: string): boolean {
|
|
249
|
-
return Object.prototype.hasOwnProperty.call(record, property);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
248
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
253
249
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
254
250
|
}
|