one-code-extension 0.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/LICENSE +32 -0
- package/README.md +202 -0
- package/agents/explore.md +24 -0
- package/agents/general-purpose.md +21 -0
- package/agents/plan.md +22 -0
- package/docs/decisions/auto-mode.md +526 -0
- package/docs/decisions/branding.md +127 -0
- package/docs/decisions/code-review-remediation.md +106 -0
- package/docs/decisions/compaction.md +140 -0
- package/docs/decisions/distribution.md +133 -0
- package/docs/decisions/hooks.md +64 -0
- package/docs/decisions/lsp.md +58 -0
- package/docs/decisions/mcp.md +104 -0
- package/docs/decisions/memory-state.md +170 -0
- package/docs/decisions/model-policy.md +126 -0
- package/docs/decisions/modes.md +178 -0
- package/docs/decisions/skills-plugins.md +56 -0
- package/docs/decisions/subagents-workflows.md +225 -0
- package/docs/decisions/system-prompt.md +98 -0
- package/docs/decisions/tools.md +407 -0
- package/docs/decisions/tui.md +202 -0
- package/docs/decisions.md +148 -0
- package/extensions/ask-user/index.ts +124 -0
- package/extensions/ask-user/questions.ts +60 -0
- package/extensions/auto-mode/actions.ts +78 -0
- package/extensions/auto-mode/classifier.ts +388 -0
- package/extensions/auto-mode/config.ts +237 -0
- package/extensions/auto-mode/decision-log.ts +60 -0
- package/extensions/auto-mode/defaults.ts +65 -0
- package/extensions/auto-mode/instructions.ts +74 -0
- package/extensions/auto-mode/model-picker.ts +225 -0
- package/extensions/auto-mode/model-select.ts +178 -0
- package/extensions/auto-mode/paths.ts +109 -0
- package/extensions/auto-mode/pause.ts +96 -0
- package/extensions/auto-mode/prompt.ts +307 -0
- package/extensions/auto-mode/rules.ts +96 -0
- package/extensions/auto-mode/safety-floor.ts +100 -0
- package/extensions/auto-mode/sensitive.ts +137 -0
- package/extensions/auto-mode/shell-analysis.ts +702 -0
- package/extensions/background/index.ts +405 -0
- package/extensions/background/registry.ts +81 -0
- package/extensions/background/wakeup.ts +41 -0
- package/extensions/bash/background.ts +136 -0
- package/extensions/bash/index.ts +149 -0
- package/extensions/branding/assistant-marker.ts +53 -0
- package/extensions/branding/index.ts +366 -0
- package/extensions/branding/prompt-editor.ts +42 -0
- package/extensions/branding/prompt-marker.ts +29 -0
- package/extensions/branding/startup.ts +152 -0
- package/extensions/claude-compat/index.ts +50 -0
- package/extensions/claude-context/index.ts +79 -0
- package/extensions/compaction/index.ts +231 -0
- package/extensions/compaction/prompt.ts +191 -0
- package/extensions/context-management/index.ts +147 -0
- package/extensions/effort/index.ts +216 -0
- package/extensions/effort/slider.ts +253 -0
- package/extensions/exit/index.ts +28 -0
- package/extensions/file-tracker/index.ts +103 -0
- package/extensions/file-tracker/tracker.ts +145 -0
- package/extensions/hooks/debug.ts +53 -0
- package/extensions/hooks/executor.ts +132 -0
- package/extensions/hooks/index.ts +310 -0
- package/extensions/hooks/matcher.ts +72 -0
- package/extensions/hooks/plugin-hooks.ts +79 -0
- package/extensions/hooks/protocol.ts +151 -0
- package/extensions/hooks/settings.ts +163 -0
- package/extensions/hooks/trust.ts +151 -0
- package/extensions/lib/branch-restore.ts +35 -0
- package/extensions/lib/claude-context.ts +148 -0
- package/extensions/lib/deferred.ts +119 -0
- package/extensions/lib/git.ts +14 -0
- package/extensions/lib/memory.ts +219 -0
- package/extensions/lib/model-policy.ts +436 -0
- package/extensions/lib/model-tier.ts +99 -0
- package/extensions/lib/notifications.ts +18 -0
- package/extensions/lib/paths.ts +26 -0
- package/extensions/lib/per-cwd.ts +21 -0
- package/extensions/lib/persisted-output.ts +70 -0
- package/extensions/lib/pi-version.ts +54 -0
- package/extensions/lib/plugins.ts +227 -0
- package/extensions/lib/reminders.ts +212 -0
- package/extensions/lib/scratchpad.ts +68 -0
- package/extensions/lib/tui-render.ts +522 -0
- package/extensions/lsp/client.ts +278 -0
- package/extensions/lsp/format.ts +46 -0
- package/extensions/lsp/index.ts +193 -0
- package/extensions/lsp/protocol.ts +64 -0
- package/extensions/lsp/servers.ts +79 -0
- package/extensions/mcp/client.ts +205 -0
- package/extensions/mcp/config.ts +199 -0
- package/extensions/mcp/index.ts +373 -0
- package/extensions/mcp/schema.ts +161 -0
- package/extensions/memory/index.ts +90 -0
- package/extensions/notebook/index.ts +77 -0
- package/extensions/notebook/notebook.ts +108 -0
- package/extensions/permissions/index.ts +876 -0
- package/extensions/permissions/matcher.ts +455 -0
- package/extensions/permissions/modes.ts +107 -0
- package/extensions/permissions/protected-paths.ts +125 -0
- package/extensions/permissions/settings.ts +98 -0
- package/extensions/plan-mode/index.ts +209 -0
- package/extensions/plan-mode/reminder.ts +33 -0
- package/extensions/plan-mode/slug.ts +28 -0
- package/extensions/plan-mode/viewer.ts +124 -0
- package/extensions/plugins/index.ts +115 -0
- package/extensions/plugins/template.ts +42 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +24 -0
- package/extensions/skill/index.ts +170 -0
- package/extensions/subagents/agents.ts +126 -0
- package/extensions/subagents/child.ts +414 -0
- package/extensions/subagents/default-model.ts +122 -0
- package/extensions/subagents/index.ts +1119 -0
- package/extensions/subagents/model-select.ts +346 -0
- package/extensions/subagents/rpc-turns.ts +123 -0
- package/extensions/subagents/runs.ts +93 -0
- package/extensions/subagents/usage.ts +53 -0
- package/extensions/subagents/worktree.ts +69 -0
- package/extensions/system-prompt/environment.ts +35 -0
- package/extensions/system-prompt/index.ts +54 -0
- package/extensions/system-prompt/template.ts +103 -0
- package/extensions/system-prompt/tiers/common.ts +63 -0
- package/extensions/system-prompt/tiers/frontier.ts +24 -0
- package/extensions/system-prompt/tiers/low.ts +71 -0
- package/extensions/system-prompt/tiers/mid.ts +56 -0
- package/extensions/system-reminder/index.ts +37 -0
- package/extensions/tasks/index.ts +140 -0
- package/extensions/tasks/store.ts +174 -0
- package/extensions/todo/index.ts +133 -0
- package/extensions/tool-search/index.ts +168 -0
- package/extensions/tool-style/index.ts +84 -0
- package/extensions/web/index.ts +43 -0
- package/extensions/web-fetch/extract.ts +139 -0
- package/extensions/web-fetch/index.ts +291 -0
- package/extensions/web-fetch/summarize.ts +78 -0
- package/extensions/workflow/agent-session.ts +368 -0
- package/extensions/workflow/globals.ts +251 -0
- package/extensions/workflow/index.ts +364 -0
- package/extensions/workflow/journal.ts +75 -0
- package/extensions/workflow/permission-gate.ts +71 -0
- package/extensions/workflow/run-manager.ts +306 -0
- package/extensions/workflow/saved-workflows.ts +45 -0
- package/extensions/workflow/script-source.ts +201 -0
- package/extensions/workflow/types.ts +99 -0
- package/extensions/workflow/vm-runtime.ts +31 -0
- package/extensions/workflow/widget.ts +56 -0
- package/extensions/worktree/index.ts +266 -0
- package/extensions/worktree/rewrite.ts +59 -0
- package/package.json +101 -0
- package/themes/one-code-light.json +76 -0
- package/themes/one-code.json +76 -0
- package/types/app.d.ts +5 -0
- package/types/community-packages.d.ts +12 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP connection management. The official SDK carries the protocol; this owns
|
|
3
|
+
* lifecycle, timeouts, and failure reporting.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
|
+
import type { McpServer } from "./config.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The MCP SDK costs ~70-80ms to load (findings §15), so it is imported on the
|
|
11
|
+
* first connect rather than at startup — a session with no configured servers
|
|
12
|
+
* never pays for it. The promise is cached: concurrent connects share one load.
|
|
13
|
+
*/
|
|
14
|
+
let sdkPromise:
|
|
15
|
+
| Promise<{
|
|
16
|
+
Client: typeof import("@modelcontextprotocol/sdk/client/index.js").Client;
|
|
17
|
+
StdioClientTransport: typeof import("@modelcontextprotocol/sdk/client/stdio.js").StdioClientTransport;
|
|
18
|
+
StreamableHTTPClientTransport: typeof import("@modelcontextprotocol/sdk/client/streamableHttp.js").StreamableHTTPClientTransport;
|
|
19
|
+
}>
|
|
20
|
+
| undefined;
|
|
21
|
+
|
|
22
|
+
function loadSdk() {
|
|
23
|
+
sdkPromise ??= Promise.all([
|
|
24
|
+
import("@modelcontextprotocol/sdk/client/index.js"),
|
|
25
|
+
import("@modelcontextprotocol/sdk/client/stdio.js"),
|
|
26
|
+
import("@modelcontextprotocol/sdk/client/streamableHttp.js"),
|
|
27
|
+
]).then(([client, stdio, http]) => ({
|
|
28
|
+
Client: client.Client,
|
|
29
|
+
StdioClientTransport: stdio.StdioClientTransport,
|
|
30
|
+
StreamableHTTPClientTransport: http.StreamableHTTPClientTransport,
|
|
31
|
+
}));
|
|
32
|
+
return sdkPromise;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const CONNECT_TIMEOUT_MS = 20_000;
|
|
36
|
+
const CALL_TIMEOUT_MS = 120_000;
|
|
37
|
+
|
|
38
|
+
export interface McpTool {
|
|
39
|
+
name: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
inputSchema?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface McpResource {
|
|
45
|
+
uri: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
mimeType?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface Connection {
|
|
52
|
+
server: McpServer;
|
|
53
|
+
client: Client;
|
|
54
|
+
tools: McpTool[];
|
|
55
|
+
resources: McpResource[];
|
|
56
|
+
/** The server's own usage instructions from its initialize result, if any. */
|
|
57
|
+
instructions?: string;
|
|
58
|
+
/** Non-fatal problems after a successful connect (e.g. listTools failed). */
|
|
59
|
+
warnings: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface FailedConnection {
|
|
63
|
+
server: McpServer;
|
|
64
|
+
error: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function withTimeout<T>(promise: Promise<T>, ms: number, what: string): Promise<T> {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const timer = setTimeout(() => reject(new Error(`${what} timed out after ${ms}ms`)), ms);
|
|
70
|
+
timer.unref?.();
|
|
71
|
+
promise.then(
|
|
72
|
+
(value) => {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
resolve(value);
|
|
75
|
+
},
|
|
76
|
+
(error) => {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
reject(error);
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function connect(server: McpServer): Promise<Connection> {
|
|
85
|
+
const { Client, StdioClientTransport, StreamableHTTPClientTransport } = await loadSdk();
|
|
86
|
+
const client = new Client({ name: "one-code", version: "0.1.0" }, { capabilities: {} });
|
|
87
|
+
|
|
88
|
+
const transport =
|
|
89
|
+
server.kind === "stdio"
|
|
90
|
+
? new StdioClientTransport({
|
|
91
|
+
command: server.command,
|
|
92
|
+
args: server.args,
|
|
93
|
+
env: { ...(process.env as Record<string, string>), ...(server.env ?? {}) },
|
|
94
|
+
stderr: "pipe",
|
|
95
|
+
})
|
|
96
|
+
: new StreamableHTTPClientTransport(new URL(server.url), {
|
|
97
|
+
requestInit: server.headers ? { headers: server.headers } : undefined,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await withTimeout(client.connect(transport), CONNECT_TIMEOUT_MS, `connecting to "${server.name}"`);
|
|
101
|
+
|
|
102
|
+
const warnings: string[] = [];
|
|
103
|
+
|
|
104
|
+
// Each list call can take up to CONNECT_TIMEOUT_MS on a slow server; run
|
|
105
|
+
// them concurrently instead of stacking their worst-case latencies.
|
|
106
|
+
const [toolsResult, resourcesResult] = await Promise.allSettled([
|
|
107
|
+
withTimeout(client.listTools(), CONNECT_TIMEOUT_MS, `listing tools of "${server.name}"`),
|
|
108
|
+
withTimeout(client.listResources(), CONNECT_TIMEOUT_MS, `listing resources of "${server.name}"`),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
let tools: McpTool[] = [];
|
|
112
|
+
if (toolsResult.status === "fulfilled") {
|
|
113
|
+
tools = (toolsResult.value.tools ?? []) as McpTool[];
|
|
114
|
+
} else {
|
|
115
|
+
// A server that connects but fails listTools would otherwise register
|
|
116
|
+
// zero tools with no signal anywhere — surface it as a warning.
|
|
117
|
+
warnings.push(`connected, but listing tools failed: ${(toolsResult.reason as Error).message}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let resources: McpResource[] = [];
|
|
121
|
+
if (resourcesResult.status === "fulfilled") {
|
|
122
|
+
resources = (resourcesResult.value.resources ?? []) as McpResource[];
|
|
123
|
+
} else {
|
|
124
|
+
// Resources are optional in MCP; "method not found" is normal. Anything
|
|
125
|
+
// else (a timeout, a protocol error) is worth a warning.
|
|
126
|
+
const message = (resourcesResult.reason as Error).message;
|
|
127
|
+
if (!/method not found|-32601/i.test(message)) {
|
|
128
|
+
warnings.push(`connected, but listing resources failed: ${message}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const instructions = client.getInstructions()?.trim() || undefined;
|
|
133
|
+
|
|
134
|
+
return { server, client, tools, resources, instructions, warnings };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const INSTRUCTIONS_CAP = 3000;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The reminder carrying servers' own usage instructions, formatted the way
|
|
141
|
+
* Claude Code injects them (findings §14). Servers without instructions are
|
|
142
|
+
* skipped; returns undefined when none have any.
|
|
143
|
+
*/
|
|
144
|
+
export function mcpInstructionsReminder(
|
|
145
|
+
connections: Array<{ server: { name: string }; instructions?: string }>,
|
|
146
|
+
): string | undefined {
|
|
147
|
+
const withInstructions = connections.filter((c) => c.instructions);
|
|
148
|
+
if (withInstructions.length === 0) return undefined;
|
|
149
|
+
const sections = withInstructions.map((c) => {
|
|
150
|
+
const text =
|
|
151
|
+
c.instructions!.length > INSTRUCTIONS_CAP ? `${c.instructions!.slice(0, INSTRUCTIONS_CAP)}… [truncated]` : c.instructions!;
|
|
152
|
+
return `## ${c.server.name}\n${text}`;
|
|
153
|
+
});
|
|
154
|
+
return [
|
|
155
|
+
"# MCP Server Instructions",
|
|
156
|
+
"",
|
|
157
|
+
"The following MCP servers have provided instructions for how to use their tools and resources:",
|
|
158
|
+
"",
|
|
159
|
+
sections.join("\n\n"),
|
|
160
|
+
].join("\n");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function callTool(
|
|
164
|
+
connection: Connection,
|
|
165
|
+
toolName: string,
|
|
166
|
+
args: Record<string, unknown>,
|
|
167
|
+
): Promise<{ content?: unknown[]; isError?: boolean }> {
|
|
168
|
+
const result = await withTimeout(
|
|
169
|
+
connection.client.callTool({ name: toolName, arguments: args }),
|
|
170
|
+
CALL_TIMEOUT_MS,
|
|
171
|
+
`calling "${toolName}"`,
|
|
172
|
+
);
|
|
173
|
+
return result as { content?: unknown[]; isError?: boolean };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function readResource(connection: Connection, uri: string): Promise<{ contents?: unknown[] }> {
|
|
177
|
+
return (await withTimeout(
|
|
178
|
+
connection.client.readResource({ uri }),
|
|
179
|
+
CALL_TIMEOUT_MS,
|
|
180
|
+
`reading resource "${uri}"`,
|
|
181
|
+
)) as { contents?: unknown[] };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* `resources/directory/read` — directory listing for resources. Not in the SDK's
|
|
186
|
+
* typed surface (servers opt in), so it goes through the generic request path.
|
|
187
|
+
* The SDK only calls `safeParse` on the result schema, and the response shape is
|
|
188
|
+
* server-defined, so a passthrough schema beats pinning one the spec hasn't settled.
|
|
189
|
+
*/
|
|
190
|
+
export async function readResourceDir(connection: Connection, uri: string): Promise<Record<string, unknown>> {
|
|
191
|
+
const passthrough = { safeParse: (data: unknown) => ({ success: true as const, data }) };
|
|
192
|
+
return (await withTimeout(
|
|
193
|
+
connection.client.request({ method: "resources/directory/read", params: { uri } }, passthrough as never),
|
|
194
|
+
CALL_TIMEOUT_MS,
|
|
195
|
+
`listing directory "${uri}"`,
|
|
196
|
+
)) as Record<string, unknown>;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function close(connection: Connection): Promise<void> {
|
|
200
|
+
try {
|
|
201
|
+
await withTimeout(connection.client.close(), 3000, "closing connection");
|
|
202
|
+
} catch {
|
|
203
|
+
// A server that won't close cleanly should not block shutdown.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server configuration in Claude Code's format (pure).
|
|
3
|
+
*
|
|
4
|
+
* Sources, lowest to highest precedence:
|
|
5
|
+
* ~/.claude.json (user, global)
|
|
6
|
+
* <cwd>/.mcp.json (project, checked in — walked up to the repo root)
|
|
7
|
+
* <cwd>/.claude/settings.local.json (project, personal)
|
|
8
|
+
*
|
|
9
|
+
* Every file holds an `mcpServers` object. A stdio server has `command` (plus
|
|
10
|
+
* optional `args`, `env`); a remote one has `url` and optional `type`/`headers`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
|
|
16
|
+
export interface StdioServer {
|
|
17
|
+
kind: "stdio";
|
|
18
|
+
name: string;
|
|
19
|
+
command: string;
|
|
20
|
+
args: string[];
|
|
21
|
+
env?: Record<string, string>;
|
|
22
|
+
source: string;
|
|
23
|
+
/** Config referenced these environment variables and they are not set. */
|
|
24
|
+
missingEnv?: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface HttpServer {
|
|
28
|
+
kind: "http";
|
|
29
|
+
name: string;
|
|
30
|
+
url: string;
|
|
31
|
+
headers?: Record<string, string>;
|
|
32
|
+
source: string;
|
|
33
|
+
/** Config referenced these environment variables and they are not set. */
|
|
34
|
+
missingEnv?: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type McpServer = StdioServer | HttpServer;
|
|
38
|
+
|
|
39
|
+
interface RawServer {
|
|
40
|
+
command?: unknown;
|
|
41
|
+
args?: unknown;
|
|
42
|
+
env?: unknown;
|
|
43
|
+
url?: unknown;
|
|
44
|
+
type?: unknown;
|
|
45
|
+
headers?: unknown;
|
|
46
|
+
disabled?: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readJson(path: string): Record<string, unknown> | undefined {
|
|
50
|
+
if (!existsSync(path)) return undefined;
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(readFileSync(path, "utf-8")) as Record<string, unknown>;
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function asStringRecord(value: unknown): Record<string, string> | undefined {
|
|
59
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
60
|
+
const out: Record<string, string> = {};
|
|
61
|
+
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
|
|
62
|
+
if (typeof item === "string") out[key] = item;
|
|
63
|
+
}
|
|
64
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Expands $VAR and ${VAR} in a config string, as Claude Code does. */
|
|
68
|
+
export function expandEnv(value: string, env: Record<string, string | undefined>): string {
|
|
69
|
+
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, braced, bare) => {
|
|
70
|
+
return env[braced ?? bare] ?? "";
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Variables a value references that are not set. Expanding them to "" produces
|
|
76
|
+
* configuration that looks valid and fails confusingly at the server — a real
|
|
77
|
+
* example being `Authorization: "Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}"`
|
|
78
|
+
* becoming `"Bearer "`, which the endpoint rejects as a badly formatted header.
|
|
79
|
+
*/
|
|
80
|
+
export function missingEnvVars(value: string, env: Record<string, string | undefined>): string[] {
|
|
81
|
+
const missing: string[] = [];
|
|
82
|
+
const pattern = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
83
|
+
let match = pattern.exec(value);
|
|
84
|
+
while (match) {
|
|
85
|
+
const name = match[1] ?? match[2];
|
|
86
|
+
if (!env[name]) missing.push(name);
|
|
87
|
+
match = pattern.exec(value);
|
|
88
|
+
}
|
|
89
|
+
return [...new Set(missing)];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function parseServer(
|
|
93
|
+
name: string,
|
|
94
|
+
raw: RawServer,
|
|
95
|
+
source: string,
|
|
96
|
+
env: Record<string, string | undefined>,
|
|
97
|
+
): McpServer | undefined {
|
|
98
|
+
if (raw.disabled === true) return undefined;
|
|
99
|
+
|
|
100
|
+
if (typeof raw.url === "string" && raw.url.trim()) {
|
|
101
|
+
const headers = asStringRecord(raw.headers);
|
|
102
|
+
const missing = [
|
|
103
|
+
...missingEnvVars(raw.url, env),
|
|
104
|
+
...Object.values(headers ?? {}).flatMap((value) => missingEnvVars(value, env)),
|
|
105
|
+
];
|
|
106
|
+
return {
|
|
107
|
+
kind: "http",
|
|
108
|
+
name,
|
|
109
|
+
url: expandEnv(raw.url, env),
|
|
110
|
+
headers: headers
|
|
111
|
+
? Object.fromEntries(Object.entries(headers).map(([k, v]) => [k, expandEnv(v, env)]))
|
|
112
|
+
: undefined,
|
|
113
|
+
source,
|
|
114
|
+
missingEnv: missing.length > 0 ? [...new Set(missing)] : undefined,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (typeof raw.command === "string" && raw.command.trim()) {
|
|
119
|
+
const args = Array.isArray(raw.args)
|
|
120
|
+
? raw.args.filter((a): a is string => typeof a === "string").map((a) => expandEnv(a, env))
|
|
121
|
+
: [];
|
|
122
|
+
const rawEnv = asStringRecord(raw.env);
|
|
123
|
+
const missing = [
|
|
124
|
+
...missingEnvVars(raw.command, env),
|
|
125
|
+
...(Array.isArray(raw.args) ? raw.args : [])
|
|
126
|
+
.filter((a): a is string => typeof a === "string")
|
|
127
|
+
.flatMap((a) => missingEnvVars(a, env)),
|
|
128
|
+
...Object.values(rawEnv ?? {}).flatMap((value) => missingEnvVars(value, env)),
|
|
129
|
+
];
|
|
130
|
+
return {
|
|
131
|
+
kind: "stdio",
|
|
132
|
+
name,
|
|
133
|
+
command: expandEnv(raw.command, env),
|
|
134
|
+
args,
|
|
135
|
+
env: rawEnv ? Object.fromEntries(Object.entries(rawEnv).map(([k, v]) => [k, expandEnv(v, env)])) : undefined,
|
|
136
|
+
source,
|
|
137
|
+
missingEnv: missing.length > 0 ? [...new Set(missing)] : undefined,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** `.mcp.json` from cwd upward, so a repo-root config applies in subdirectories. */
|
|
145
|
+
export function findProjectConfigs(cwd: string): string[] {
|
|
146
|
+
const found: string[] = [];
|
|
147
|
+
let dir = cwd;
|
|
148
|
+
while (true) {
|
|
149
|
+
const candidate = join(dir, ".mcp.json");
|
|
150
|
+
if (existsSync(candidate)) found.push(candidate);
|
|
151
|
+
if (existsSync(join(dir, ".git"))) break;
|
|
152
|
+
const parent = dirname(dir);
|
|
153
|
+
if (parent === dir) break;
|
|
154
|
+
dir = parent;
|
|
155
|
+
}
|
|
156
|
+
// Nearest last, so it overrides ancestors.
|
|
157
|
+
return found.reverse();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function configPaths(cwd: string, home: string): string[] {
|
|
161
|
+
return [join(home, ".claude.json"), ...findProjectConfigs(cwd), join(cwd, ".claude", "settings.local.json")];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* A plugin's `.mcp.json` is a **bare** server map with no `mcpServers` wrapper,
|
|
166
|
+
* unlike a project's. Accept either shape.
|
|
167
|
+
*/
|
|
168
|
+
function serverMapOf(file: Record<string, unknown> | undefined): Record<string, RawServer> | undefined {
|
|
169
|
+
if (!file || typeof file !== "object") return undefined;
|
|
170
|
+
const wrapped = (file as { mcpServers?: unknown }).mcpServers;
|
|
171
|
+
if (wrapped && typeof wrapped === "object" && !Array.isArray(wrapped)) {
|
|
172
|
+
return wrapped as Record<string, RawServer>;
|
|
173
|
+
}
|
|
174
|
+
const looksLikeServerMap = Object.values(file).every(
|
|
175
|
+
(value) => value && typeof value === "object" && !Array.isArray(value),
|
|
176
|
+
);
|
|
177
|
+
return looksLikeServerMap ? (file as Record<string, RawServer>) : undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function loadServers(
|
|
181
|
+
cwd: string,
|
|
182
|
+
home: string,
|
|
183
|
+
env: Record<string, string | undefined> = process.env,
|
|
184
|
+
extraPaths: string[] = [],
|
|
185
|
+
): McpServer[] {
|
|
186
|
+
const byName = new Map<string, McpServer>();
|
|
187
|
+
// Plugin configs come first so project and user files can override them.
|
|
188
|
+
for (const path of [...extraPaths, ...configPaths(cwd, home)]) {
|
|
189
|
+
const file = readJson(path) as Record<string, unknown> | undefined;
|
|
190
|
+
const servers = serverMapOf(file);
|
|
191
|
+
if (!servers) continue;
|
|
192
|
+
for (const [name, raw] of Object.entries(servers)) {
|
|
193
|
+
const server = parseServer(name, raw ?? {}, path, env);
|
|
194
|
+
if (server) byName.set(name, server);
|
|
195
|
+
else byName.delete(name); // an explicit `disabled` entry removes an inherited one
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
199
|
+
}
|