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 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 || typeof raw !== "object") {
641
+ if (!isRecord(raw)) {
642
642
  return { mcpServers: {} };
643
643
  }
644
644
 
645
- const obj = raw as Record<string, unknown>;
646
- const servers = obj.mcpServers ?? obj["mcp-servers"] ?? {};
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
- if (typeof servers !== "object" || servers === null || Array.isArray(servers)) {
649
- return { mcpServers: {} };
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
- return {
653
- mcpServers: servers as Record<string, ServerEntry>,
654
- ...(Array.isArray(obj.imports) ? { imports: obj.imports as ImportKind[] } : {}),
655
- ...(obj.settings !== undefined ? { settings: obj.settings as McpSettings } : {}),
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 (kind !== "codex" || !entry || typeof entry !== "object" || Array.isArray(entry)) {
791
- mappedServers[name] = entry as ServerEntry;
800
+ if (!isRecord(entry)) continue;
801
+ if (kind !== "codex") {
802
+ mappedServers[name] = entry;
792
803
  continue;
793
804
  }
794
805
 
795
- const mapped = { ...(entry as Record<string, unknown>) };
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
- const AUTH_SECRET_CHUNK_SIZE = 1800;
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
- return parseJsonPayload(serverName, payload, source) as AuthEntry;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-adapter",
3
- "version": "2.21.1",
3
+ "version": "2.21.2",
4
4
  "description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
5
5
  "type": "module",
6
6
  "types": "./index.ts",
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: { mode: "search", matches: [], count: 0, hasMore: false, nextOffset: null, query },
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
  }
@@ -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 = hasOwnProperty(meta, "openai/widgetCSP")
182
+ const openAiCsp = Object.hasOwn(meta, "openai/widgetCSP")
183
183
  ? normalizeOpenAiWidgetCsp(meta["openai/widgetCSP"])
184
184
  : undefined;
185
- const hasStandardCsp = !!ui && hasOwnProperty(ui, "csp");
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 (hasOwnProperty(standardCspValue, standardField) && !copyStringArray(standardCspValue[standardField])) {
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
  }