bestony-pi-preset 0.0.21 → 0.0.23
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/node_modules/pi-mcp-adapter/CHANGELOG.md +21 -0
- package/node_modules/pi-mcp-adapter/README.md +28 -2
- package/node_modules/pi-mcp-adapter/commands.ts +21 -2
- package/node_modules/pi-mcp-adapter/index.ts +13 -3
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +64 -3
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
- package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
- package/node_modules/pi-mcp-adapter/package.json +3 -3
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +7 -3
- package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
- package/node_modules/pi-mcp-adapter/server-manager.ts +36 -7
- package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
- package/node_modules/pi-mcp-adapter/types.ts +20 -9
- package/node_modules/pi-web-access/CHANGELOG.md +9 -0
- package/node_modules/pi-web-access/README.md +12 -10
- package/node_modules/pi-web-access/bocha.ts +221 -0
- package/node_modules/pi-web-access/curator-page.ts +5 -3
- package/node_modules/pi-web-access/curator-server.ts +2 -1
- package/node_modules/pi-web-access/gemini-search.ts +18 -4
- package/node_modules/pi-web-access/index.ts +33 -16
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/storage.ts +252 -29
- package/node_modules/pi-xai/CHANGELOG.md +6 -0
- package/node_modules/pi-xai/README.md +4 -3
- package/node_modules/pi-xai/index.ts +14 -6
- package/node_modules/pi-xai/package.json +1 -1
- package/node_modules/pi-xai/xai-config.ts +1 -0
- package/node_modules/pi-xai/xai-provider.ts +19 -0
- package/node_modules/pi-xai/xai-vision.ts +1 -1
- package/package.json +4 -4
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.24.0] - 2026-08-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Added per-server `searchKeywords` so `mcp({ search })` and `mcpScript` `tools.search` understand user-defined synonyms and aliases for tools. Keywords are keyed by tool name or glob and boost ranked and regex search only. They never appear in tool schemas, describe output, or the metadata cache. Thanks @Serisium for PR #336.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Interpolated environment placeholders in stdio server arguments. Thanks @vjik for issue #333.
|
|
17
|
+
- Kept remote `/mcp-auth` authorization links reachable before the callback input opens. Thanks @trevorleibert-mixpanel for PR #331.
|
|
18
|
+
- Cached OAuth credentials in memory and refreshed them after OAuth-backed 401 responses. Thanks @daniel-sampliner for PR #335.
|
|
19
|
+
- Sanitized MCP server-name prefixes for provider-safe tool names while preserving server resolution. Thanks @triple-dex for issue #334.
|
|
20
|
+
- Validated persisted npx resolver cache entries before reuse, including malformed and prototype-sensitive persisted keys.
|
|
21
|
+
|
|
22
|
+
## [2.23.0] - 2026-08-11
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- Added interactive callback URL pasting to `/mcp-auth` for OAuth flows running on remote or headless machines. Thanks @trevorleibert-mixpanel for PR #330.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Stopped load-time MCP initialization from printing a TUI startup error when Pi action methods are not bound yet. Thanks @21307369 for issue #327.
|
|
29
|
+
- Kept interactive OAuth authorization URLs clickable as a single terminal hyperlink. Thanks @rfccg for PR #329.
|
|
30
|
+
|
|
10
31
|
## [2.22.0] - 2026-08-11
|
|
11
32
|
|
|
12
33
|
### Added
|
|
@@ -227,6 +227,7 @@ In the configuration examples below, `30000` is illustrative only. If `requestTi
|
|
|
227
227
|
| `toolPrefix` | Override global `settings.toolPrefix` for this server (`"server"`, `"short"`, `"none"`, or `"mcp"`) |
|
|
228
228
|
| `includeTools` | `string[]` of tool names or glob patterns to expose (matches original names like `get_screenshot`, generated resource names like `read_figjam`, and prefixed names like `figma_get_screenshot`) |
|
|
229
229
|
| `excludeTools` | `string[]` of tool names or glob patterns to hide (applied after `includeTools`) |
|
|
230
|
+
| `searchKeywords` | `{ "tool-or-glob": ["keyword", ...] }` — extra keywords that boost `mcp({ search })` ranking for matching tools; never shown to the model |
|
|
230
231
|
| `debug` | Show server stderr (default: false) |
|
|
231
232
|
| `trace` | Enable metadata-only JSONL protocol tracing for this server; payloads, prompts, tool arguments/results, authorization data, and URLs are never persisted |
|
|
232
233
|
| `disabled` | Keep the server visible in config and status, but prevent connections, authentication, tools, and resource calls (only literal `true` disables it) |
|
|
@@ -265,7 +266,9 @@ The adapter owns only its client socket and closes that connection when the Pi r
|
|
|
265
266
|
|
|
266
267
|
### Remote/headless OAuth
|
|
267
268
|
|
|
268
|
-
If Pi is running on a remote server
|
|
269
|
+
If Pi is running on a remote server, `/mcp-auth <server>` shows a clickable authorization URL first. Open it in your local browser and approve access, then select **Yes** in Pi to open the callback input. The browser may fail to load the localhost callback page because localhost refers to your workstation; copy the full URL from its address bar and paste it into Pi. The authorization screen closes automatically instead when the browser can reach Pi's callback directly.
|
|
270
|
+
|
|
271
|
+
The same flow is available through the proxy tool for non-interactive clients. Persistent OAuth still requires an available OS credential store; on headless Linux that usually means an unlocked Secret Service/libsecret keyring. The adapter fails closed instead of falling back to plaintext credentials when the secure store is unavailable.
|
|
269
272
|
|
|
270
273
|
On Linux, if credential access fails because Pi inherited a revoked session keyring, the adapter uses a best-effort recovery path through `keyctl session - node <packaged helper>` so explicit re-authentication can write fresh credentials without killing a long-lived tmux server. This path requires `keyctl` and `node` on `PATH`; missing, locked, or otherwise unavailable credential stores still fail closed.
|
|
271
274
|
|
|
@@ -648,10 +651,33 @@ Prefer `.mcp.json` for project-local shared MCP config. Use `.pi/mcp.json` only
|
|
|
648
651
|
|
|
649
652
|
MCP proxy and direct-tool results render compactly by default: long text shows the first three terminal-wrapped lines plus a `Ctrl+O to expand` hint, while the full result remains available when expanded and is still returned unchanged to the model.
|
|
650
653
|
|
|
651
|
-
Search includes both MCP tools and Pi tools (from extensions). Pi tools appear first with `[pi tool]` prefix. Space-separated words are ranked by weighted matches across name, server,
|
|
654
|
+
Search includes both MCP tools and Pi tools (from extensions). Pi tools appear first with `[pi tool]` prefix. Space-separated words are ranked by weighted matches across name, server, description, and any configured `searchKeywords`, then returned one page at a time (`limit` defaults to 12). Use `details.nextOffset` for the next page. Regex search is still available with `regex: true`, but regex results are paginated without ranking.
|
|
652
655
|
|
|
653
656
|
Tool names are fuzzy-matched on hyphens and underscores — `context7_resolve_library_id` finds `context7_resolve-library-id`. When `describe` or `tool` cannot resolve a name, the result includes top suggestions so the agent can correct a typo or missing prefix in the same turn.
|
|
654
657
|
|
|
658
|
+
### Search keywords
|
|
659
|
+
|
|
660
|
+
Search uses literal matching so a tool whose name and description use different vocabulary than the query won't be found. Per-server `searchKeywords` adds extra vocabulary for matching tools:
|
|
661
|
+
|
|
662
|
+
```json
|
|
663
|
+
{
|
|
664
|
+
"mcpServers": {
|
|
665
|
+
"github": {
|
|
666
|
+
"command": "npx",
|
|
667
|
+
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
668
|
+
"searchKeywords": {
|
|
669
|
+
"search_code": ["grep"],
|
|
670
|
+
"*": ["gh"]
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
With this config, `mcp({ search: "grep" })` finds `github_search_code` even though neither its name nor description contains that word. Similarly, `mcp({ search: "gh" })` finds all tools provided by the github server.
|
|
678
|
+
|
|
679
|
+
Keys match a tool's original name, prefixed name, or a glob (`*` applies to every tool on the server) and all matching entries combine. Keywords are weighted like description text, with an extra boost when the query exactly matches a configured phrase. They affect ranked and regex search only (including `tools.search` in `mcpScript`): they never appear in tool schemas, `describe` output, direct-tool registration, or the metadata cache, and search with keywords works offline from cached metadata.
|
|
680
|
+
|
|
655
681
|
When `includeSchemas` is enabled, search and describe render common JSON Schema parameters as compact TypeScript shapes like `{ query: string; limit?: number; }`, with the older schema formatter retained as a fallback for unsupported schemas.
|
|
656
682
|
|
|
657
683
|
For HTTP servers, failed connects run a one-request shape probe that can turn opaque transport errors into setup hints such as `endpoint returned HTML (200) — this URL does not appear to speak MCP`. Healthy connections are not probed.
|
|
@@ -25,6 +25,10 @@ import { loadOnboardingState, markSetupCompleted as persistSetupCompleted, markS
|
|
|
25
25
|
import { openPath, resolveServerUrl, sanitizeTerminalText } from "./utils.ts";
|
|
26
26
|
import { isAbortError } from "./runtime-owner.ts";
|
|
27
27
|
|
|
28
|
+
function terminalHyperlink(label: string, url: string): string {
|
|
29
|
+
return `\u001B]8;;${sanitizeTerminalText(url)}\u001B\\${sanitizeTerminalText(label)}\u001B]8;;\u001B\\`;
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
export async function showStatus(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {
|
|
29
33
|
if (!ctx.hasUI) return;
|
|
30
34
|
|
|
@@ -273,11 +277,26 @@ export async function authenticateServer(
|
|
|
273
277
|
...(authStorageOptions.baseDir ? { authStorageOptions } : {}),
|
|
274
278
|
onAuthorizationUrl: (authorizationUrl) => {
|
|
275
279
|
ui.notify(
|
|
276
|
-
`Open this URL to authenticate ${serverName}:\n\n${authorizationUrl}\n\n` +
|
|
277
|
-
"After approving,
|
|
280
|
+
`Open this URL to authenticate ${serverName}:\n\n${terminalHyperlink(authorizationUrl, authorizationUrl)}\n\n` +
|
|
281
|
+
"After approving, Pi will complete automatically if the browser can reach its localhost callback. " +
|
|
282
|
+
"On a remote machine, copy the full localhost URL from the browser address bar and paste it into Pi.",
|
|
278
283
|
"info"
|
|
279
284
|
);
|
|
280
285
|
},
|
|
286
|
+
onAuthorizationInput: async (authorizationUrl, inputSignal) => {
|
|
287
|
+
const readyToPaste = await ui.confirm(
|
|
288
|
+
`Authorize ${serverName}`,
|
|
289
|
+
`Open this link in your browser:\n${terminalHyperlink(authorizationUrl, authorizationUrl)}\n\n` +
|
|
290
|
+
"After approving access, select Yes to paste the callback URL.",
|
|
291
|
+
{ signal: inputSignal },
|
|
292
|
+
);
|
|
293
|
+
if (!readyToPaste || inputSignal.aborted) return undefined;
|
|
294
|
+
return ui.input(
|
|
295
|
+
`Complete ${serverName} OAuth`,
|
|
296
|
+
"Paste the full callback URL",
|
|
297
|
+
{ signal: inputSignal },
|
|
298
|
+
);
|
|
299
|
+
},
|
|
281
300
|
...(signal ? { signal } : {}),
|
|
282
301
|
...(runtime ? { runtime } : {}),
|
|
283
302
|
});
|
|
@@ -169,13 +169,23 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
169
169
|
return resolveDirectTools(config, cache, prefix, envDirectToolOverride);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
function getActiveToolsIfReady(): string[] | undefined {
|
|
173
|
+
try {
|
|
174
|
+
return pi.getActiveTools?.();
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error instanceof Error
|
|
177
|
+
&& error.message.includes("Action methods cannot be called during extension loading")) return undefined;
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
172
182
|
function deactivateTools(toolNames: string[]): string[] {
|
|
173
183
|
if (toolNames.length === 0) return [];
|
|
174
184
|
const unregisterTool = (pi as ExtensionAPI & { unregisterTool?: (name: string) => boolean }).unregisterTool;
|
|
175
185
|
const unregistered = toolNames.filter((toolName) => unregisterTool?.(toolName) === true);
|
|
176
186
|
const fallbackNames = toolNames.filter((toolName) => !unregistered.includes(toolName));
|
|
177
187
|
const remove = new Set(toolNames);
|
|
178
|
-
const activeTools =
|
|
188
|
+
const activeTools = getActiveToolsIfReady();
|
|
179
189
|
if (!activeTools || activeTools.length === 0) {
|
|
180
190
|
for (const toolName of fallbackNames) fallbackDeactivatedTools.add(toolName);
|
|
181
191
|
return unregistered;
|
|
@@ -207,7 +217,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
207
217
|
registerDirectTool(spec);
|
|
208
218
|
registeredDirectTools.set(spec.prefixedName, fingerprint);
|
|
209
219
|
if (fallbackDeactivatedTools.delete(spec.prefixedName)) {
|
|
210
|
-
const activeTools =
|
|
220
|
+
const activeTools = getActiveToolsIfReady();
|
|
211
221
|
if (activeTools && !activeTools.includes(spec.prefixedName)) {
|
|
212
222
|
pi.setActiveTools([...activeTools, spec.prefixedName]);
|
|
213
223
|
}
|
|
@@ -849,7 +859,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
849
859
|
registerProxyTool(description);
|
|
850
860
|
return;
|
|
851
861
|
}
|
|
852
|
-
const activeTools =
|
|
862
|
+
const activeTools = getActiveToolsIfReady();
|
|
853
863
|
if (activeTools && !activeTools.includes("mcp")) {
|
|
854
864
|
pi.setActiveTools([...activeTools, "mcp"]);
|
|
855
865
|
}
|
|
@@ -49,6 +49,10 @@ export interface McpOAuthRuntime {
|
|
|
49
49
|
|
|
50
50
|
export interface AuthenticateOptions {
|
|
51
51
|
onAuthorizationUrl?: (authorizationUrl: string) => void | Promise<void>
|
|
52
|
+
onAuthorizationInput?: (
|
|
53
|
+
authorizationUrl: string,
|
|
54
|
+
signal: AbortSignal,
|
|
55
|
+
) => Promise<string | undefined>
|
|
52
56
|
authStorageOptions?: AuthStorageOptions
|
|
53
57
|
signal?: AbortSignal
|
|
54
58
|
runtime?: McpOAuthRuntime
|
|
@@ -568,6 +572,53 @@ export function parseAuthorizationCodeInput(input: string, expectedState?: strin
|
|
|
568
572
|
return parseAuthorizationRedirectInput(input, expectedState).code
|
|
569
573
|
}
|
|
570
574
|
|
|
575
|
+
type AuthorizationResponse = {
|
|
576
|
+
input: AuthorizationCodeInput
|
|
577
|
+
source: "callback" | "manual"
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Wait for either the localhost callback or a manually pasted redirect URL.
|
|
582
|
+
* The manual input prompt is dismissed as soon as either path finishes.
|
|
583
|
+
*/
|
|
584
|
+
export async function waitForAuthorizationResponse(
|
|
585
|
+
callbackPromise: Promise<AuthorizationCodeInput>,
|
|
586
|
+
authorizationUrl: string,
|
|
587
|
+
expectedState: string,
|
|
588
|
+
onAuthorizationInput?: AuthenticateOptions["onAuthorizationInput"],
|
|
589
|
+
signal?: AbortSignal,
|
|
590
|
+
): Promise<AuthorizationResponse> {
|
|
591
|
+
if (!onAuthorizationInput) {
|
|
592
|
+
return {
|
|
593
|
+
input: await abortable(callbackPromise, signal),
|
|
594
|
+
source: "callback",
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const inputController = new AbortController()
|
|
599
|
+
try {
|
|
600
|
+
const response = await abortable(Promise.race([
|
|
601
|
+
callbackPromise.then((input) => ({ input, source: "callback" as const })),
|
|
602
|
+
onAuthorizationInput(authorizationUrl, inputController.signal).then((input) => ({
|
|
603
|
+
input,
|
|
604
|
+
source: "manual" as const,
|
|
605
|
+
})),
|
|
606
|
+
]), signal)
|
|
607
|
+
|
|
608
|
+
if (response.source === "callback") return response
|
|
609
|
+
if (!response.input?.trim()) throw new Error("OAuth authentication cancelled")
|
|
610
|
+
if (!getSearchParamsFromInput(response.input.trim())) {
|
|
611
|
+
throw new Error("Paste the full OAuth callback URL, including its code and state parameters")
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
input: parseAuthorizationRedirectInput(response.input, expectedState),
|
|
615
|
+
source: "manual",
|
|
616
|
+
}
|
|
617
|
+
} finally {
|
|
618
|
+
inputController.abort()
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
571
622
|
/**
|
|
572
623
|
* Complete OAuth authentication from manual user input.
|
|
573
624
|
*/
|
|
@@ -728,12 +779,22 @@ export async function authenticate(
|
|
|
728
779
|
console.warn(`MCP Auth: Failed to open browser for ${serverName}; waiting for manual callback`, { error })
|
|
729
780
|
}
|
|
730
781
|
|
|
731
|
-
const
|
|
782
|
+
const authorizationResponse = await waitForAuthorizationResponse(
|
|
783
|
+
callbackPromise,
|
|
784
|
+
authorizationUrl,
|
|
785
|
+
oauthState,
|
|
786
|
+
options.onAuthorizationInput,
|
|
787
|
+
signal,
|
|
788
|
+
)
|
|
789
|
+
if (authorizationResponse.source === "manual") {
|
|
790
|
+
cancelPendingCallback(oauthState)
|
|
791
|
+
}
|
|
732
792
|
|
|
733
|
-
// The callback server accepted only the flow-local reserved state.
|
|
793
|
+
// The callback server accepted only the flow-local reserved state. Manual
|
|
794
|
+
// input is checked against the same state before token exchange.
|
|
734
795
|
throwIfAborted(signal)
|
|
735
796
|
|
|
736
|
-
return await completeAuth(serverName,
|
|
797
|
+
return await completeAuth(serverName, authorizationResponse.input, {
|
|
737
798
|
...options,
|
|
738
799
|
...(signal ? { signal } : {}),
|
|
739
800
|
runtime,
|
|
@@ -37,6 +37,7 @@ const KEYRING_RECOVERY_KEYCTL_ENV = 'PI_MCP_ADAPTER_KEYRING_RECOVERY_KEYCTL';
|
|
|
37
37
|
const KEYRING_RECOVERY_NODE_ENV = 'PI_MCP_ADAPTER_KEYRING_RECOVERY_NODE';
|
|
38
38
|
const KEYRING_RECOVERY_HELPER_ENV = 'PI_MCP_ADAPTER_KEYRING_RECOVERY_HELPER';
|
|
39
39
|
const TEST_LINUX_KEYRING_RECOVERY_ENV = 'PI_MCP_ADAPTER_TEST_LINUX_KEYRING_RECOVERY';
|
|
40
|
+
const AUTH_CACHE_DISABLED_ENV = 'PI_MCP_ADAPTER_DISABLE_AUTH_CACHE';
|
|
40
41
|
const KEYRING_RECOVERY_TIMEOUT_MS = 10_000;
|
|
41
42
|
const AUTH_CHUNK_MANIFEST_KEY = '__piMcpAdapterOAuthChunked';
|
|
42
43
|
|
|
@@ -148,8 +149,20 @@ interface AuthEntryChunkManifest {
|
|
|
148
149
|
let KeyringEntryClass: KeyringEntryConstructor | undefined;
|
|
149
150
|
const memoryAuthEntries = new Map<string, string>();
|
|
150
151
|
|
|
152
|
+
let testAuthSecretStoreReadCount = 0;
|
|
153
|
+
const authEntryCache = new Map<string, AuthEntry | undefined>();
|
|
154
|
+
|
|
155
|
+
function isAuthEntryCacheEnabled(): boolean {
|
|
156
|
+
return process.env[AUTH_CACHE_DISABLED_ENV] !== '1';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function cloneAuthEntry(entry: AuthEntry | undefined): AuthEntry | undefined {
|
|
160
|
+
return entry === undefined ? undefined : structuredClone(entry);
|
|
161
|
+
}
|
|
162
|
+
|
|
151
163
|
const memoryAuthSecretStore: AuthSecretStore = {
|
|
152
164
|
read(account) {
|
|
165
|
+
testAuthSecretStoreReadCount++;
|
|
153
166
|
return memoryAuthEntries.get(account);
|
|
154
167
|
},
|
|
155
168
|
write(account, payload) {
|
|
@@ -175,6 +188,7 @@ const keyringAuthSecretStore: AuthSecretStore = {
|
|
|
175
188
|
/** Mimics the Windows Credential Manager per-value ceiling for tests. */
|
|
176
189
|
const sizeLimitedAuthSecretStore: AuthSecretStore = {
|
|
177
190
|
read(account) {
|
|
191
|
+
testAuthSecretStoreReadCount++;
|
|
178
192
|
return memoryAuthEntries.get(account);
|
|
179
193
|
},
|
|
180
194
|
write(account, payload) {
|
|
@@ -190,6 +204,7 @@ const sizeLimitedAuthSecretStore: AuthSecretStore = {
|
|
|
190
204
|
|
|
191
205
|
const unavailableAuthSecretStore: AuthSecretStore = {
|
|
192
206
|
read() {
|
|
207
|
+
testAuthSecretStoreReadCount++;
|
|
193
208
|
throw new Error('simulated secure credential store unavailable');
|
|
194
209
|
},
|
|
195
210
|
write() {
|
|
@@ -206,6 +221,7 @@ function createKeyRevokedTestError(): Error {
|
|
|
206
221
|
|
|
207
222
|
const keyRevokedAuthSecretStore: AuthSecretStore = {
|
|
208
223
|
read() {
|
|
224
|
+
testAuthSecretStoreReadCount++;
|
|
209
225
|
throw createKeyRevokedTestError();
|
|
210
226
|
},
|
|
211
227
|
write() {
|
|
@@ -218,6 +234,16 @@ const keyRevokedAuthSecretStore: AuthSecretStore = {
|
|
|
218
234
|
|
|
219
235
|
export function resetTestAuthSecretStore(): void {
|
|
220
236
|
memoryAuthEntries.clear();
|
|
237
|
+
authEntryCache.clear();
|
|
238
|
+
testAuthSecretStoreReadCount = 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function resetAuthEntryCache(): void {
|
|
242
|
+
authEntryCache.clear();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function getTestAuthSecretStoreReadCount(): number {
|
|
246
|
+
return testAuthSecretStoreReadCount;
|
|
221
247
|
}
|
|
222
248
|
|
|
223
249
|
export function getTestAuthSecretStoreEntries(): [string, string][] {
|
|
@@ -649,6 +675,19 @@ function writeSecureAuthEntryToStore(store: AuthSecretStore, serverName: string,
|
|
|
649
675
|
error,
|
|
650
676
|
);
|
|
651
677
|
}
|
|
678
|
+
|
|
679
|
+
publishAuthEntryToCache(serverName, payload);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function publishAuthEntryToCache(serverName: string, payload: string): void {
|
|
683
|
+
if (!isAuthEntryCacheEnabled()) return;
|
|
684
|
+
// Cache the same normalized shape a fresh persistent-store read returns.
|
|
685
|
+
const normalized = toAuthEntry(JSON.parse(payload) as unknown);
|
|
686
|
+
if (!normalized) {
|
|
687
|
+
authEntryCache.delete(serverName);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
authEntryCache.set(serverName, cloneAuthEntry(normalized));
|
|
652
691
|
}
|
|
653
692
|
|
|
654
693
|
function writeSecureAuthEntry(serverName: string, entry: AuthEntry): void {
|
|
@@ -704,12 +743,23 @@ function readAuthEntry(
|
|
|
704
743
|
options?: AuthStorageOptions,
|
|
705
744
|
behavior: { migrateLegacy?: boolean } = {},
|
|
706
745
|
): AuthEntry | undefined {
|
|
746
|
+
// Status-only reads deliberately bypass the cache because they do not
|
|
747
|
+
// migrate legacy entries.
|
|
748
|
+
const cacheable = behavior.migrateLegacy !== false && isAuthEntryCacheEnabled();
|
|
749
|
+
if (cacheable && authEntryCache.has(serverName)) {
|
|
750
|
+
return cloneAuthEntry(authEntryCache.get(serverName));
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
let entry: AuthEntry | undefined;
|
|
707
754
|
try {
|
|
708
|
-
|
|
755
|
+
entry = readAuthEntryFromStore(getAuthSecretStore(), serverName, options, behavior);
|
|
709
756
|
} catch (error) {
|
|
710
757
|
if (!shouldAttemptLinuxKeyringRecovery(error)) throw error;
|
|
711
|
-
|
|
758
|
+
entry = readAuthEntryFromStore(linuxKeyringRecoveryAuthSecretStore, serverName, options, behavior);
|
|
712
759
|
}
|
|
760
|
+
|
|
761
|
+
if (cacheable) authEntryCache.set(serverName, cloneAuthEntry(entry));
|
|
762
|
+
return entry;
|
|
713
763
|
}
|
|
714
764
|
|
|
715
765
|
/**
|
|
@@ -794,9 +844,17 @@ export function removeAuthEntry(serverName: string, options?: AuthStorageOptions
|
|
|
794
844
|
if (!shouldAttemptLinuxKeyringRecovery(error)) throw error;
|
|
795
845
|
removeAuthEntryFromStore(linuxKeyringRecoveryAuthSecretStore, serverName);
|
|
796
846
|
}
|
|
847
|
+
authEntryCache.delete(serverName);
|
|
797
848
|
removeLegacyAuthEntry(serverName, options);
|
|
798
849
|
}
|
|
799
850
|
|
|
851
|
+
/**
|
|
852
|
+
* Forget a cached entry so the next ordinary read reloads secure storage.
|
|
853
|
+
*/
|
|
854
|
+
export function invalidateAuthEntryCache(serverName: string): void {
|
|
855
|
+
authEntryCache.delete(serverName);
|
|
856
|
+
}
|
|
857
|
+
|
|
800
858
|
/**
|
|
801
859
|
* Update tokens for a server.
|
|
802
860
|
*/
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// npx-resolver.ts - Resolve npx/npm exec binaries to avoid npm parent processes
|
|
2
|
-
import { existsSync, readFileSync, realpathSync, readdirSync, statSync, writeFileSync, renameSync, mkdirSync, openSync, readSync, closeSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, realpathSync, readdirSync, statSync, writeFileSync, renameSync, mkdirSync, openSync, readSync, closeSync, unlinkSync } from "node:fs";
|
|
3
3
|
import { join, dirname, extname, resolve, sep } from "node:path";
|
|
4
4
|
import { getAgentPath } from "./agent-dir.ts";
|
|
5
5
|
import { throwIfAborted } from "./abort.ts";
|
|
6
6
|
import crossSpawn from "cross-spawn";
|
|
7
7
|
|
|
8
|
-
const CACHE_VERSION =
|
|
8
|
+
const CACHE_VERSION = 2;
|
|
9
9
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
10
10
|
const EXACT_PACKAGE_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?$/;
|
|
11
11
|
|
|
@@ -53,7 +53,7 @@ export async function resolveNpxBinary(
|
|
|
53
53
|
if (!parsed) return null;
|
|
54
54
|
|
|
55
55
|
const packageSpec = parsePackageSpec(parsed.packageSpec);
|
|
56
|
-
const cacheKey = JSON.stringify([command,
|
|
56
|
+
const cacheKey = JSON.stringify([command, parsed.packageSpec, parsed.binName ?? ""]);
|
|
57
57
|
const cache = loadCache();
|
|
58
58
|
const cached = cache?.entries?.[cacheKey];
|
|
59
59
|
|
|
@@ -434,41 +434,96 @@ function getNpxCachePath(): string {
|
|
|
434
434
|
return getAgentPath("mcp-npx-cache.json");
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
function
|
|
438
|
-
const cachePath = getNpxCachePath();
|
|
437
|
+
function readNpxCachePayload(cachePath: string): unknown | null {
|
|
439
438
|
if (!existsSync(cachePath)) return null;
|
|
440
439
|
try {
|
|
441
|
-
|
|
442
|
-
if (!raw || typeof raw !== "object") return null;
|
|
443
|
-
if (raw.version !== CACHE_VERSION) return null;
|
|
444
|
-
if (!raw.entries || typeof raw.entries !== "object") return null;
|
|
445
|
-
return raw as NpxCache;
|
|
440
|
+
return JSON.parse(readFileSync(cachePath, "utf-8")) as unknown;
|
|
446
441
|
} catch {
|
|
447
442
|
return null;
|
|
448
443
|
}
|
|
449
444
|
}
|
|
450
445
|
|
|
451
|
-
function
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
446
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
447
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
448
|
+
? value as Record<string, unknown>
|
|
449
|
+
: null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function createCacheEntries(): Record<string, NpxCacheEntry> {
|
|
453
|
+
return Object.create(null) as Record<string, NpxCacheEntry>;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function toNpxCacheEntry(value: unknown): NpxCacheEntry | null {
|
|
457
|
+
const raw = asRecord(value);
|
|
458
|
+
if (!raw) return null;
|
|
459
|
+
if (typeof raw.resolvedBin !== "string") return null;
|
|
460
|
+
if (typeof raw.resolvedAt !== "number" || !Number.isFinite(raw.resolvedAt)) return null;
|
|
461
|
+
if (typeof raw.isJs !== "boolean") return null;
|
|
462
|
+
if (raw.packageVersion !== undefined && typeof raw.packageVersion !== "string") return null;
|
|
463
|
+
return {
|
|
464
|
+
resolvedBin: raw.resolvedBin,
|
|
465
|
+
resolvedAt: raw.resolvedAt,
|
|
466
|
+
...(raw.packageVersion !== undefined ? { packageVersion: raw.packageVersion } : {}),
|
|
467
|
+
isJs: raw.isJs,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function toNpxCache(value: unknown): NpxCache | null {
|
|
472
|
+
const raw = asRecord(value);
|
|
473
|
+
if (!raw || raw.version !== CACHE_VERSION) return null;
|
|
474
|
+
const rawEntries = asRecord(raw.entries);
|
|
475
|
+
if (!rawEntries) return null;
|
|
476
|
+
|
|
477
|
+
const entries = createCacheEntries();
|
|
478
|
+
for (const [key, rawEntry] of Object.entries(rawEntries)) {
|
|
479
|
+
const entry = toNpxCacheEntry(rawEntry);
|
|
480
|
+
if (entry) entries[key] = entry;
|
|
481
|
+
}
|
|
482
|
+
return { version: CACHE_VERSION, entries };
|
|
483
|
+
}
|
|
455
484
|
|
|
456
|
-
|
|
485
|
+
function clearLegacyCache(): boolean {
|
|
486
|
+
const cachePath = getNpxCachePath();
|
|
487
|
+
const raw = asRecord(readNpxCachePayload(cachePath));
|
|
488
|
+
if (raw?.version !== 1) return false;
|
|
457
489
|
try {
|
|
458
|
-
|
|
459
|
-
const existing = JSON.parse(readFileSync(cachePath, "utf-8")) as NpxCache;
|
|
460
|
-
if (existing && existing.version === CACHE_VERSION && existing.entries) {
|
|
461
|
-
merged.entries = { ...existing.entries };
|
|
462
|
-
}
|
|
463
|
-
}
|
|
490
|
+
unlinkSync(cachePath);
|
|
464
491
|
} catch {
|
|
465
|
-
|
|
492
|
+
try {
|
|
493
|
+
writeFileSync(cachePath, "", "utf-8");
|
|
494
|
+
} catch {
|
|
495
|
+
// Cache cleanup is best effort; resolution must still proceed.
|
|
496
|
+
}
|
|
466
497
|
}
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
467
500
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
501
|
+
clearLegacyCache();
|
|
502
|
+
|
|
503
|
+
function loadCache(): NpxCache | null {
|
|
504
|
+
if (clearLegacyCache()) return null;
|
|
505
|
+
|
|
506
|
+
return toNpxCache(readNpxCachePayload(getNpxCachePath()));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function saveCacheEntry(key: string, entry: NpxCacheEntry): void {
|
|
510
|
+
try {
|
|
511
|
+
const cachePath = getNpxCachePath();
|
|
512
|
+
const dir = dirname(cachePath);
|
|
513
|
+
mkdirSync(dir, { recursive: true });
|
|
514
|
+
|
|
515
|
+
const existing = toNpxCache(readNpxCachePayload(cachePath));
|
|
516
|
+
const entries = createCacheEntries();
|
|
517
|
+
if (existing) Object.assign(entries, existing.entries);
|
|
518
|
+
const merged: NpxCache = { version: CACHE_VERSION, entries };
|
|
519
|
+
|
|
520
|
+
merged.entries[key] = entry;
|
|
521
|
+
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
522
|
+
writeFileSync(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
523
|
+
renameSync(tmpPath, cachePath);
|
|
524
|
+
} catch {
|
|
525
|
+
// Cache writes are best effort; resolution must still proceed.
|
|
526
|
+
}
|
|
472
527
|
}
|
|
473
528
|
|
|
474
529
|
function safeRealpath(path: string): string {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mcp-adapter",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.24.0",
|
|
4
4
|
"description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.ts",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"test": "vitest run",
|
|
35
35
|
"test:watch": "vitest",
|
|
36
36
|
"test:coverage": "vitest run --coverage",
|
|
37
|
-
"test:oauth": "PI_MCP_ADAPTER_TEST_AUTH_STORE=memory node --import tsx --test --test-concurrency=1 oauth-public-api.test.ts mcp-auth.test.ts mcp-auth-flow.test.ts mcp-callback-server.test.ts mcp-oauth-provider.test.ts",
|
|
38
|
-
"test:oauth-provider": "PI_MCP_ADAPTER_TEST_AUTH_STORE=memory node --import tsx --test mcp-oauth-provider.test.ts",
|
|
37
|
+
"test:oauth": "PI_MCP_ADAPTER_TEST_AUTH_STORE=memory PI_MCP_ADAPTER_DISABLE_AUTH_CACHE=1 node --import tsx --test --test-concurrency=1 oauth-public-api.test.ts mcp-auth.test.ts mcp-auth-flow.test.ts mcp-callback-server.test.ts mcp-oauth-provider.test.ts",
|
|
38
|
+
"test:oauth-provider": "PI_MCP_ADAPTER_TEST_AUTH_STORE=memory PI_MCP_ADAPTER_DISABLE_AUTH_CACHE=1 node --import tsx --test mcp-oauth-provider.test.ts",
|
|
39
39
|
"test:conformance": "bash conformance/run.sh"
|
|
40
40
|
},
|
|
41
41
|
"repository": {
|
|
@@ -16,7 +16,7 @@ import { maybeStartUiSession, summarizeUiSessionResult, type UiSessionRuntime }
|
|
|
16
16
|
import { formatAuthRequiredMessage, formatMcpStatus, resolveServerUrl, truncateAtWord } from "./utils.ts";
|
|
17
17
|
import { authenticate, completeAuthFromInput, startAuth, supportsOAuth } from "./mcp-auth-flow.ts";
|
|
18
18
|
import { SessionRecoveryAuthRequiredError, withSessionRecovery } from "./session-recovery.ts";
|
|
19
|
-
import { paginate, rankSuggestions, rankToolMatches } from "./search-ranking.ts";
|
|
19
|
+
import { paginate, rankSuggestions, rankToolMatches, resolveSearchKeywords } from "./search-ranking.ts";
|
|
20
20
|
import { ensureToolCallApproved, isToolCallApprovalRequired } from "./tool-approval.ts";
|
|
21
21
|
|
|
22
22
|
type ProxyToolResult = AgentToolResult<Record<string, unknown>>;
|
|
@@ -503,11 +503,15 @@ export function executeSearch(
|
|
|
503
503
|
}
|
|
504
504
|
|
|
505
505
|
matches = [];
|
|
506
|
+
const globalPrefix = state.config.settings?.toolPrefix ?? "server";
|
|
506
507
|
for (const [serverName, metadata] of state.toolMetadata.entries()) {
|
|
507
|
-
|
|
508
|
+
const definition = state.config.mcpServers[serverName];
|
|
509
|
+
if (isServerDisabled(definition)) continue;
|
|
508
510
|
if (server && serverName !== server) continue;
|
|
509
511
|
for (const tool of metadata) {
|
|
510
|
-
|
|
512
|
+
const matched = pattern.test(tool.name) || pattern.test(tool.description)
|
|
513
|
+
|| resolveSearchKeywords(definition, tool.originalName, serverName, globalPrefix).some(keyword => pattern.test(keyword));
|
|
514
|
+
if (matched) matches.push({ server: serverName, tool, score: 0 });
|
|
511
515
|
}
|
|
512
516
|
}
|
|
513
517
|
} else if (query.trim().length === 0) {
|