@visual-z/xeno 0.8.4 → 0.9.1
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 +19 -0
- package/README.md +31 -746
- package/bin/xeno.exe +26 -0
- package/install.cjs +88 -0
- package/package.json +18 -48
- package/docs/app-server.md +0 -115
- package/docs/foundations.md +0 -88
- package/docs/install.md +0 -244
- package/docs/providers.md +0 -172
- package/docs/research/foundation-clients-2026-10-09.md +0 -27
- package/docs/research/foundation-editing-2026-10-09.md +0 -56
- package/docs/research/foundation-network-2026-10-09.md +0 -37
- package/docs/research/foundation-workflow-2026-10-09.md +0 -37
- package/src/apps/acp-server.ts +0 -41
- package/src/apps/agent-profiles.ts +0 -19
- package/src/apps/app-server.ts +0 -30
- package/src/apps/approve.ts +0 -49
- package/src/apps/cli.ts +0 -99
- package/src/apps/commands.ts +0 -319
- package/src/apps/completions.ts +0 -87
- package/src/apps/control-transport.ts +0 -90
- package/src/apps/export-cli.ts +0 -24
- package/src/apps/goal-flow.ts +0 -70
- package/src/apps/image-draft.ts +0 -46
- package/src/apps/input.ts +0 -312
- package/src/apps/model-catalog.ts +0 -69
- package/src/apps/preferences.ts +0 -20
- package/src/apps/project-trust.ts +0 -9
- package/src/apps/provider-connect.ts +0 -346
- package/src/apps/question-ui.ts +0 -27
- package/src/apps/render.ts +0 -181
- package/src/apps/resources.ts +0 -66
- package/src/apps/review.ts +0 -109
- package/src/apps/session-hub.ts +0 -96
- package/src/apps/session-lifecycle.ts +0 -64
- package/src/apps/shell.ts +0 -2071
- package/src/apps/startup-input.ts +0 -135
- package/src/apps/startup.ts +0 -1677
- package/src/apps/ui/app.ts +0 -1818
- package/src/apps/ui/control-model.ts +0 -43
- package/src/apps/ui/extension-host.ts +0 -194
- package/src/apps/ui/image-cache.ts +0 -121
- package/src/apps/ui/palette.ts +0 -88
- package/src/apps/ui/project.ts +0 -127
- package/src/apps/ui/standalone.ts +0 -148
- package/src/apps/ui/status-model.ts +0 -68
- package/src/apps/ui/surface.ts +0 -91
- package/src/apps/ui/theme.ts +0 -86
- package/src/apps/update-check.ts +0 -112
- package/src/apps/workflow.ts +0 -58
- package/src/core/acceptance.ts +0 -139
- package/src/core/action-defaults.ts +0 -58
- package/src/core/activity-status.ts +0 -52
- package/src/core/agent-profile.ts +0 -130
- package/src/core/attachment-input.ts +0 -17
- package/src/core/blind-checks.ts +0 -72
- package/src/core/boundary.ts +0 -174
- package/src/core/branch-receipt.ts +0 -26
- package/src/core/catalog-merge.ts +0 -19
- package/src/core/checkpoints.ts +0 -85
- package/src/core/churn.ts +0 -61
- package/src/core/command-artifacts.ts +0 -94
- package/src/core/command-contract.ts +0 -151
- package/src/core/completion-action.ts +0 -20
- package/src/core/completion-review.ts +0 -98
- package/src/core/context-detail.ts +0 -37
- package/src/core/context-policy.ts +0 -133
- package/src/core/context-window.ts +0 -54
- package/src/core/continuation.ts +0 -130
- package/src/core/control-protocol.ts +0 -69
- package/src/core/control-view.ts +0 -39
- package/src/core/cycle.ts +0 -17
- package/src/core/data-query.ts +0 -201
- package/src/core/deadline.ts +0 -40
- package/src/core/derive.ts +0 -516
- package/src/core/draft-images.ts +0 -56
- package/src/core/edit-plan.ts +0 -200
- package/src/core/environment.ts +0 -181
- package/src/core/event-validation.ts +0 -220
- package/src/core/events.ts +0 -670
- package/src/core/evidence-ladder.ts +0 -62
- package/src/core/evidence-links.ts +0 -68
- package/src/core/evidence.ts +0 -240
- package/src/core/execution.ts +0 -18
- package/src/core/exhaustive.ts +0 -18
- package/src/core/export.ts +0 -179
- package/src/core/extension-flags.ts +0 -14
- package/src/core/external-effects.ts +0 -488
- package/src/core/failure-recovery.ts +0 -173
- package/src/core/file-precondition.ts +0 -78
- package/src/core/file-references.ts +0 -35
- package/src/core/frontmatter.ts +0 -213
- package/src/core/git-status.ts +0 -29
- package/src/core/goal-view.ts +0 -64
- package/src/core/goal.ts +0 -200
- package/src/core/handoff.ts +0 -479
- package/src/core/history-query.ts +0 -139
- package/src/core/history.ts +0 -237
- package/src/core/image-limits.ts +0 -72
- package/src/core/images.ts +0 -46
- package/src/core/integration-health.ts +0 -34
- package/src/core/json-event.ts +0 -131
- package/src/core/json-pointer.ts +0 -46
- package/src/core/keymap.ts +0 -153
- package/src/core/lsp-framing.ts +0 -34
- package/src/core/mcp-config.ts +0 -68
- package/src/core/model-picker.ts +0 -64
- package/src/core/model-preferences.ts +0 -28
- package/src/core/model-replay.ts +0 -22
- package/src/core/model-scope.ts +0 -53
- package/src/core/note-guidance.ts +0 -34
- package/src/core/oauth-fields.ts +0 -44
- package/src/core/observation-view.ts +0 -152
- package/src/core/outcome.ts +0 -44
- package/src/core/overflow.ts +0 -59
- package/src/core/packages.ts +0 -47
- package/src/core/patch.ts +0 -130
- package/src/core/path-suggestions.ts +0 -59
- package/src/core/permissions.ts +0 -410
- package/src/core/process-receipt.ts +0 -105
- package/src/core/process-state.ts +0 -37
- package/src/core/program-status.ts +0 -40
- package/src/core/progress.ts +0 -150
- package/src/core/project.ts +0 -431
- package/src/core/prompt-policy.ts +0 -46
- package/src/core/prompts.ts +0 -111
- package/src/core/question.ts +0 -94
- package/src/core/queue.ts +0 -78
- package/src/core/read-bounds.ts +0 -24
- package/src/core/read-observation.ts +0 -106
- package/src/core/recovery-sources.ts +0 -74
- package/src/core/regex-dialect.ts +0 -25
- package/src/core/request-budget.ts +0 -23
- package/src/core/request-progress.ts +0 -87
- package/src/core/request-state.ts +0 -273
- package/src/core/resource-selection.ts +0 -36
- package/src/core/response-recovery.ts +0 -59
- package/src/core/resume.ts +0 -59
- package/src/core/review.ts +0 -68
- package/src/core/ripgrep-release.ts +0 -64
- package/src/core/runtime-target.ts +0 -94
- package/src/core/scheduling.ts +0 -8
- package/src/core/search-observation.ts +0 -97
- package/src/core/secrets.ts +0 -188
- package/src/core/session-archive.ts +0 -4
- package/src/core/session-browser.ts +0 -82
- package/src/core/session-entry.ts +0 -61
- package/src/core/session-exit.ts +0 -42
- package/src/core/session-format.ts +0 -169
- package/src/core/session-position.ts +0 -26
- package/src/core/session-tree.ts +0 -147
- package/src/core/settings.ts +0 -192
- package/src/core/skills.ts +0 -165
- package/src/core/sql-query.ts +0 -211
- package/src/core/stall.ts +0 -128
- package/src/core/stop-report.ts +0 -68
- package/src/core/text-normalize.ts +0 -81
- package/src/core/theme.ts +0 -166
- package/src/core/thinking.ts +0 -26
- package/src/core/tokens.ts +0 -151
- package/src/core/tool-contract.ts +0 -153
- package/src/core/tool-discovery.ts +0 -87
- package/src/core/tool-failure.ts +0 -81
- package/src/core/tool-input.ts +0 -44
- package/src/core/transcript.ts +0 -108
- package/src/core/tui-mode.ts +0 -29
- package/src/core/update-check.ts +0 -157
- package/src/core/version.ts +0 -55
- package/src/core/web-content.ts +0 -80
- package/src/core/work-state.ts +0 -154
- package/src/core/workflow-mode.ts +0 -34
- package/src/core/working-set.ts +0 -119
- package/src/core/yaml.ts +0 -282
- package/src/runtime/acp-agent.ts +0 -289
- package/src/runtime/agent-profiles.ts +0 -21
- package/src/runtime/auth.ts +0 -367
- package/src/runtime/blind-check.ts +0 -36
- package/src/runtime/branch-jobs.ts +0 -36
- package/src/runtime/catalog.ts +0 -285
- package/src/runtime/clipboard.ts +0 -125
- package/src/runtime/command-artifacts.ts +0 -64
- package/src/runtime/connection-reuse.ts +0 -40
- package/src/runtime/control-client.ts +0 -149
- package/src/runtime/control-connection.ts +0 -139
- package/src/runtime/counter-check.ts +0 -74
- package/src/runtime/environment-tools.ts +0 -60
- package/src/runtime/evidence-notes.ts +0 -35
- package/src/runtime/exec-sessions.ts +0 -456
- package/src/runtime/execution-control.ts +0 -40
- package/src/runtime/extension-loader.ts +0 -37
- package/src/runtime/extension-ui.ts +0 -47
- package/src/runtime/extensions.ts +0 -370
- package/src/runtime/formatter-tool.ts +0 -38
- package/src/runtime/formatter.ts +0 -50
- package/src/runtime/foundation.ts +0 -116
- package/src/runtime/history-cache.ts +0 -57
- package/src/runtime/history-tools.ts +0 -168
- package/src/runtime/images.ts +0 -80
- package/src/runtime/integrations.ts +0 -216
- package/src/runtime/loop.ts +0 -3108
- package/src/runtime/lsp-client.ts +0 -201
- package/src/runtime/mcp-client.ts +0 -264
- package/src/runtime/mcp-oauth.ts +0 -168
- package/src/runtime/mcp-schema.ts +0 -51
- package/src/runtime/model-config.ts +0 -232
- package/src/runtime/model-data.LICENSE +0 -27
- package/src/runtime/model-data.ts +0 -21
- package/src/runtime/model-discovery.ts +0 -208
- package/src/runtime/model-replay.ts +0 -10
- package/src/runtime/model-scope.ts +0 -20
- package/src/runtime/model-sources.ts +0 -151
- package/src/runtime/model.ts +0 -686
- package/src/runtime/models-fetch.ts +0 -57
- package/src/runtime/oauth-device.ts +0 -86
- package/src/runtime/oauth.ts +0 -853
- package/src/runtime/open-image.ts +0 -42
- package/src/runtime/openai-responses.ts +0 -304
- package/src/runtime/openai.ts +0 -697
- package/src/runtime/output-schema.ts +0 -26
- package/src/runtime/package-manager.ts +0 -125
- package/src/runtime/patch-tool.ts +0 -38
- package/src/runtime/permission-resources.ts +0 -18
- package/src/runtime/pi-provider.ts +0 -423
- package/src/runtime/program-status.ts +0 -38
- package/src/runtime/progress.ts +0 -26
- package/src/runtime/provider-access.ts +0 -21
- package/src/runtime/provider-connect.ts +0 -109
- package/src/runtime/provider-presets.ts +0 -98
- package/src/runtime/provider-protocols.ts +0 -29
- package/src/runtime/provider-request.ts +0 -87
- package/src/runtime/provider-tool-policy.ts +0 -33
- package/src/runtime/provider.ts +0 -580
- package/src/runtime/question-tool.ts +0 -62
- package/src/runtime/retry.ts +0 -447
- package/src/runtime/review-tool.ts +0 -48
- package/src/runtime/rich-text.ts +0 -36
- package/src/runtime/runtime-target.ts +0 -149
- package/src/runtime/secrets.ts +0 -79
- package/src/runtime/self-update.ts +0 -74
- package/src/runtime/session-control.ts +0 -265
- package/src/runtime/session-hub.ts +0 -85
- package/src/runtime/stream-idle.ts +0 -80
- package/src/runtime/tool-broker.ts +0 -137
- package/src/runtime/tool-discovery.ts +0 -58
- package/src/runtime/tool-queue.ts +0 -77
- package/src/runtime/tools.ts +0 -1820
- package/src/runtime/turn-effects.ts +0 -31
- package/src/runtime/update-check.ts +0 -48
- package/src/runtime/usage.ts +0 -253
- package/src/runtime/version.ts +0 -12
- package/src/runtime/web-http.ts +0 -56
- package/src/runtime/web-tools.ts +0 -88
- package/src/runtime/working-notes.ts +0 -60
- package/src/runtime/workspace-lifecycle.ts +0 -141
- package/src/store/acceptance.ts +0 -15
- package/src/store/agent-profile-files.ts +0 -13
- package/src/store/atomic.ts +0 -31
- package/src/store/auth-store.ts +0 -50
- package/src/store/blind-checks.ts +0 -32
- package/src/store/blobs.ts +0 -143
- package/src/store/checkpoint-history.ts +0 -32
- package/src/store/command-artifacts.ts +0 -96
- package/src/store/command.ts +0 -111
- package/src/store/config.ts +0 -69
- package/src/store/context-files.ts +0 -436
- package/src/store/file-checkpoints.ts +0 -108
- package/src/store/file-index.ts +0 -174
- package/src/store/foundation-config.ts +0 -24
- package/src/store/git-checkpoints.ts +0 -67
- package/src/store/git-status.ts +0 -15
- package/src/store/history-search.ts +0 -19
- package/src/store/log.ts +0 -725
- package/src/store/lsp-document.ts +0 -30
- package/src/store/mcp-credentials.ts +0 -85
- package/src/store/models.ts +0 -60
- package/src/store/observations.ts +0 -224
- package/src/store/packages.ts +0 -98
- package/src/store/patch.ts +0 -92
- package/src/store/permission-paths.ts +0 -34
- package/src/store/process-receipts.ts +0 -34
- package/src/store/query-source.ts +0 -49
- package/src/store/recovery.ts +0 -44
- package/src/store/replay.ts +0 -76
- package/src/store/resource-index.ts +0 -170
- package/src/store/resources.ts +0 -285
- package/src/store/restore.ts +0 -110
- package/src/store/review.ts +0 -109
- package/src/store/runtime-target.ts +0 -64
- package/src/store/search.ts +0 -728
- package/src/store/secrets.ts +0 -44
- package/src/store/serial.ts +0 -13
- package/src/store/session-archive.ts +0 -165
- package/src/store/session-browser.ts +0 -68
- package/src/store/session-position.ts +0 -27
- package/src/store/sessions.ts +0 -525
- package/src/store/source-cancellation.ts +0 -6
- package/src/store/tool-binaries.ts +0 -90
- package/src/store/trust.ts +0 -82
- package/src/store/update-cache.ts +0 -36
- package/src/store/user-settings.ts +0 -30
- package/src/store/workspace-clone.ts +0 -42
- package/src/store/workspace-observation.ts +0 -113
- package/src/store/workspace.ts +0 -115
- package/src/store/worktree.ts +0 -278
- package/src/tui/ansi-spans.ts +0 -126
- package/src/tui/diff.ts +0 -160
- package/src/tui/exhaustive.ts +0 -14
- package/src/tui/fuzzy.ts +0 -75
- package/src/tui/style.ts +0 -208
- package/src/tui/ui/atomic-draft.ts +0 -121
- package/src/tui/ui/branch-panel.ts +0 -47
- package/src/tui/ui/canvas.ts +0 -96
- package/src/tui/ui/colors.ts +0 -70
- package/src/tui/ui/composer.ts +0 -199
- package/src/tui/ui/control-panel.ts +0 -25
- package/src/tui/ui/control-sidebar.ts +0 -49
- package/src/tui/ui/dialog.ts +0 -398
- package/src/tui/ui/draft.ts +0 -23
- package/src/tui/ui/duration.ts +0 -16
- package/src/tui/ui/edge.ts +0 -186
- package/src/tui/ui/image-size.ts +0 -44
- package/src/tui/ui/ink.ts +0 -210
- package/src/tui/ui/items.ts +0 -42
- package/src/tui/ui/keys.ts +0 -40
- package/src/tui/ui/layout.ts +0 -74
- package/src/tui/ui/list-model.ts +0 -86
- package/src/tui/ui/list-view.ts +0 -172
- package/src/tui/ui/mascot.ts +0 -67
- package/src/tui/ui/messages.ts +0 -518
- package/src/tui/ui/motion.ts +0 -28
- package/src/tui/ui/paths.ts +0 -10
- package/src/tui/ui/pictures.ts +0 -108
- package/src/tui/ui/prompt-history.ts +0 -60
- package/src/tui/ui/screen.ts +0 -579
- package/src/tui/ui/status.ts +0 -403
- package/src/tui/ui/styled.ts +0 -85
- package/src/tui/ui/tool-card.ts +0 -150
- package/src/tui/width.ts +0 -567
- package/src/vendor/pi-rich/LICENSE.upstream +0 -21
- package/src/vendor/pi-rich/UPSTREAM.json +0 -17
- package/src/vendor/pi-rich/clipboard-command.ts +0 -44
- package/src/vendor/pi-rich/exif-orientation.ts +0 -182
- package/src/vendor/pi-rich/html.ts +0 -51
- package/src/vendor/pi-rich/image-convert.ts +0 -49
- package/src/vendor/pi-rich/image-resize-core.ts +0 -164
- package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
- package/src/vendor/pi-rich/image-resize.ts +0 -123
- package/src/vendor/pi-rich/mime.ts +0 -116
- package/src/vendor/pi-rich/photon.ts +0 -139
- package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/** Explicit trusted servers, one lifecycle each; server edits never bypass checkpoint ownership. */
|
|
2
|
-
import { extname } from "node:path";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { nextLspFrame, encodeLspFrame, diagnosticFreshness, MAX_LSP_FRAME, MAX_LSP_HEADER } from "../core/lsp-framing.ts";
|
|
5
|
-
import { readLspDocument } from "../store/lsp-document.ts";
|
|
6
|
-
import { presentObservation, type ToolDefinition, type ToolResult } from "./tools.ts";
|
|
7
|
-
|
|
8
|
-
export interface LspConfig { name: string; command: string[]; extensions: string[]; languageId: string }
|
|
9
|
-
export function parseLspConfig(value: unknown): LspConfig[] {
|
|
10
|
-
if (value === undefined) return [];
|
|
11
|
-
if (!Array.isArray(value) || value.length > 16) throw Error("lsp must be an array of at most 16 servers");
|
|
12
|
-
const names = new Set<string>();
|
|
13
|
-
return value.map(item => {
|
|
14
|
-
if (!item || typeof item !== "object" || Array.isArray(item)) throw Error("invalid LSP config");
|
|
15
|
-
const v = item as Record<string, unknown>;
|
|
16
|
-
if (Object.keys(v).some(key => !["name", "command", "extensions", "languageId"].includes(key)) || typeof v["name"] !== "string" || !/^[a-zA-Z0-9_-]{1,64}$/.test(v["name"]) || names.has(v["name"]) || typeof v["languageId"] !== "string" || !v["languageId"] || !Array.isArray(v["command"]) || !v["command"].length || v["command"].some(part => typeof part !== "string" || !part || part.includes("\0")) || !Array.isArray(v["extensions"]) || !v["extensions"].length || v["extensions"].some(part => typeof part !== "string" || !/^\.[a-zA-Z0-9]+$/.test(part))) throw Error("invalid or duplicate LSP config");
|
|
17
|
-
names.add(v["name"]); return { name: v["name"], languageId: v["languageId"], command: v["command"].map(String), extensions: v["extensions"].map(String) };
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
type Pending = { resolve(value: unknown): void; reject(error: Error): void; cancel(): void };
|
|
21
|
-
// Operational limits, not segment thresholds: bound unsolicited traffic and retained server state.
|
|
22
|
-
const MAX_LSP_STREAM_BYTES = 32 * 1024 * 1024;
|
|
23
|
-
const MAX_LSP_DIAGNOSTIC_BYTES = 512 * 1024;
|
|
24
|
-
const MAX_LSP_OPEN_DOCUMENTS = 32;
|
|
25
|
-
class LspConnection {
|
|
26
|
-
private readonly root: string;
|
|
27
|
-
private readonly config: LspConfig;
|
|
28
|
-
private process: ReturnType<typeof Bun.spawn> | undefined;
|
|
29
|
-
private buffer = new Uint8Array(0);
|
|
30
|
-
private next = 1;
|
|
31
|
-
private readonly pending = new Map<number, Pending>();
|
|
32
|
-
private readonly documents = new Map<string, { sha256: string; version: number }>();
|
|
33
|
-
private readonly diagnostics = new Map<string, { version?: number; items: unknown[] }>();
|
|
34
|
-
private ready: Promise<void> | undefined;
|
|
35
|
-
private closed = false;
|
|
36
|
-
private failure: string | undefined;
|
|
37
|
-
private streamBytes = 0;
|
|
38
|
-
private documentVersion = 0;
|
|
39
|
-
get usable(): boolean { return !this.closed && this.failure === undefined; }
|
|
40
|
-
constructor(root: string, config: LspConfig) { this.root = root; this.config = config; }
|
|
41
|
-
private send(message: unknown): void {
|
|
42
|
-
if (this.failure) throw Error(this.failure);
|
|
43
|
-
const stdin = this.process?.stdin;
|
|
44
|
-
if (!stdin || typeof stdin === "number") throw Error("LSP server is unavailable");
|
|
45
|
-
stdin.write(encodeLspFrame(message)); stdin.flush();
|
|
46
|
-
}
|
|
47
|
-
private notify(method: string, params: unknown): void { this.send({ jsonrpc: "2.0", method, params }); }
|
|
48
|
-
private request(method: string, params: unknown, signal?: AbortSignal, timeoutMs = 10_000): Promise<unknown> {
|
|
49
|
-
if (signal?.aborted) return Promise.reject(Error("LSP request cancelled"));
|
|
50
|
-
const id = this.next++;
|
|
51
|
-
return new Promise((resolve, reject) => {
|
|
52
|
-
const stop = (reason: string) => { const pending = this.pending.get(id); if (!pending) return; this.pending.delete(id); pending.cancel(); try { this.notify("$/cancelRequest", { id }); } catch { /* process may be dead */ } reject(Error(reason)); };
|
|
53
|
-
const aborted = () => stop("LSP request cancelled");
|
|
54
|
-
const timer = setTimeout(() => stop("LSP request timed out"), timeoutMs);
|
|
55
|
-
this.pending.set(id, { resolve, reject, cancel: () => { clearTimeout(timer); signal?.removeEventListener("abort", aborted); } });
|
|
56
|
-
signal?.addEventListener("abort", aborted, { once: true });
|
|
57
|
-
try { this.streamBytes = 0; this.send({ jsonrpc: "2.0", id, method, params }); } catch (error) { this.pending.delete(id); clearTimeout(timer); signal?.removeEventListener("abort", aborted); reject(error); }
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
private fail(reason: string): void {
|
|
61
|
-
this.failure ??= reason;
|
|
62
|
-
for (const item of this.pending.values()) { item.cancel(); item.reject(Error(reason)); } this.pending.clear();
|
|
63
|
-
}
|
|
64
|
-
private receive(value: unknown): void {
|
|
65
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw Error("invalid LSP envelope");
|
|
66
|
-
const v = value as Record<string, unknown>;
|
|
67
|
-
if (v["jsonrpc"] !== "2.0") throw Error("invalid LSP version");
|
|
68
|
-
if (typeof v["method"] === "string") {
|
|
69
|
-
const params = v["params"] !== null && typeof v["params"] === "object" && !Array.isArray(v["params"]) ? v["params"] as Record<string, unknown> : undefined;
|
|
70
|
-
if (v["method"] === "textDocument/publishDiagnostics" && typeof params?.["uri"] === "string" && Array.isArray(params["diagnostics"])) {
|
|
71
|
-
const version = params["version"];
|
|
72
|
-
// A server may report a whole workspace; cache only documents opened by this client.
|
|
73
|
-
if (this.documents.has(params["uri"])) {
|
|
74
|
-
if (version !== undefined && (typeof version !== "number" || !Number.isInteger(version) || version < 0 || version > 2 ** 31 - 1)) throw Error("invalid LSP diagnostic version");
|
|
75
|
-
if (new TextEncoder().encode(JSON.stringify(params["diagnostics"])).length > MAX_LSP_DIAGNOSTIC_BYTES) throw Error("LSP diagnostics exceeded");
|
|
76
|
-
this.diagnostics.set(params["uri"], { ...(typeof version === "number" ? { version } : {}), items: params["diagnostics"] });
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
if (v["id"] !== undefined) {
|
|
80
|
-
if (typeof v["id"] !== "string" && (typeof v["id"] !== "number" || !Number.isSafeInteger(v["id"]))) throw Error("invalid LSP request id");
|
|
81
|
-
const method = v["method"];
|
|
82
|
-
if (method === "workspace/configuration") this.send({ jsonrpc: "2.0", id: v["id"], result: Array.isArray(params?.["items"]) ? params["items"].map(() => null) : [] });
|
|
83
|
-
else if (method === "workspace/workspaceFolders") this.send({ jsonrpc: "2.0", id: v["id"], result: [{ uri: pathToFileURL(this.root).href, name: this.config.name }] });
|
|
84
|
-
else if (method === "workspace/applyEdit") this.send({ jsonrpc: "2.0", id: v["id"], result: { applied: false, failureReason: "use the owned Patch tool; LSP cannot write files" } });
|
|
85
|
-
else this.send({ jsonrpc: "2.0", id: v["id"], error: { code: -32601, message: "client capability not supported" } });
|
|
86
|
-
}
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
if (typeof v["id"] !== "number" || !Number.isSafeInteger(v["id"])) throw Error("invalid LSP response id");
|
|
90
|
-
const pending = this.pending.get(v["id"]); if (!pending) return;
|
|
91
|
-
if (Object.hasOwn(v, "result") === Object.hasOwn(v, "error")) throw Error("invalid LSP response result");
|
|
92
|
-
this.pending.delete(v["id"]); pending.cancel(); this.streamBytes = 0;
|
|
93
|
-
if (v["error"] !== undefined) pending.reject(Error(JSON.stringify(v["error"]).slice(0, 512))); else pending.resolve(v["result"]);
|
|
94
|
-
}
|
|
95
|
-
async start(signal?: AbortSignal): Promise<void> {
|
|
96
|
-
signal?.throwIfAborted();
|
|
97
|
-
if (this.closed) throw Error("LSP connection disposed");
|
|
98
|
-
if (this.failure) throw Error(this.failure);
|
|
99
|
-
if (this.ready) return this.ready;
|
|
100
|
-
this.ready = (async () => {
|
|
101
|
-
this.process = Bun.spawn(this.config.command, { cwd: this.root, stdin: "pipe", stdout: "pipe", stderr: "ignore" });
|
|
102
|
-
const process = this.process;
|
|
103
|
-
void (async () => {
|
|
104
|
-
try {
|
|
105
|
-
if (!process.stdout || typeof process.stdout === "number") throw Error("missing LSP stdout");
|
|
106
|
-
for await (const chunk of process.stdout) {
|
|
107
|
-
this.streamBytes += chunk.length;
|
|
108
|
-
if (this.streamBytes > MAX_LSP_STREAM_BYTES) throw Error("LSP stream budget exceeded");
|
|
109
|
-
// Drain complete frames before admitting more bytes; OS chunk boundaries are arbitrary.
|
|
110
|
-
for (let offset = 0; offset < chunk.length;) {
|
|
111
|
-
const capacity = MAX_LSP_FRAME + MAX_LSP_HEADER + 4 - this.buffer.length;
|
|
112
|
-
if (capacity <= 0) throw Error("LSP receive buffer exceeded");
|
|
113
|
-
const part = chunk.subarray(offset, offset + capacity); offset += part.length;
|
|
114
|
-
const merged = new Uint8Array(this.buffer.length + part.length); merged.set(this.buffer); merged.set(part, this.buffer.length); this.buffer = merged;
|
|
115
|
-
for (;;) { const frame = nextLspFrame(this.buffer); if (!frame) break; this.buffer = this.buffer.slice(frame.consumed); this.receive(JSON.parse(new TextDecoder("utf8", { fatal: true }).decode(frame.body))); }
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
this.fail("LSP server exited");
|
|
119
|
-
} catch { this.fail("LSP protocol failed"); process.kill("SIGKILL"); }
|
|
120
|
-
})();
|
|
121
|
-
const result = await this.request("initialize", { processId: globalThis.process.pid, rootUri: pathToFileURL(this.root).href,
|
|
122
|
-
capabilities: { general: { positionEncodings: ["utf-16"] }, workspace: { applyEdit: false }, textDocument: { synchronization: { dynamicRegistration: false }, diagnostic: {} } },
|
|
123
|
-
workspaceFolders: [{ uri: pathToFileURL(this.root).href, name: this.config.name }] }, signal);
|
|
124
|
-
if (!result || typeof result !== "object" || Array.isArray(result) || !("capabilities" in result) || !result.capabilities || typeof result.capabilities !== "object") throw Error("invalid LSP initialize result");
|
|
125
|
-
const encoding = (result.capabilities as Record<string, unknown>)["positionEncoding"];
|
|
126
|
-
if (encoding !== undefined && encoding !== "utf-16") throw Error("LSP server does not support UTF-16 positions");
|
|
127
|
-
signal?.throwIfAborted();
|
|
128
|
-
this.notify("initialized", {});
|
|
129
|
-
})();
|
|
130
|
-
try { await this.ready; } catch (error) { this.fail(error instanceof Error ? error.message : "LSP initialization failed"); this.process?.kill("SIGKILL"); throw error; }
|
|
131
|
-
}
|
|
132
|
-
async call(action: string, path: string, line: number, character: number, signal?: AbortSignal): Promise<unknown> {
|
|
133
|
-
const document = await readLspDocument(this.root, path, signal);
|
|
134
|
-
await this.start(signal); signal?.throwIfAborted();
|
|
135
|
-
const old = this.documents.get(document.uri);
|
|
136
|
-
const changed = old !== undefined && old.sha256 !== document.sha256;
|
|
137
|
-
// Monotonic versions also prevent late diagnostics from an evicted/reopened file matching it.
|
|
138
|
-
const version = !old || changed ? ++this.documentVersion : old.version;
|
|
139
|
-
if (!old && this.documents.size >= MAX_LSP_OPEN_DOCUMENTS) {
|
|
140
|
-
const oldest = this.documents.keys().next().value;
|
|
141
|
-
if (oldest !== undefined) { this.notify("textDocument/didClose", { textDocument: { uri: oldest } }); this.documents.delete(oldest); this.diagnostics.delete(oldest); }
|
|
142
|
-
}
|
|
143
|
-
if (!old) this.notify("textDocument/didOpen", { textDocument: { uri: document.uri, languageId: this.config.languageId, version, text: document.text } });
|
|
144
|
-
else if (changed) { this.diagnostics.delete(document.uri); this.notify("textDocument/didChange", { textDocument: { uri: document.uri, version }, contentChanges: [{ text: document.text }] }); }
|
|
145
|
-
const current = version;
|
|
146
|
-
this.documents.set(document.uri, { sha256: document.sha256, version: current });
|
|
147
|
-
if (action === "diagnostics") {
|
|
148
|
-
try {
|
|
149
|
-
const result = await this.request("textDocument/diagnostic", { textDocument: { uri: document.uri } }, signal);
|
|
150
|
-
const onDisk = await readLspDocument(this.root, path, signal);
|
|
151
|
-
return { sha256: document.sha256, version: current, kind: "pull", freshness: onDisk.sha256 === document.sha256 ? "current" : "stale", result };
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
signal?.throwIfAborted(); if (this.failure) throw Error(this.failure);
|
|
155
|
-
const observed = this.diagnostics.get(document.uri), onDisk = await readLspDocument(this.root, path, signal);
|
|
156
|
-
const freshness = onDisk.sha256 !== document.sha256 ? "stale" : observed ? diagnosticFreshness(observed.version, current) : "unavailable";
|
|
157
|
-
return { sha256: document.sha256, version: current, kind: "push", freshness, observedVersion: observed?.version ?? null,
|
|
158
|
-
available: freshness === "current", versionConfirmed: freshness === "current", diagnostics: freshness === "current" ? observed?.items ?? [] : [],
|
|
159
|
-
unconfirmedCount: freshness === "current" ? 0 : observed?.items.length ?? 0, reason: error instanceof Error ? error.message : "pull diagnostics unavailable" };
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
const methods: Record<string, string> = { hover: "textDocument/hover", definition: "textDocument/definition", references: "textDocument/references" };
|
|
163
|
-
const method = methods[action]; if (!method) throw Error("unsupported LSP action");
|
|
164
|
-
const result = await this.request(method, { textDocument: { uri: document.uri }, position: { line, character }, ...(action === "references" ? { context: { includeDeclaration: true } } : {}) }, signal);
|
|
165
|
-
const onDisk = await readLspDocument(this.root, path, signal);
|
|
166
|
-
return { sha256: document.sha256, version: current, freshness: onDisk.sha256 === document.sha256 ? "current" : "stale", result };
|
|
167
|
-
}
|
|
168
|
-
async dispose(): Promise<void> {
|
|
169
|
-
if (this.closed) return; this.closed = true;
|
|
170
|
-
if (this.process) {
|
|
171
|
-
try { await this.request("shutdown", null, undefined, 500); this.notify("exit", null); } catch { /* bounded shutdown falls back to kill */ }
|
|
172
|
-
// This process is client-owned. A server ignoring shutdown cannot leave a resident leak.
|
|
173
|
-
this.process.kill("SIGKILL"); await this.process.exited;
|
|
174
|
-
}
|
|
175
|
-
this.fail("LSP disposed"); this.documents.clear(); this.diagnostics.clear();
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
export class LspManager {
|
|
179
|
-
private readonly root: string;
|
|
180
|
-
private readonly configs: LspConfig[];
|
|
181
|
-
private readonly connections = new Map<string, LspConnection>();
|
|
182
|
-
private closed = false;
|
|
183
|
-
constructor(root: string, configs: LspConfig[]) { this.root = root; this.configs = configs.map(config => ({ ...config, command: [...config.command], extensions: [...config.extensions] })); }
|
|
184
|
-
tool(): ToolDefinition {
|
|
185
|
-
return { name: "lsp", description: "Read language-server diagnostics, hover, definitions or references for a current workspace file. Positions use zero-based UTF-16 units. Servers come from trusted explicit configuration.",
|
|
186
|
-
access: "read", branch: "none", execution: "exclusive", input_schema: { type: "object", required: ["action", "path"],
|
|
187
|
-
properties: { action: { type: "string", enum: ["diagnostics", "hover", "definition", "references"] }, path: { type: "string" }, line: { type: "integer", minimum: 0 }, character: { type: "integer", minimum: 0 }, server: { type: "string" } } },
|
|
188
|
-
run: async (ctx, input): Promise<ToolResult> => {
|
|
189
|
-
if (this.closed) throw Error("LSP manager disposed");
|
|
190
|
-
const selected = this.configs.filter(config => config.extensions.includes(extname(String(input["path"]))) && (input["server"] === undefined || config.name === input["server"]));
|
|
191
|
-
if (selected.length !== 1) return { ok: false, content: "Select exactly one configured language server for this file." };
|
|
192
|
-
const config = selected[0]!; let connection = this.connections.get(config.name);
|
|
193
|
-
if (!connection) { connection = new LspConnection(this.root, config); this.connections.set(config.name, connection); }
|
|
194
|
-
try {
|
|
195
|
-
const result = await connection.call(String(input["action"]), String(input["path"]), Number(input["line"] ?? 0), Number(input["character"] ?? 0), ctx.signal);
|
|
196
|
-
return { ok: true, ...await presentObservation(ctx, JSON.stringify(result)) };
|
|
197
|
-
} catch (error) { if (!connection.usable) this.connections.delete(config.name); throw error; }
|
|
198
|
-
} };
|
|
199
|
-
}
|
|
200
|
-
async dispose(): Promise<void> { this.closed = true; await Promise.all([...this.connections.values()].map(connection => connection.dispose())); this.connections.clear(); }
|
|
201
|
-
}
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
/** Standard MCP adapters reuse the existing integration owner; no business request is automatically replayed. */
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { Client, StreamableHTTPClientTransport, UnauthorizedError, InsufficientScopeError, SdkError, SdkHttpError, SdkErrorCode, extractWWWAuthenticateParams,
|
|
5
|
-
type Tool, type Transport, type FetchLike, type ContentBlock, type JsonSchemaType } from "@modelcontextprotocol/client";
|
|
6
|
-
import { StdioClientTransport, getDefaultEnvironment } from "@modelcontextprotocol/client/stdio";
|
|
7
|
-
import { parseMcpServers, type McpServerConfig, type McpServers } from "../core/mcp-config.ts";
|
|
8
|
-
import { isJsonValue, type JsonValue } from "../core/tool-contract.ts";
|
|
9
|
-
import type { IntegrationFailureCode } from "../core/integration-health.ts";
|
|
10
|
-
import { FileMcpCredentialsStore, type McpCredentialsStore } from "../store/mcp-credentials.ts";
|
|
11
|
-
import type { ExtensionIntegration, IntegrationConnection, IntegrationFailure } from "./integrations.ts";
|
|
12
|
-
import type { ExtensionToolResult } from "./extensions.ts";
|
|
13
|
-
import { McpAuthRequired, McpOAuthProvider } from "./mcp-oauth.ts";
|
|
14
|
-
import { MAX_WEB_RESPONSE_BYTES, type WebFetch } from "./web-http.ts";
|
|
15
|
-
import { McpSchemaError, McpSchemas } from "./mcp-schema.ts";
|
|
16
|
-
import { ToolFault } from "../core/tool-failure.ts";
|
|
17
|
-
|
|
18
|
-
export interface McpServerStatus { name: string; transport: "stdio" | "http"; state: "ready" | "disabled" | IntegrationFailureCode; tools: number; diagnostic?: string }
|
|
19
|
-
export interface McpManagerOptions { cwd: string; credentialsDirectory: string; credentialsStore?: McpCredentialsStore;
|
|
20
|
-
env?: Readonly<Record<string, string | undefined>>; fetch?: WebFetch }
|
|
21
|
-
class McpFailure extends Error {
|
|
22
|
-
readonly code: IntegrationFailureCode;
|
|
23
|
-
constructor(code: IntegrationFailureCode) { super(`MCP ${code}`); this.name = "McpFailure"; this.code = code; }
|
|
24
|
-
}
|
|
25
|
-
function fault(error: unknown): IntegrationFailure {
|
|
26
|
-
const code = error instanceof McpFailure ? error.code : error instanceof McpAuthRequired || error instanceof UnauthorizedError || error instanceof InsufficientScopeError ? "auth_required"
|
|
27
|
-
: error instanceof SdkHttpError ? error.status === 401 || error.status === 403 ? "auth_required" : error.status === 429 ? "rate_limited" : error.status >= 500 ? "offline" : "protocol_error"
|
|
28
|
-
: error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout ? "timeout" : "protocol_error";
|
|
29
|
-
return { kind: "integration_failure", code };
|
|
30
|
-
}
|
|
31
|
-
export function mcpToolName(server: string, name: string, kind = "tool"): string {
|
|
32
|
-
const digest = createHash("sha256").update(JSON.stringify([server, kind, name])).digest("hex").slice(0, 12);
|
|
33
|
-
return `mcp_${server.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 12)}_${name.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 20)}_${digest}`;
|
|
34
|
-
}
|
|
35
|
-
function integrationName(name: string) { return `mcp_${name.slice(0, 48)}_${createHash("sha256").update(name).digest("hex").slice(0, 8)}`; }
|
|
36
|
-
function immutable<T>(value: T): T {
|
|
37
|
-
if (value && typeof value === "object") { for (const entry of Object.values(value)) immutable(entry); Object.freeze(value); }
|
|
38
|
-
return value;
|
|
39
|
-
}
|
|
40
|
-
function contentResult(content: readonly ContentBlock[], details: unknown, isError = false): ExtensionToolResult {
|
|
41
|
-
const mapped: ExtensionToolResult["content"] = [];
|
|
42
|
-
for (const item of content) {
|
|
43
|
-
if (item.type === "text") mapped.push({ type: "text", text: item.text });
|
|
44
|
-
else if (item.type === "image") mapped.push({ type: "image", data: item.data, mimeType: item.mimeType });
|
|
45
|
-
else if (item.type === "resource" && "text" in item.resource) mapped.push({ type: "text", text: `Resource URI: ${item.resource.uri}\n${item.resource.text}` });
|
|
46
|
-
else if (item.type === "resource_link") mapped.push({ type: "text", text: `Resource: ${item.name}\nURI: ${item.uri}${item.description ? `\n${item.description}` : ""}` });
|
|
47
|
-
else mapped.push({ type: "text", text: `MCP ${item.type} content retained in the structured result; direct presentation unavailable.` });
|
|
48
|
-
}
|
|
49
|
-
return { content: mapped, ...(isJsonValue(details) ? { details } : {}), ...(isError ? { isError: true } : {}) };
|
|
50
|
-
}
|
|
51
|
-
interface Opened { server: string; client: Client; transport: Transport; tools: readonly Tool[]; schemas: McpSchemas; catalog: string; dirty: boolean; close(): Promise<void> }
|
|
52
|
-
/** A single manager belongs to one app/session generation. Disposing never starts a replacement connection. */
|
|
53
|
-
export class McpManager {
|
|
54
|
-
private readonly servers: McpServers;
|
|
55
|
-
private readonly options: McpManagerOptions;
|
|
56
|
-
private readonly store: McpCredentialsStore;
|
|
57
|
-
private readonly lifetime = new AbortController();
|
|
58
|
-
private readonly opened = new Set<Opened>();
|
|
59
|
-
private readonly oauth = new Map<string, McpOAuthProvider>();
|
|
60
|
-
private readonly statuses = new Map<string, McpServerStatus>();
|
|
61
|
-
private readonly challenges = new Map<string, { resourceMetadataUrl?: URL; scope?: string }>();
|
|
62
|
-
private discovered = false;
|
|
63
|
-
private disposal: Promise<void> | undefined;
|
|
64
|
-
constructor(servers: McpServers, options: McpManagerOptions) {
|
|
65
|
-
this.servers = parseMcpServers(servers); this.options = options;
|
|
66
|
-
this.store = options.credentialsStore ?? new FileMcpCredentialsStore(options.credentialsDirectory);
|
|
67
|
-
}
|
|
68
|
-
private async provider(name: string, config: Extract<McpServerConfig, { transport: "http" }>) {
|
|
69
|
-
let provider = this.oauth.get(name);
|
|
70
|
-
if (!provider) {
|
|
71
|
-
provider = await McpOAuthProvider.create(config.url, typeof config.oauth === "object" ? config.oauth : {}, this.store, this.options.env ?? process.env);
|
|
72
|
-
this.oauth.set(name, provider);
|
|
73
|
-
}
|
|
74
|
-
const challenge = this.challenges.get(name);
|
|
75
|
-
if (challenge) provider.setChallenge(challenge.resourceMetadataUrl, challenge.scope);
|
|
76
|
-
return provider;
|
|
77
|
-
}
|
|
78
|
-
private fetch(signal: AbortSignal, timeoutMs: number): FetchLike {
|
|
79
|
-
return async (input, init) => {
|
|
80
|
-
// Protocol session retirement has its own bounded finalizer after business cancellation.
|
|
81
|
-
// DELETE cannot start a business operation and must survive the owner's aborted request signal.
|
|
82
|
-
const cleanup = init?.method === "DELETE", combined = cleanup ? new AbortController().signal
|
|
83
|
-
: AbortSignal.any([signal, this.lifetime.signal, ...(init?.signal ? [init.signal] : [])]), controller = new AbortController();
|
|
84
|
-
const timer = setTimeout(() => controller.abort(), cleanup ? Math.min(timeoutMs, 5_000) : timeoutMs);
|
|
85
|
-
try {
|
|
86
|
-
const response = await (this.options.fetch ?? fetch)(input, { ...init, signal: AbortSignal.any([combined, controller.signal]), redirect: "manual" });
|
|
87
|
-
// Buffer JSON replies with a streamed cap. SSE remains a stream, also counted before parsing any event.
|
|
88
|
-
if (!response.body) { clearTimeout(timer); return response; }
|
|
89
|
-
const reader = response.body.getReader(); let seen = 0;
|
|
90
|
-
const body = new ReadableStream<Uint8Array>({
|
|
91
|
-
async pull(output) {
|
|
92
|
-
try {
|
|
93
|
-
const next = await reader.read();
|
|
94
|
-
if (next.done) { clearTimeout(timer); reader.releaseLock(); output.close(); return; }
|
|
95
|
-
seen += next.value.byteLength;
|
|
96
|
-
if (seen > MAX_WEB_RESPONSE_BYTES) { clearTimeout(timer); await reader.cancel(); output.error(new McpFailure("protocol_error")); return; }
|
|
97
|
-
output.enqueue(next.value);
|
|
98
|
-
} catch { clearTimeout(timer); output.error(new McpFailure(controller.signal.aborted ? "timeout" : "offline")); }
|
|
99
|
-
}, async cancel() { clearTimeout(timer); await reader.cancel(); }
|
|
100
|
-
});
|
|
101
|
-
return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
102
|
-
} catch (error) { clearTimeout(timer); if (error instanceof McpAuthRequired) throw error; throw new McpFailure(controller.signal.aborted ? "timeout" : "offline"); }
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
private async open(name: string, config: McpServerConfig, signal: AbortSignal): Promise<Opened> {
|
|
106
|
-
signal.throwIfAborted(); this.lifetime.signal.throwIfAborted();
|
|
107
|
-
const timeout = config.timeoutMs ?? 60_000;
|
|
108
|
-
const schemas = new McpSchemas();
|
|
109
|
-
const client = new Client({ name: "xeno", version: "0.5.0" }, { capabilities: { roots: { listChanged: false } },
|
|
110
|
-
versionNegotiation: { mode: "auto" }, inputRequired: { autoFulfill: false }, listMaxPages: 64, jsonSchemaValidator: schemas });
|
|
111
|
-
client.setRequestHandler("roots/list", async () => ({ roots: [{ uri: pathToFileURL(this.options.cwd).href, name: "workspace" }] }));
|
|
112
|
-
// Remote log notifications and stderr are deliberately not copied into logs: they can contain server secrets.
|
|
113
|
-
client.onerror = () => {};
|
|
114
|
-
const combined = AbortSignal.any([signal, this.lifetime.signal]);
|
|
115
|
-
let closingTransport = false;
|
|
116
|
-
let transport: Transport;
|
|
117
|
-
if (config.transport === "stdio") {
|
|
118
|
-
const env: Record<string, string> = { ...getDefaultEnvironment() };
|
|
119
|
-
for (const [key, value] of Object.entries(config.env ?? {})) env[key] = value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match: string, variable: string) => {
|
|
120
|
-
const found = (this.options.env ?? process.env)[variable]; if (found === undefined) throw new McpFailure("auth_required"); return found;
|
|
121
|
-
});
|
|
122
|
-
transport = new StdioClientTransport({ command: config.command, args: config.args ?? [], env, cwd: this.options.cwd, stderr: "ignore", maxBufferSize: MAX_WEB_RESPONSE_BYTES });
|
|
123
|
-
} else {
|
|
124
|
-
const headers = new Headers(config.headers), env = this.options.env ?? process.env, baseFetch = this.fetch(combined, timeout);
|
|
125
|
-
const fetcher: FetchLike = async (input, init) => {
|
|
126
|
-
const response = await baseFetch(input, init);
|
|
127
|
-
if (response.status === 401 || response.status === 403) {
|
|
128
|
-
const challenge = extractWWWAuthenticateParams(response);
|
|
129
|
-
this.challenges.set(name, { ...(challenge.resourceMetadataUrl ? { resourceMetadataUrl: challenge.resourceMetadataUrl } : {}), ...(challenge.scope ? { scope: challenge.scope } : {}) });
|
|
130
|
-
}
|
|
131
|
-
return response;
|
|
132
|
-
};
|
|
133
|
-
for (const [header, variable] of Object.entries(config.headersEnv ?? {})) {
|
|
134
|
-
const value = env[variable]; if (!value) throw new McpFailure("auth_required"); headers.set(header, value);
|
|
135
|
-
}
|
|
136
|
-
const provider = config.oauth ? await this.provider(name, config) : undefined;
|
|
137
|
-
if (config.bearerTokenEnv && !env[config.bearerTokenEnv]) throw new McpFailure("auth_required");
|
|
138
|
-
transport = new StreamableHTTPClientTransport(new URL(config.url), { requestInit: { headers, redirect: "manual" }, fetch: fetcher,
|
|
139
|
-
// A token-only provider has no onUnauthorized callback: the SDK cannot re-authorize and resend a business call.
|
|
140
|
-
...((provider || config.bearerTokenEnv) ? { authProvider: { token: async () => provider ? closingTransport ? provider.tokens()?.access_token : await provider.token(fetcher) : env[config.bearerTokenEnv!] } } : {}),
|
|
141
|
-
onInsufficientScope: "throw", reconnectionOptions: { maxRetries: 0, initialReconnectionDelay: 1_000, maxReconnectionDelay: 30_000, reconnectionDelayGrowFactor: 1.5 } });
|
|
142
|
-
}
|
|
143
|
-
const cancel = () => { void transport.close(); };
|
|
144
|
-
let shuttingDown: Promise<void> | undefined;
|
|
145
|
-
const shutdown = () => shuttingDown ??= (async () => {
|
|
146
|
-
closingTransport = true;
|
|
147
|
-
try {
|
|
148
|
-
if (transport instanceof StreamableHTTPClientTransport) await transport.terminateSession();
|
|
149
|
-
} catch (error) { throw new McpFailure(fault(error).code); }
|
|
150
|
-
finally { await client.close(); }
|
|
151
|
-
})();
|
|
152
|
-
combined.addEventListener("abort", cancel, { once: true });
|
|
153
|
-
try {
|
|
154
|
-
await client.connect(transport, { signal: combined, timeout, maxTotalTimeout: timeout });
|
|
155
|
-
const discovered = await client.listTools(undefined, { signal: combined, timeout, cacheMode: "bypass" });
|
|
156
|
-
if (discovered.tools.length > 1_024) throw new McpFailure("protocol_error");
|
|
157
|
-
const names = new Set(discovered.tools.map(tool => tool.name));
|
|
158
|
-
if (names.size !== discovered.tools.length) throw new McpFailure("protocol_error");
|
|
159
|
-
const tools = immutable(structuredClone(discovered.tools));
|
|
160
|
-
for (const tool of tools) { schemas.validator(tool.inputSchema); if (tool.outputSchema) schemas.validator(tool.outputSchema); }
|
|
161
|
-
const opened: Opened = { server: name, client, transport, tools, schemas, catalog: JSON.stringify(tools), dirty: false,
|
|
162
|
-
close: async () => { combined.removeEventListener("abort", cancel); try { await shutdown(); } finally { this.opened.delete(opened); } } };
|
|
163
|
-
for (const method of ["notifications/tools/list_changed", "notifications/prompts/list_changed", "notifications/resources/list_changed"] as const)
|
|
164
|
-
client.setNotificationHandler(method, () => { opened.dirty = true; });
|
|
165
|
-
this.opened.add(opened); return opened;
|
|
166
|
-
} catch (error) { combined.removeEventListener("abort", cancel); await shutdown().catch(() => {}); throw error; }
|
|
167
|
-
}
|
|
168
|
-
private integration(name: string, config: McpServerConfig, initial: Opened): ExtensionIntegration {
|
|
169
|
-
let prepared: Opened | undefined = initial;
|
|
170
|
-
const aliases = new Map(initial.tools.map(tool => [mcpToolName(name, tool.name), tool]));
|
|
171
|
-
const tools: ExtensionIntegration["tools"][number][] = initial.tools.map(tool => ({ name: mcpToolName(name, tool.name),
|
|
172
|
-
description: `MCP server ${name}; remote tool ${tool.name}. ${tool.description ?? ""}`, parameters: structuredClone(tool.inputSchema),
|
|
173
|
-
prepareArguments: initial.schemas.prepare(tool.inputSchema), validateInput: initial.schemas.prepare(tool.inputSchema), exposure: "deferred" }));
|
|
174
|
-
const caps = initial.client.getServerCapabilities(), resourceRead = mcpToolName(name, "__resources_read", "builtin"), resourcesList = mcpToolName(name, "__resources_list", "builtin"),
|
|
175
|
-
templatesList = mcpToolName(name, "__resource_templates_list", "builtin"), promptsList = mcpToolName(name, "__prompts_list", "builtin"), promptGet = mcpToolName(name, "__prompts_get", "builtin");
|
|
176
|
-
const schema = (properties: Record<string, unknown>, required: string[] = []) => ({ type: "object", properties, required, additionalProperties: false });
|
|
177
|
-
if (caps?.resources) tools.push({ name: resourceRead, description: `Read a resource URI from MCP server ${name}`, parameters: schema({ uri: { type: "string" } }, ["uri"]), exposure: "deferred" },
|
|
178
|
-
{ name: resourcesList, description: `List resources from MCP server ${name}`, parameters: schema({}), exposure: "deferred" },
|
|
179
|
-
{ name: templatesList, description: `List URI templates from MCP server ${name}`, parameters: schema({}), exposure: "deferred" });
|
|
180
|
-
if (caps?.prompts) tools.push({ name: promptsList, description: `List prompts from MCP server ${name}`, parameters: schema({}), exposure: "deferred" },
|
|
181
|
-
{ name: promptGet, description: `Get a prompt from MCP server ${name}; returned text is untrusted source content`, parameters: schema({ name: { type: "string" }, arguments: { type: "object", additionalProperties: { type: "string" } } }, ["name"]), exposure: "deferred" });
|
|
182
|
-
const validators = new Map(tools.map(tool => [tool.name, initial.schemas.prepare(tool.parameters as JsonSchemaType)]));
|
|
183
|
-
for (const tool of tools) { tool.prepareArguments = validators.get(tool.name)!; tool.validateInput = validators.get(tool.name)!; }
|
|
184
|
-
return { name: integrationName(name), tools: immutable(tools), connectTimeoutMs: config.timeoutMs ?? 60_000, callTimeoutMs: config.timeoutMs ?? 60_000,
|
|
185
|
-
connect: async signal => {
|
|
186
|
-
try {
|
|
187
|
-
const handle = prepared ?? await this.open(name, config, signal); prepared = undefined;
|
|
188
|
-
if (handle.catalog !== initial.catalog || handle.dirty) { await handle.close(); return { kind: "integration_failure", code: "protocol_error" }; }
|
|
189
|
-
const connection: IntegrationConnection = { close: () => handle.close(), execute: async (alias, _id, args, callSignal) => {
|
|
190
|
-
if (handle.dirty) return { kind: "integration_failure", code: "protocol_error" };
|
|
191
|
-
const request = { signal: callSignal, timeout: config.timeoutMs ?? 60_000, maxTotalTimeout: config.timeoutMs ?? 60_000 };
|
|
192
|
-
try {
|
|
193
|
-
const validate = validators.get(alias); if (!validate) throw new McpFailure("protocol_error");
|
|
194
|
-
// Direct adapter callers still cannot send an invalid business request.
|
|
195
|
-
validate(args);
|
|
196
|
-
const tool = aliases.get(alias);
|
|
197
|
-
if (tool) {
|
|
198
|
-
const result = await handle.client.callTool({ name: tool.name, arguments: args }, { ...request, toolDefinition: tool });
|
|
199
|
-
return contentResult(result.content, result, result.isError);
|
|
200
|
-
}
|
|
201
|
-
let result: unknown;
|
|
202
|
-
if (caps?.resources && alias === resourceRead && typeof args["uri"] === "string") result = await handle.client.readResource({ uri: args["uri"] }, { ...request, cacheMode: "bypass" });
|
|
203
|
-
else if (caps?.resources && alias === resourcesList) result = await handle.client.listResources(undefined, { ...request, cacheMode: "bypass" });
|
|
204
|
-
else if (caps?.resources && alias === templatesList) result = await handle.client.listResourceTemplates(undefined, { ...request, cacheMode: "bypass" });
|
|
205
|
-
else if (caps?.prompts && alias === promptsList) result = await handle.client.listPrompts(undefined, { ...request, cacheMode: "bypass" });
|
|
206
|
-
else if (caps?.prompts && alias === promptGet && typeof args["name"] === "string") {
|
|
207
|
-
const values = args["arguments"];
|
|
208
|
-
if (values !== undefined && (!values || typeof values !== "object" || Array.isArray(values) || Object.values(values).some(value => typeof value !== "string"))) throw new McpFailure("protocol_error");
|
|
209
|
-
result = await handle.client.getPrompt({ name: args["name"], ...(values ? { arguments: values as Record<string, string> } : {}) }, request);
|
|
210
|
-
} else throw new McpFailure("protocol_error");
|
|
211
|
-
return { content: [{ type: "text", text: JSON.stringify(result) }], ...(isJsonValue(result) ? { details: result } : {}) };
|
|
212
|
-
} catch (error) {
|
|
213
|
-
if (error instanceof ToolFault) return { isError: true, content: [{ type: "text", text: error.message }] };
|
|
214
|
-
const failure = fault(error);
|
|
215
|
-
this.statuses.set(name, { name, transport: config.transport, state: failure.code, tools: tools.length });
|
|
216
|
-
return failure;
|
|
217
|
-
}
|
|
218
|
-
} }; return connection;
|
|
219
|
-
} catch (error) { return fault(error); }
|
|
220
|
-
} };
|
|
221
|
-
}
|
|
222
|
-
async discover(signal?: AbortSignal): Promise<{ integrations: ExtensionIntegration[]; statuses: McpServerStatus[] }> {
|
|
223
|
-
// Registered generations keep ownership until disposal; rediscovery must never overlap their connections.
|
|
224
|
-
if (this.discovered) throw new McpFailure("protocol_error");
|
|
225
|
-
this.discovered = true;
|
|
226
|
-
const combined = AbortSignal.any([this.lifetime.signal, ...(signal ? [signal] : [])]), integrations: ExtensionIntegration[] = [];
|
|
227
|
-
for (const [name, config] of Object.entries(this.servers)) {
|
|
228
|
-
if (config.enabled === false) { this.statuses.set(name, { name, transport: config.transport, state: "disabled", tools: 0 }); continue; }
|
|
229
|
-
try {
|
|
230
|
-
const opened = await this.open(name, config, combined), definition = this.integration(name, config, opened);
|
|
231
|
-
if (definition.tools.length) integrations.push(definition);
|
|
232
|
-
this.statuses.set(name, { name, transport: config.transport, state: "ready", tools: definition.tools.length });
|
|
233
|
-
} catch (error) { this.statuses.set(name, { name, transport: config.transport, state: fault(error).code, tools: 0,
|
|
234
|
-
...(error instanceof McpSchemaError ? { diagnostic: error.message } : {}) }); }
|
|
235
|
-
}
|
|
236
|
-
return { integrations, statuses: this.status() };
|
|
237
|
-
}
|
|
238
|
-
status(): McpServerStatus[] { return [...this.statuses.values()].map(status => ({ ...status })); }
|
|
239
|
-
async login(name: string, onAuthorizationUrl: (url: URL) => void | Promise<void>, signal?: AbortSignal): Promise<void> {
|
|
240
|
-
const config = this.servers[name];
|
|
241
|
-
if (!config || config.transport !== "http" || config.oauth === false || config.bearerTokenEnv) throw new McpAuthRequired();
|
|
242
|
-
const timeout = AbortSignal.timeout(300_000), combined = AbortSignal.any([timeout, this.lifetime.signal, ...(signal ? [signal] : [])]);
|
|
243
|
-
try { await (await this.provider(name, config)).login(onAuthorizationUrl, this.fetch(combined, 30_000), combined); }
|
|
244
|
-
catch { throw new McpAuthRequired(); }
|
|
245
|
-
}
|
|
246
|
-
async logout(name: string): Promise<void> {
|
|
247
|
-
const config = this.servers[name]; if (!config || config.transport !== "http") throw new McpAuthRequired();
|
|
248
|
-
await Promise.all([...this.opened].filter(handle => handle.server === name).map(handle => handle.close()));
|
|
249
|
-
await (await this.provider(name, config)).invalidateCredentials("all"); this.oauth.delete(name);
|
|
250
|
-
this.statuses.set(name, { name, transport: config.transport, state: "auth_required", tools: 0 });
|
|
251
|
-
}
|
|
252
|
-
dispose(): Promise<void> {
|
|
253
|
-
if (this.disposal) return this.disposal;
|
|
254
|
-
this.lifetime.abort();
|
|
255
|
-
return this.disposal = (async () => {
|
|
256
|
-
const results = await Promise.allSettled([...this.opened].map(opened => opened.close()));
|
|
257
|
-
const failure = results.find(result => result.status === "rejected");
|
|
258
|
-
if (failure?.status === "rejected") throw new McpFailure(fault(failure.reason).code);
|
|
259
|
-
})();
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** JsonValue is intentionally used only after validation; remote schema/result types never own evidence. */
|
|
264
|
-
export function mcpStatusData(statuses: readonly McpServerStatus[]): JsonValue { return statuses.map(status => ({ ...status })); }
|