@oh-my-pi/pi-coding-agent 17.0.8 → 17.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +87 -1
- package/README.md +1 -1
- package/dist/cli.js +5495 -5021
- package/dist/types/async/job-manager.d.ts +37 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +41 -0
- package/dist/types/commands/bench.d.ts +15 -3
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +30 -1
- package/dist/types/config/models-config-schema.d.ts +18 -6
- package/dist/types/config/models-config.d.ts +14 -5
- package/dist/types/config/provider-globals.d.ts +2 -2
- package/dist/types/config/service-tier.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +350 -50
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/dap/client.d.ts +20 -0
- package/dist/types/exec/bash-executor.d.ts +38 -0
- package/dist/types/exec/direnv.d.ts +33 -0
- package/dist/types/export/html/args.d.ts +15 -0
- package/dist/types/export/html/index.d.ts +11 -26
- package/dist/types/export/html/web-palette.d.ts +55 -116
- package/dist/types/extensibility/extensions/loader.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +14 -1
- package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
- package/dist/types/live/controller.d.ts +40 -0
- package/dist/types/live/protocol.d.ts +85 -0
- package/dist/types/live/protocol.test.d.ts +1 -0
- package/dist/types/live/transport.d.ts +48 -0
- package/dist/types/live/visualizer.d.ts +33 -0
- package/dist/types/lsp/client.d.ts +18 -1
- package/dist/types/mcp/config-writer.test.d.ts +1 -0
- package/dist/types/mcp/manager.d.ts +6 -2
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/render.d.ts +2 -2
- package/dist/types/mcp/smithery-auth.d.ts +1 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
- package/dist/types/mcp/tool-bridge.d.ts +7 -3
- package/dist/types/mcp/types.d.ts +6 -0
- package/dist/types/memory-backend/tool-names.d.ts +2 -0
- package/dist/types/modes/components/login-dialog.d.ts +2 -2
- package/dist/types/modes/components/oauth-selector.d.ts +8 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
- package/dist/types/modes/components/settings-defs.d.ts +8 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +6 -1
- package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
- package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/context-usage.d.ts +12 -2
- package/dist/types/plan-mode/plan-files.d.ts +10 -0
- package/dist/types/registry/agent-lifecycle.d.ts +21 -9
- package/dist/types/registry/agent-registry.d.ts +11 -4
- package/dist/types/sdk.d.ts +15 -1
- package/dist/types/secrets/index.d.ts +15 -2
- package/dist/types/secrets/obfuscator.d.ts +113 -15
- package/dist/types/session/acp-permission-gate.d.ts +19 -0
- package/dist/types/session/agent-session-events.d.ts +84 -0
- package/dist/types/session/agent-session-types.d.ts +314 -0
- package/dist/types/session/agent-session.d.ts +120 -750
- package/dist/types/session/async-job-delivery.d.ts +28 -0
- package/dist/types/session/bash-runner.d.ts +75 -0
- package/dist/types/session/blob-store.d.ts +11 -1
- package/dist/types/session/blob-store.test.d.ts +1 -0
- package/dist/types/session/checkpoint-entries.d.ts +28 -0
- package/dist/types/session/eval-runner.d.ts +49 -0
- package/dist/types/session/irc-bridge.d.ts +46 -0
- package/dist/types/session/messages.d.ts +22 -0
- package/dist/types/session/model-controls.d.ts +180 -0
- package/dist/types/session/prewalk.d.ts +46 -0
- package/dist/types/session/queued-messages.d.ts +22 -0
- package/dist/types/session/redis-session-storage.d.ts +1 -0
- package/dist/types/session/retry-fallback-chains.d.ts +74 -0
- package/dist/types/session/role-models.d.ts +15 -0
- package/dist/types/session/session-advisors.d.ts +228 -0
- package/dist/types/session/session-entries.d.ts +8 -0
- package/dist/types/session/session-handoff.d.ts +70 -0
- package/dist/types/session/session-history-format.d.ts +6 -1
- package/dist/types/session/session-loader.d.ts +6 -4
- package/dist/types/session/session-maintenance.d.ts +246 -0
- package/dist/types/session/session-manager.d.ts +41 -0
- package/dist/types/session/session-memory.d.ts +56 -0
- package/dist/types/session/session-provider-boundary.d.ts +62 -0
- package/dist/types/session/session-stats.d.ts +48 -0
- package/dist/types/session/session-tools.d.ts +143 -0
- package/dist/types/session/session-workspace.d.ts +32 -0
- package/dist/types/session/stream-guards.d.ts +44 -0
- package/dist/types/session/todo-tracker.d.ts +56 -0
- package/dist/types/session/ttsr-coordinator.d.ts +51 -0
- package/dist/types/session/turn-recovery.d.ts +225 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +4 -1
- package/dist/types/task/output-manager.d.ts +3 -1
- package/dist/types/task/types.d.ts +14 -0
- package/dist/types/tools/approval.d.ts +1 -0
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/hub/jobs.d.ts +1 -1
- package/dist/types/tools/image-gen.d.ts +4 -4
- package/dist/types/tools/image-providers.d.ts +38 -0
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/report-tool-issue.d.ts +24 -9
- package/dist/types/tools/todo.d.ts +10 -4
- package/dist/types/tools/xdev.d.ts +5 -1
- package/dist/types/tts/streaming-player.d.ts +14 -0
- package/dist/types/tts/vocalizer.d.ts +5 -0
- package/dist/types/utils/jj.d.ts +29 -0
- package/dist/types/utils/lang-from-path.d.ts +1 -0
- package/dist/types/utils/title-generator.d.ts +30 -0
- package/dist/types/vibe/runtime.d.ts +36 -8
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +15 -5
- package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +95 -1
- package/package.json +13 -12
- package/scripts/generate-share-viewer.ts +4 -6
- package/scripts/legacy-pi-virtual-module.ts +1 -1
- package/src/advisor/__tests__/advisor.test.ts +643 -0
- package/src/advisor/runtime.ts +144 -43
- package/src/async/job-manager.ts +90 -2
- package/src/cli/args.ts +2 -0
- package/src/cli/auth-gateway-cli.ts +18 -3
- package/src/cli/bench-cli.ts +395 -29
- package/src/cli/flag-tables.ts +3 -0
- package/src/cli/gc-cli.ts +2 -2
- package/src/cli/grievances-cli.ts +2 -2
- package/src/cli/usage-cli.ts +11 -2
- package/src/collab/host.ts +3 -2
- package/src/commands/bench.ts +18 -4
- package/src/commands/launch.ts +4 -0
- package/src/config/__tests__/model-registry.test.ts +147 -0
- package/src/config/model-registry.ts +50 -2
- package/src/config/model-resolver.ts +0 -1
- package/src/config/models-config-schema.ts +15 -4
- package/src/config/provider-globals.ts +9 -9
- package/src/config/service-tier.ts +26 -1
- package/src/config/settings-schema.ts +250 -54
- package/src/config/settings.ts +81 -2
- package/src/dap/client.ts +37 -4
- package/src/edit/index.ts +49 -12
- package/src/eval/py/executor.ts +103 -33
- package/src/exec/bash-executor.ts +89 -6
- package/src/exec/direnv.ts +145 -0
- package/src/export/html/args.ts +20 -0
- package/src/export/html/index.ts +45 -47
- package/src/export/html/template.css +19 -1
- package/src/export/html/template.html +6 -0
- package/src/export/html/template.js +21 -0
- package/src/export/html/tool-views.generated.js +31 -31
- package/src/export/html/web-palette.ts +116 -132
- package/src/export/share.ts +249 -49
- package/src/extensibility/extensions/loader.ts +29 -1
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +33 -2
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
- package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +870 -311
- package/src/hindsight/state.ts +64 -6
- package/src/internal-urls/local-protocol.ts +2 -1
- package/src/internal-urls/memory-protocol.ts +2 -2
- package/src/internal-urls/registry-helpers.ts +40 -0
- package/src/internal-urls/skill-protocol.ts +2 -2
- package/src/internal-urls/ssh-protocol.ts +2 -1
- package/src/internal-urls/vault-protocol.ts +2 -1
- package/src/live/audio-worklet.txt +59 -0
- package/src/live/browser-runtime.txt +221 -0
- package/src/live/controller.ts +464 -0
- package/src/live/prompts/agent-final-message.md +3 -0
- package/src/live/prompts/live-instructions.md +23 -0
- package/src/live/protocol.test.ts +140 -0
- package/src/live/protocol.ts +233 -0
- package/src/live/transport.ts +490 -0
- package/src/live/visualizer.ts +234 -0
- package/src/lsp/client.ts +52 -9
- package/src/lsp/clients/biome-client.ts +3 -4
- package/src/lsp/index.ts +44 -11
- package/src/main.ts +9 -1
- package/src/mcp/config-writer.test.ts +43 -0
- package/src/mcp/config-writer.ts +109 -82
- package/src/mcp/manager.ts +39 -8
- package/src/mcp/oauth-discovery.ts +10 -2
- package/src/mcp/render.ts +94 -35
- package/src/mcp/smithery-auth.test.ts +29 -0
- package/src/mcp/smithery-auth.ts +3 -2
- package/src/mcp/tool-bridge.ts +107 -11
- package/src/mcp/types.ts +7 -0
- package/src/memories/index.ts +40 -20
- package/src/memory-backend/tool-names.ts +2 -0
- package/src/mnemopi/backend.ts +24 -7
- package/src/modes/acp/acp-agent.ts +56 -10
- package/src/modes/acp/acp-event-mapper.ts +8 -1
- package/src/modes/components/agent-hub.ts +27 -10
- package/src/modes/components/login-dialog.ts +14 -4
- package/src/modes/components/oauth-selector.ts +24 -7
- package/src/modes/components/plan-review-overlay.ts +350 -35
- package/src/modes/components/settings-defs.ts +28 -3
- package/src/modes/components/settings-selector.ts +178 -6
- package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
- package/src/modes/components/status-line/component.ts +123 -2
- package/src/modes/components/tool-execution.test.ts +63 -2
- package/src/modes/components/tool-execution.ts +11 -2
- package/src/modes/controllers/command-controller.ts +70 -22
- package/src/modes/controllers/event-controller.ts +111 -16
- package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -4
- package/src/modes/controllers/live-command-controller.ts +178 -0
- package/src/modes/controllers/mcp-command-controller.ts +80 -41
- package/src/modes/controllers/omfg-controller.ts +44 -40
- package/src/modes/controllers/selector-controller.ts +18 -10
- package/src/modes/controllers/session-focus-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +160 -38
- package/src/modes/rpc/rpc-client.ts +94 -3
- package/src/modes/rpc/rpc-frame.ts +164 -4
- package/src/modes/rpc/rpc-messages.ts +127 -0
- package/src/modes/rpc/rpc-mode.ts +79 -7
- package/src/modes/rpc/rpc-types.ts +34 -2
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/setup-wizard/scenes/model.ts +5 -7
- package/src/modes/setup-wizard/scenes/providers.ts +3 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
- package/src/modes/setup-wizard/scenes/theme.ts +13 -3
- package/src/modes/setup-wizard/scenes/types.ts +9 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +23 -10
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +3 -1
- package/src/modes/utils/context-usage.ts +23 -7
- package/src/modes/utils/ui-helpers.ts +10 -5
- package/src/plan-mode/plan-files.ts +40 -0
- package/src/prompts/bench/cache-prefix-chunk.md +1 -0
- package/src/prompts/bench/cache-prefix.md +3 -0
- package/src/prompts/bench/cache-suffix.md +1 -0
- package/src/prompts/goals/guided-goal-system.md +24 -3
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/project-prompt.md +8 -1
- package/src/prompts/system/subagent-async-pending.md +6 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/workflow-notice.md +48 -40
- package/src/prompts/system/xdev-mount-notice.md +4 -0
- package/src/prompts/tools/task.md +12 -2
- package/src/prompts/tools/todo.md +3 -1
- package/src/registry/agent-lifecycle.ts +116 -46
- package/src/registry/agent-registry.ts +35 -8
- package/src/registry/persisted-agents.ts +26 -2
- package/src/sdk.ts +255 -139
- package/src/secrets/index.ts +127 -4
- package/src/secrets/obfuscator.ts +2328 -105
- package/src/session/acp-permission-gate.ts +165 -0
- package/src/session/agent-session-error-log.test.ts +1 -1
- package/src/session/agent-session-events.ts +66 -0
- package/src/session/agent-session-types.ts +334 -0
- package/src/session/agent-session.ts +1910 -12213
- package/src/session/artifacts.ts +6 -5
- package/src/session/async-job-delivery.ts +74 -0
- package/src/session/bash-runner.ts +326 -0
- package/src/session/blob-store.test.ts +56 -0
- package/src/session/blob-store.ts +18 -2
- package/src/session/checkpoint-entries.ts +81 -0
- package/src/session/eval-runner.ts +212 -0
- package/src/session/indexed-session-storage.ts +9 -2
- package/src/session/irc-bridge.ts +203 -0
- package/src/session/messages.ts +263 -1
- package/src/session/model-controls.ts +714 -0
- package/src/session/prewalk.ts +252 -0
- package/src/session/queued-messages.ts +93 -0
- package/src/session/redis-session-storage.ts +51 -11
- package/src/session/retry-fallback-chains.ts +455 -0
- package/src/session/role-models.ts +85 -0
- package/src/session/session-advisors.ts +1679 -0
- package/src/session/session-context.test.ts +224 -1
- package/src/session/session-context.ts +47 -5
- package/src/session/session-entries.ts +8 -0
- package/src/session/session-handoff.ts +308 -0
- package/src/session/session-history-format.ts +20 -9
- package/src/session/session-loader.ts +10 -51
- package/src/session/session-maintenance.ts +2983 -0
- package/src/session/session-manager.ts +390 -30
- package/src/session/session-memory.ts +222 -0
- package/src/session/session-provider-boundary.ts +307 -0
- package/src/session/session-stats.ts +293 -0
- package/src/session/session-tools.ts +943 -0
- package/src/session/session-workspace.ts +53 -0
- package/src/session/stream-guards.ts +417 -0
- package/src/session/todo-tracker.ts +380 -0
- package/src/session/ttsr-coordinator.ts +496 -0
- package/src/session/turn-recovery.ts +1629 -0
- package/src/slash-commands/builtin-registry.ts +92 -11
- package/src/slash-commands/helpers/usage-report.ts +25 -4
- package/src/system-prompt.ts +41 -17
- package/src/task/executor.ts +288 -70
- package/src/task/index.ts +148 -111
- package/src/task/output-manager.ts +25 -3
- package/src/task/persisted-revive.ts +4 -3
- package/src/task/structured-subagent.ts +4 -1
- package/src/task/types.ts +16 -0
- package/src/tools/approval.ts +55 -10
- package/src/tools/bash-interactive.ts +26 -0
- package/src/tools/bash-skill-urls.ts +28 -2
- package/src/tools/bash.ts +339 -123
- package/src/tools/browser/launch.ts +9 -1
- package/src/tools/browser/tab-supervisor.ts +8 -5
- package/src/tools/browser.ts +94 -47
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +67 -8
- package/src/tools/image-gen.ts +20 -28
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +12 -0
- package/src/tools/path-utils.ts +49 -2
- package/src/tools/read.ts +156 -88
- package/src/tools/renderers.ts +7 -1
- package/src/tools/report-tool-issue.ts +79 -28
- package/src/tools/todo.ts +101 -11
- package/src/tools/vibe.ts +1 -2
- package/src/tools/write.ts +51 -1
- package/src/tools/xdev.ts +67 -3
- package/src/tts/streaming-player.ts +62 -4
- package/src/tts/vocalizer.ts +18 -1
- package/src/utils/jj.ts +125 -4
- package/src/utils/lang-from-path.ts +7 -0
- package/src/utils/title-generator.ts +137 -1
- package/src/vibe/runtime.ts +852 -72
- package/src/web/search/index.ts +3 -6
- package/src/web/search/provider.ts +31 -16
- package/src/web/search/providers/firecrawl.ts +46 -13
- package/src/web/search/providers/xai.ts +7 -1
- package/src/web/search/types.ts +8 -1
package/src/mcp/config-writer.ts
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Utilities for reading/writing .omp/mcp.json files at user or project level.
|
|
5
5
|
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
6
7
|
import * as fs from "node:fs";
|
|
7
8
|
import * as path from "node:path";
|
|
8
9
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
9
10
|
import { invalidate as invalidateFsCache } from "../capability/fs";
|
|
11
|
+
import { withFileLock } from "../config/file-lock";
|
|
10
12
|
|
|
11
13
|
import { validateServerConfig } from "./config";
|
|
12
14
|
import { MCP_CONFIG_SCHEMA_URL, type MCPConfigFile, type MCPServerConfig } from "./types";
|
|
@@ -18,6 +20,20 @@ function withSchema(config: MCPConfigFile): MCPConfigFile {
|
|
|
18
20
|
};
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Serialize a read-modify-write against one config file.
|
|
25
|
+
*
|
|
26
|
+
* Wraps {@link withFileLock} but first ensures the config's parent directory
|
|
27
|
+
* exists, because the lock directory (`${filePath}.lock`) is created with a
|
|
28
|
+
* non-recursive `mkdir` — without this the very first write (before the config
|
|
29
|
+
* file or its parent exists) would fail to acquire the lock with ENOENT.
|
|
30
|
+
*/
|
|
31
|
+
function withConfigLock<T>(filePath: string, fn: () => Promise<T>): Promise<T> {
|
|
32
|
+
return fs.promises
|
|
33
|
+
.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 })
|
|
34
|
+
.then(() => withFileLock(filePath, fn));
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
/**
|
|
22
38
|
* Read an MCP config file.
|
|
23
39
|
* Returns empty config if file doesn't exist.
|
|
@@ -45,13 +61,20 @@ export async function writeMCPConfigFile(filePath: string, config: MCPConfigFile
|
|
|
45
61
|
const dir = path.dirname(filePath);
|
|
46
62
|
await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
47
63
|
|
|
48
|
-
// Write to temp file
|
|
49
|
-
|
|
64
|
+
// Write to a per-writer temp file, then atomically rename into place. The
|
|
65
|
+
// temp name is unique (pid + random) so two concurrent writers to the same
|
|
66
|
+
// config never share one `.tmp` path and rename each other's file out from
|
|
67
|
+
// under them (which surfaced as ENOENT or a clobbered final file).
|
|
68
|
+
const tmpPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
50
69
|
const content = JSON.stringify(withSchema(config), null, 2);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
70
|
+
try {
|
|
71
|
+
await fs.promises.writeFile(tmpPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
72
|
+
// Rename to final path (atomic on most systems)
|
|
73
|
+
await fs.promises.rename(tmpPath, filePath);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
await fs.promises.rm(tmpPath, { force: true }).catch(() => {});
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
55
78
|
// Invalidate the capability fs cache so subsequent reads see the new content
|
|
56
79
|
invalidateFsCache(filePath);
|
|
57
80
|
}
|
|
@@ -97,25 +120,26 @@ export async function addMCPServer(filePath: string, name: string, config: MCPSe
|
|
|
97
120
|
throw new Error(`Invalid server config: ${errors.join("; ")}`);
|
|
98
121
|
}
|
|
99
122
|
|
|
100
|
-
//
|
|
101
|
-
|
|
123
|
+
// Serialize the read-modify-write under a per-file lock so a concurrent
|
|
124
|
+
// mutation cannot overwrite this one (lost update). The lock also guards
|
|
125
|
+
// against cross-process writers sharing the same config file.
|
|
126
|
+
await withConfigLock(filePath, async () => {
|
|
127
|
+
const existing = await readMCPConfigFile(filePath);
|
|
102
128
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// Add server
|
|
109
|
-
const updated: MCPConfigFile = {
|
|
110
|
-
...existing,
|
|
111
|
-
mcpServers: {
|
|
112
|
-
...existing.mcpServers,
|
|
113
|
-
[name]: config,
|
|
114
|
-
},
|
|
115
|
-
};
|
|
129
|
+
// Check for duplicate name
|
|
130
|
+
if (existing.mcpServers?.[name]) {
|
|
131
|
+
throw new Error(`Server "${name}" already exists in ${filePath}`);
|
|
132
|
+
}
|
|
116
133
|
|
|
117
|
-
|
|
118
|
-
|
|
134
|
+
const updated: MCPConfigFile = {
|
|
135
|
+
...existing,
|
|
136
|
+
mcpServers: {
|
|
137
|
+
...existing.mcpServers,
|
|
138
|
+
[name]: config,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
await writeMCPConfigFile(filePath, updated);
|
|
142
|
+
});
|
|
119
143
|
}
|
|
120
144
|
|
|
121
145
|
/**
|
|
@@ -137,20 +161,19 @@ export async function updateMCPServer(filePath: string, name: string, config: MC
|
|
|
137
161
|
throw new Error(`Invalid server config: ${errors.join("; ")}`);
|
|
138
162
|
}
|
|
139
163
|
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
await writeMCPConfigFile(filePath, updated);
|
|
164
|
+
// Serialize the read-modify-write (see addMCPServer).
|
|
165
|
+
await withConfigLock(filePath, async () => {
|
|
166
|
+
const existing = await readMCPConfigFile(filePath);
|
|
167
|
+
|
|
168
|
+
const updated: MCPConfigFile = {
|
|
169
|
+
...existing,
|
|
170
|
+
mcpServers: {
|
|
171
|
+
...existing.mcpServers,
|
|
172
|
+
[name]: config,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
await writeMCPConfigFile(filePath, updated);
|
|
176
|
+
});
|
|
154
177
|
}
|
|
155
178
|
|
|
156
179
|
/**
|
|
@@ -159,23 +182,21 @@ export async function updateMCPServer(filePath: string, name: string, config: MC
|
|
|
159
182
|
* @throws Error if server doesn't exist
|
|
160
183
|
*/
|
|
161
184
|
export async function removeMCPServer(filePath: string, name: string): Promise<void> {
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// Check if server exists
|
|
166
|
-
if (!existing.mcpServers?.[name]) {
|
|
167
|
-
throw new Error(`Server "${name}" not found in ${filePath}`);
|
|
168
|
-
}
|
|
185
|
+
// Serialize the read-modify-write (see addMCPServer).
|
|
186
|
+
await withConfigLock(filePath, async () => {
|
|
187
|
+
const existing = await readMCPConfigFile(filePath);
|
|
169
188
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
...existing,
|
|
174
|
-
mcpServers: remaining,
|
|
175
|
-
};
|
|
189
|
+
if (!existing.mcpServers?.[name]) {
|
|
190
|
+
throw new Error(`Server "${name}" not found in ${filePath}`);
|
|
191
|
+
}
|
|
176
192
|
|
|
177
|
-
|
|
178
|
-
|
|
193
|
+
const { [name]: _removed, ...remaining } = existing.mcpServers;
|
|
194
|
+
const updated: MCPConfigFile = {
|
|
195
|
+
...existing,
|
|
196
|
+
mcpServers: remaining,
|
|
197
|
+
};
|
|
198
|
+
await writeMCPConfigFile(filePath, updated);
|
|
199
|
+
});
|
|
179
200
|
}
|
|
180
201
|
|
|
181
202
|
/**
|
|
@@ -207,25 +228,28 @@ export async function readDisabledServers(filePath: string): Promise<string[]> {
|
|
|
207
228
|
* Add or remove a server name from the disabled servers list.
|
|
208
229
|
*/
|
|
209
230
|
export async function setServerDisabled(filePath: string, name: string, disabled: boolean): Promise<void> {
|
|
210
|
-
|
|
211
|
-
|
|
231
|
+
// Serialize the read-modify-write (see addMCPServer).
|
|
232
|
+
await withConfigLock(filePath, async () => {
|
|
233
|
+
const config = await readMCPConfigFile(filePath);
|
|
234
|
+
const current = new Set(config.disabledServers ?? []);
|
|
212
235
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
236
|
+
if (disabled) {
|
|
237
|
+
current.add(name);
|
|
238
|
+
} else {
|
|
239
|
+
current.delete(name);
|
|
240
|
+
}
|
|
218
241
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
242
|
+
const updated: MCPConfigFile = {
|
|
243
|
+
...config,
|
|
244
|
+
disabledServers: current.size > 0 ? Array.from(current).sort() : undefined,
|
|
245
|
+
};
|
|
223
246
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
247
|
+
if (!updated.disabledServers) {
|
|
248
|
+
delete updated.disabledServers;
|
|
249
|
+
}
|
|
227
250
|
|
|
228
|
-
|
|
251
|
+
await writeMCPConfigFile(filePath, updated);
|
|
252
|
+
});
|
|
229
253
|
}
|
|
230
254
|
|
|
231
255
|
/**
|
|
@@ -243,25 +267,28 @@ export async function readEnabledServers(filePath: string): Promise<string[]> {
|
|
|
243
267
|
* NOT override the `disabledServers` denylist.
|
|
244
268
|
*/
|
|
245
269
|
export async function setServerForceEnabled(filePath: string, name: string, force: boolean): Promise<void> {
|
|
246
|
-
|
|
247
|
-
|
|
270
|
+
// Serialize the read-modify-write (see addMCPServer).
|
|
271
|
+
await withConfigLock(filePath, async () => {
|
|
272
|
+
const config = await readMCPConfigFile(filePath);
|
|
273
|
+
const current = new Set(config.enabledServers ?? []);
|
|
248
274
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
275
|
+
if (force) {
|
|
276
|
+
current.add(name);
|
|
277
|
+
} else {
|
|
278
|
+
current.delete(name);
|
|
279
|
+
}
|
|
254
280
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
281
|
+
const updated: MCPConfigFile = {
|
|
282
|
+
...config,
|
|
283
|
+
enabledServers: current.size > 0 ? Array.from(current).sort() : undefined,
|
|
284
|
+
};
|
|
259
285
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
286
|
+
if (!updated.enabledServers) {
|
|
287
|
+
delete updated.enabledServers;
|
|
288
|
+
}
|
|
263
289
|
|
|
264
|
-
|
|
290
|
+
await writeMCPConfigFile(filePath, updated);
|
|
291
|
+
});
|
|
265
292
|
}
|
|
266
293
|
|
|
267
294
|
/** Paths and target state for toggling one MCP server across known config files. */
|
package/src/mcp/manager.ts
CHANGED
|
@@ -38,6 +38,7 @@ import type { MCPToolDetails } from "./tool-bridge";
|
|
|
38
38
|
import { DeferredMCPTool, MCPTool } from "./tool-bridge";
|
|
39
39
|
import type { MCPToolCache } from "./tool-cache";
|
|
40
40
|
import type {
|
|
41
|
+
MCPAuthChallenge,
|
|
41
42
|
MCPGetPromptResult,
|
|
42
43
|
MCPPrompt,
|
|
43
44
|
MCPRequestOptions,
|
|
@@ -160,6 +161,9 @@ export interface MCPDiscoverOptions {
|
|
|
160
161
|
onStatus?: (event: McpConnectionStatusEvent) => void;
|
|
161
162
|
}
|
|
162
163
|
|
|
164
|
+
/** Handles an MCP `WWW-Authenticate` challenge and returns refreshed config. */
|
|
165
|
+
export type MCPAuthHandler = (serverName: string, challenge: MCPAuthChallenge) => Promise<MCPServerConfig | undefined>;
|
|
166
|
+
|
|
163
167
|
/**
|
|
164
168
|
* MCP Server Manager.
|
|
165
169
|
*
|
|
@@ -189,6 +193,7 @@ export class MCPManager {
|
|
|
189
193
|
#pendingToolLoads = new Map<string, Promise<ToolLoadResult>>();
|
|
190
194
|
#sources = new Map<string, SourceMeta>();
|
|
191
195
|
#authStorage: AuthStorage | null = null;
|
|
196
|
+
#authHandler?: MCPAuthHandler;
|
|
192
197
|
#onNotification?: (serverName: string, method: string, params: unknown) => void;
|
|
193
198
|
#onToolsChanged?: (tools: CustomTool<TSchema, MCPToolDetails>[]) => void;
|
|
194
199
|
#onResourcesChanged?: (serverName: string, uri: string) => void;
|
|
@@ -201,7 +206,7 @@ export class MCPManager {
|
|
|
201
206
|
/** Preserved configs for reconnection after connection loss. */
|
|
202
207
|
#serverConfigs = new Map<string, MCPServerConfig>();
|
|
203
208
|
/**
|
|
204
|
-
* Timestamps of recent
|
|
209
|
+
* Timestamps of recent reconnectServer invocations per server, used by the
|
|
205
210
|
* crash-storm circuit breaker (see {@link RECONNECT_BURST_LIMIT}).
|
|
206
211
|
*/
|
|
207
212
|
#reconnectHistory = new Map<string, number[]>();
|
|
@@ -312,6 +317,11 @@ export class MCPManager {
|
|
|
312
317
|
this.#authStorage = authStorage;
|
|
313
318
|
}
|
|
314
319
|
|
|
320
|
+
/** Set the callback used to complete OAuth after a tool-level auth challenge. */
|
|
321
|
+
setAuthHandler(handler: MCPAuthHandler | undefined): void {
|
|
322
|
+
this.#authHandler = handler;
|
|
323
|
+
}
|
|
324
|
+
|
|
315
325
|
/**
|
|
316
326
|
* Discover and connect to all MCP servers from .mcp.json files.
|
|
317
327
|
* Returns tools and any connection errors.
|
|
@@ -474,7 +484,8 @@ export class MCPManager {
|
|
|
474
484
|
.then(async ({ connection, serverTools }) => {
|
|
475
485
|
if (this.#pendingToolLoads.get(name) !== toolsPromise) return;
|
|
476
486
|
this.#pendingToolLoads.delete(name);
|
|
477
|
-
const reconnect = () =>
|
|
487
|
+
const reconnect = (options?: { authChallenge?: MCPAuthChallenge }) =>
|
|
488
|
+
this.reconnectServer(name, options);
|
|
478
489
|
const customTools = MCPTool.fromTools(connection, serverTools, reconnect);
|
|
479
490
|
this.#replaceServerTools(name, customTools);
|
|
480
491
|
this.#onToolsChanged?.(this.#tools);
|
|
@@ -810,13 +821,15 @@ export class MCPManager {
|
|
|
810
821
|
* the same server share one reconnection attempt. Returns the new
|
|
811
822
|
* connection, or `null` if reconnection failed or the per-server crash
|
|
812
823
|
* burst limit (see {@link RECONNECT_BURST_LIMIT}) is exceeded.
|
|
813
|
-
*
|
|
814
824
|
* @param options.manual - When `true`, resets the crash-burst window so a
|
|
815
825
|
* user-driven retry (e.g. `/mcp reconnect`) is never blocked by an
|
|
816
826
|
* earlier storm. Defaults to `false`; the transport `onClose` callback
|
|
817
827
|
* and the per-tool-call retry path in `tool-bridge` MUST NOT set it.
|
|
818
828
|
*/
|
|
819
|
-
async reconnectServer(
|
|
829
|
+
async reconnectServer(
|
|
830
|
+
name: string,
|
|
831
|
+
options?: { manual?: boolean; authChallenge?: MCPAuthChallenge },
|
|
832
|
+
): Promise<MCPServerConnection | null> {
|
|
820
833
|
if (options?.manual) {
|
|
821
834
|
this.#reconnectHistory.delete(name);
|
|
822
835
|
}
|
|
@@ -828,7 +841,7 @@ export class MCPManager {
|
|
|
828
841
|
return null;
|
|
829
842
|
}
|
|
830
843
|
|
|
831
|
-
const attempt = this.#doReconnect(name);
|
|
844
|
+
const attempt = this.#doReconnect(name, options?.authChallenge);
|
|
832
845
|
this.#pendingReconnections.set(name, attempt);
|
|
833
846
|
return attempt.finally(() => this.#pendingReconnections.delete(name));
|
|
834
847
|
}
|
|
@@ -873,12 +886,30 @@ export class MCPManager {
|
|
|
873
886
|
return false;
|
|
874
887
|
}
|
|
875
888
|
|
|
876
|
-
async #doReconnect(name: string): Promise<MCPServerConnection | null> {
|
|
889
|
+
async #doReconnect(name: string, authChallenge?: MCPAuthChallenge): Promise<MCPServerConnection | null> {
|
|
877
890
|
const oldConnection = this.#connections.get(name);
|
|
878
|
-
|
|
891
|
+
let config = oldConnection?.config ?? this.#serverConfigs.get(name);
|
|
879
892
|
const source = this.#sources.get(name) ?? oldConnection?._source;
|
|
880
893
|
if (!config) return null;
|
|
881
894
|
|
|
895
|
+
if (authChallenge) {
|
|
896
|
+
if (!this.#authHandler) {
|
|
897
|
+
logger.error("MCP auth challenge cannot be handled; no auth handler is configured", {
|
|
898
|
+
path: `mcp:${name}`,
|
|
899
|
+
});
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
try {
|
|
903
|
+
const refreshedConfig = await this.#authHandler(name, authChallenge);
|
|
904
|
+
if (!refreshedConfig) return null;
|
|
905
|
+
config = refreshedConfig;
|
|
906
|
+
this.#serverConfigs.set(name, config);
|
|
907
|
+
} catch (error) {
|
|
908
|
+
logger.error("MCP auth challenge handling failed", { path: `mcp:${name}`, error });
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
882
913
|
logger.debug("MCP reconnecting", { path: `mcp:${name}` });
|
|
883
914
|
|
|
884
915
|
// Close the old transport without removing tools or notifying consumers.
|
|
@@ -987,7 +1018,7 @@ export class MCPManager {
|
|
|
987
1018
|
};
|
|
988
1019
|
try {
|
|
989
1020
|
const serverTools = await listTools(connection);
|
|
990
|
-
const reconnect = () => this.reconnectServer(name);
|
|
1021
|
+
const reconnect = (options?: { authChallenge?: MCPAuthChallenge }) => this.reconnectServer(name, options);
|
|
991
1022
|
const customTools = MCPTool.fromTools(connection, serverTools, reconnect);
|
|
992
1023
|
void this.toolCache?.set(name, config, serverTools);
|
|
993
1024
|
this.#replaceServerTools(name, customTools);
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
8
8
|
import type { FetchImpl } from "@oh-my-pi/pi-ai/types";
|
|
9
|
+
import { withTimeoutSignal } from "../utils/fetch-timeout";
|
|
10
|
+
|
|
11
|
+
/** Per-request abort deadline for each OAuth discovery metadata fetch. */
|
|
12
|
+
const DISCOVERY_FETCH_TIMEOUT_MS = 10_000;
|
|
9
13
|
|
|
10
14
|
export interface OAuthEndpoints {
|
|
11
15
|
authorizationUrl: string;
|
|
@@ -346,7 +350,7 @@ function readMetadataScopes(metadata: Record<string, unknown>): string | undefin
|
|
|
346
350
|
*/
|
|
347
351
|
export async function fetchResourceMetadataScopes(
|
|
348
352
|
resourceMetadataUrl: string,
|
|
349
|
-
opts?: { fetch?: FetchImpl },
|
|
353
|
+
opts?: { fetch?: FetchImpl; signal?: AbortSignal },
|
|
350
354
|
): Promise<string | undefined> {
|
|
351
355
|
const fetchImpl: FetchImpl = opts?.fetch ?? fetch;
|
|
352
356
|
try {
|
|
@@ -354,6 +358,7 @@ export async function fetchResourceMetadataScopes(
|
|
|
354
358
|
method: "GET",
|
|
355
359
|
headers: { Accept: "application/json" },
|
|
356
360
|
redirect: "follow",
|
|
361
|
+
signal: withTimeoutSignal(DISCOVERY_FETCH_TIMEOUT_MS, opts?.signal),
|
|
357
362
|
});
|
|
358
363
|
if (!resp.ok) return undefined;
|
|
359
364
|
const meta = (await resp.json()) as Record<string, unknown>;
|
|
@@ -371,7 +376,7 @@ export async function discoverOAuthEndpoints(
|
|
|
371
376
|
serverUrl: string,
|
|
372
377
|
authServerUrl?: string,
|
|
373
378
|
resourceMetadataUrl?: string,
|
|
374
|
-
opts?: { fetch?: FetchImpl; protectedResource?: string; protectedScopes?: string },
|
|
379
|
+
opts?: { fetch?: FetchImpl; protectedResource?: string; protectedScopes?: string; signal?: AbortSignal },
|
|
375
380
|
): Promise<OAuthEndpoints | null> {
|
|
376
381
|
const fetchImpl: FetchImpl = opts?.fetch ?? fetch;
|
|
377
382
|
const wellKnownPaths = [
|
|
@@ -402,6 +407,7 @@ export async function discoverOAuthEndpoints(
|
|
|
402
407
|
method: "GET",
|
|
403
408
|
headers: { Accept: "application/json" },
|
|
404
409
|
redirect: "follow",
|
|
410
|
+
signal: withTimeoutSignal(DISCOVERY_FETCH_TIMEOUT_MS, opts?.signal),
|
|
405
411
|
});
|
|
406
412
|
if (metaResp.ok) {
|
|
407
413
|
const meta = (await metaResp.json()) as Record<string, unknown>;
|
|
@@ -486,6 +492,7 @@ export async function discoverOAuthEndpoints(
|
|
|
486
492
|
method: "GET",
|
|
487
493
|
headers: { Accept: "application/json" },
|
|
488
494
|
redirect: "follow",
|
|
495
|
+
signal: withTimeoutSignal(DISCOVERY_FETCH_TIMEOUT_MS, opts?.signal),
|
|
489
496
|
});
|
|
490
497
|
|
|
491
498
|
if (response.ok) {
|
|
@@ -521,6 +528,7 @@ export async function discoverOAuthEndpoints(
|
|
|
521
528
|
fetch: fetchImpl,
|
|
522
529
|
protectedResource: discoveredProtectedResource,
|
|
523
530
|
protectedScopes: readMetadataScopes(metadata) ?? protectedScopes,
|
|
531
|
+
signal: opts?.signal,
|
|
524
532
|
});
|
|
525
533
|
if (discovered) return discovered;
|
|
526
534
|
}
|
package/src/mcp/render.ts
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* Provides structured display of MCP tool calls and results,
|
|
5
5
|
* showing args and output in JSON tree format similar to task tool.
|
|
6
6
|
*/
|
|
7
|
-
import type
|
|
7
|
+
import { type Component, Markdown } from "@oh-my-pi/pi-tui";
|
|
8
|
+
import { settings } from "../config/settings";
|
|
8
9
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
9
|
-
import type
|
|
10
|
+
import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
|
|
10
11
|
import {
|
|
11
12
|
formatArgsInline,
|
|
12
13
|
JSON_TREE_MAX_DEPTH_COLLAPSED,
|
|
@@ -48,6 +49,61 @@ export function renderMCPCall(args: Record<string, unknown>, theme: Theme, label
|
|
|
48
49
|
);
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/** Render an MCP status/args prefix followed by Markdown-aware text output. */
|
|
53
|
+
function renderMarkdownMCPResult(
|
|
54
|
+
result: { details?: MCPToolDetails; isError?: boolean },
|
|
55
|
+
trimmedOutput: string,
|
|
56
|
+
truncationWarning: string | null,
|
|
57
|
+
options: RenderResultOptions,
|
|
58
|
+
theme: Theme,
|
|
59
|
+
args?: Record<string, unknown>,
|
|
60
|
+
): Component {
|
|
61
|
+
const markdown = new Markdown(trimmedOutput, 0, 0, getMarkdownTheme(), {
|
|
62
|
+
color: text => theme.fg("toolOutput", text),
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
render(contentWidth: number): readonly string[] {
|
|
66
|
+
const lines: string[] = [];
|
|
67
|
+
const isError = result.isError ?? result.details?.isError ?? false;
|
|
68
|
+
const title = result.details ? `${result.details.serverName}/${result.details.mcpToolName}` : "MCP";
|
|
69
|
+
lines.push(
|
|
70
|
+
renderStatusLine(
|
|
71
|
+
isError ? { icon: "error", title } : { iconOverride: theme.styledSymbol("tool.mcp", "accent"), title },
|
|
72
|
+
theme,
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (options.expanded && args && Object.keys(args).length > 0) {
|
|
77
|
+
lines.push(theme.fg("dim", "Args"));
|
|
78
|
+
const tree = renderJsonTreeLines(
|
|
79
|
+
args,
|
|
80
|
+
theme,
|
|
81
|
+
JSON_TREE_MAX_DEPTH_EXPANDED,
|
|
82
|
+
JSON_TREE_MAX_LINES_EXPANDED,
|
|
83
|
+
JSON_TREE_SCALAR_LEN_EXPANDED,
|
|
84
|
+
);
|
|
85
|
+
lines.push(...tree.lines);
|
|
86
|
+
if (tree.truncated) lines.push(theme.fg("dim", "…"));
|
|
87
|
+
lines.push("");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const rendered = markdown.render(Math.max(1, contentWidth));
|
|
91
|
+
const maxOutputLines = options.expanded ? 12 : 4;
|
|
92
|
+
lines.push(...rendered.slice(0, maxOutputLines));
|
|
93
|
+
if (rendered.length > maxOutputLines) {
|
|
94
|
+
lines.push(
|
|
95
|
+
`${theme.fg("dim", `… ${rendered.length - maxOutputLines} more lines`)} ${formatExpandHint(theme, options.expanded, true)}`,
|
|
96
|
+
);
|
|
97
|
+
} else if (!options.expanded) {
|
|
98
|
+
lines.push(formatExpandHint(theme, options.expanded, true));
|
|
99
|
+
}
|
|
100
|
+
if (truncationWarning) lines.push(truncationWarning);
|
|
101
|
+
return lines;
|
|
102
|
+
},
|
|
103
|
+
invalidate(): void {},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
51
107
|
/**
|
|
52
108
|
* Render MCP tool result.
|
|
53
109
|
*/
|
|
@@ -58,6 +114,24 @@ export function renderMCPResult(
|
|
|
58
114
|
args?: Record<string, unknown>,
|
|
59
115
|
): Component {
|
|
60
116
|
const { expanded } = options;
|
|
117
|
+
const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
118
|
+
const trimmedOutput = stripOutputNotice(textContent, result.details?.meta).trimEnd();
|
|
119
|
+
const truncationWarning = result.details?.meta?.truncation
|
|
120
|
+
? formatStyledTruncationWarning(result.details.meta, theme)
|
|
121
|
+
: null;
|
|
122
|
+
let parsedOutput: unknown;
|
|
123
|
+
let isJsonOutput = false;
|
|
124
|
+
if (trimmedOutput.startsWith("{") || trimmedOutput.startsWith("[")) {
|
|
125
|
+
try {
|
|
126
|
+
parsedOutput = JSON.parse(trimmedOutput);
|
|
127
|
+
isJsonOutput = true;
|
|
128
|
+
} catch {
|
|
129
|
+
// Non-JSON text beginning with a bracket is still eligible for Markdown.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (trimmedOutput && settings.get("mcp.renderMarkdownResults") && !isJsonOutput) {
|
|
133
|
+
return renderMarkdownMCPResult(result, trimmedOutput, truncationWarning, options, theme, args);
|
|
134
|
+
}
|
|
61
135
|
return new WidthAwareText(
|
|
62
136
|
contentWidth => {
|
|
63
137
|
const lines: string[] = [];
|
|
@@ -86,46 +160,31 @@ export function renderMCPResult(
|
|
|
86
160
|
lines.push(""); // Blank line before output
|
|
87
161
|
}
|
|
88
162
|
|
|
89
|
-
// Output section
|
|
90
|
-
|
|
91
|
-
// Strip the LLM-facing spill notice before parsing/rendering: a spilled
|
|
92
|
-
// result appends `[Showing… artifact://N]` to the body, which would break
|
|
93
|
-
// JSON detection and bury the recovery link. Surface it as a styled warning
|
|
94
|
-
// instead, mirroring the built-in read/bash/ssh/browser renderers.
|
|
95
|
-
const trimmedOutput = stripOutputNotice(textContent, result.details?.meta).trimEnd();
|
|
96
|
-
const truncationWarning = result.details?.meta?.truncation
|
|
97
|
-
? formatStyledTruncationWarning(result.details.meta, theme)
|
|
98
|
-
: null;
|
|
163
|
+
// Output section. The body and spill metadata are normalized before
|
|
164
|
+
// component selection so the opt-in Markdown path can use its own renderer.
|
|
99
165
|
|
|
100
166
|
if (!trimmedOutput) {
|
|
101
167
|
lines.push(theme.fg("dim", "(no output)"));
|
|
102
168
|
return lines.join("\n");
|
|
103
169
|
}
|
|
104
170
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (!expanded) {
|
|
120
|
-
lines.push(formatExpandHint(theme, expanded, true));
|
|
121
|
-
} else if (tree.truncated) {
|
|
122
|
-
lines.push(theme.fg("dim", "…"));
|
|
123
|
-
}
|
|
124
|
-
if (truncationWarning) lines.push(truncationWarning);
|
|
125
|
-
return lines.join("\n");
|
|
171
|
+
// Preserve the existing structured JSON renderer regardless of the
|
|
172
|
+
// Markdown preference; JSON trees remain more useful than styled source.
|
|
173
|
+
if (isJsonOutput) {
|
|
174
|
+
const maxDepth = expanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
|
|
175
|
+
const maxLines = expanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
|
|
176
|
+
const maxScalarLen = expanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
|
|
177
|
+
const tree = renderJsonTreeLines(parsedOutput, theme, maxDepth, maxLines, maxScalarLen);
|
|
178
|
+
|
|
179
|
+
if (tree.lines.length > 0) {
|
|
180
|
+
lines.push(...tree.lines);
|
|
181
|
+
if (!expanded) {
|
|
182
|
+
lines.push(formatExpandHint(theme, expanded, true));
|
|
183
|
+
} else if (tree.truncated) {
|
|
184
|
+
lines.push(theme.fg("dim", "…"));
|
|
126
185
|
}
|
|
127
|
-
|
|
128
|
-
|
|
186
|
+
if (truncationWarning) lines.push(truncationWarning);
|
|
187
|
+
return lines.join("\n");
|
|
129
188
|
}
|
|
130
189
|
}
|
|
131
190
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "bun:test";
|
|
2
|
+
import { pollSmitheryCliAuthSession } from "./smithery-auth";
|
|
3
|
+
|
|
4
|
+
type FetchInput = string | URL | Request;
|
|
5
|
+
type FetchInit = RequestInit | BunFetchRequestInit;
|
|
6
|
+
|
|
7
|
+
describe("pollSmitheryCliAuthSession fetch cancellation", () => {
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("bounds each poll request with a timeout abort signal even without a caller signal", async () => {
|
|
13
|
+
const signals: AbortSignal[] = [];
|
|
14
|
+
const fetchStub = Object.assign(
|
|
15
|
+
async (_input: FetchInput, init?: FetchInit) => {
|
|
16
|
+
if (init?.signal instanceof AbortSignal) signals.push(init.signal);
|
|
17
|
+
return Response.json({ status: "pending" });
|
|
18
|
+
},
|
|
19
|
+
{ preconnect: globalThis.fetch.preconnect },
|
|
20
|
+
);
|
|
21
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(fetchStub);
|
|
22
|
+
|
|
23
|
+
const result = await pollSmitheryCliAuthSession("sess-123");
|
|
24
|
+
|
|
25
|
+
expect(result.status).toBe("pending");
|
|
26
|
+
expect(signals).toHaveLength(1);
|
|
27
|
+
expect(signals[0]?.aborted).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
});
|
package/src/mcp/smithery-auth.ts
CHANGED
|
@@ -7,13 +7,14 @@ import { withTimeoutSignal } from "../utils/fetch-timeout";
|
|
|
7
7
|
const SMITHERY_AUTH_FILENAME = "smithery.json";
|
|
8
8
|
const SMITHERY_URL = process.env.SMITHERY_URL || "https://smithery.ai";
|
|
9
9
|
const SMITHERY_AUTH_TIMEOUT_MS = 10_000;
|
|
10
|
+
const SMITHERY_POLL_TIMEOUT_MS = 30_000;
|
|
10
11
|
|
|
11
12
|
type SmitheryCliAuthSession = {
|
|
12
13
|
sessionId: string;
|
|
13
14
|
authUrl: string;
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
type SmitheryCliPollResponse = {
|
|
17
|
+
export type SmitheryCliPollResponse = {
|
|
17
18
|
status: "pending" | "success" | "error";
|
|
18
19
|
apiKey?: string;
|
|
19
20
|
message?: string;
|
|
@@ -53,7 +54,7 @@ export async function pollSmitheryCliAuthSession(
|
|
|
53
54
|
signal?: AbortSignal,
|
|
54
55
|
): Promise<SmitheryCliPollResponse> {
|
|
55
56
|
const response = await fetch(`${SMITHERY_URL}/api/auth/cli/poll/${sessionId}`, {
|
|
56
|
-
signal,
|
|
57
|
+
signal: withTimeoutSignal(SMITHERY_POLL_TIMEOUT_MS, signal),
|
|
57
58
|
});
|
|
58
59
|
if (!response.ok) {
|
|
59
60
|
if (response.status === 404 || response.status === 410) {
|