1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
|
/**
* PURE config resolution — resolve MCP server configurations.
*
* Sources, in precedence order:
* 1. cwd/.dispatch/mcp.json servers
* 2. fallback cwd/opencode.json mcp key
*
* No I/O — callers pass the file contents as strings.
*/
import type { McpServerConfig, ResolvedMcpServer, ResolveResult } from "./types.js";
export interface ResolveServersDeps {
readonly dispatchMcpJson: string | null;
readonly opencodeJson: string | null;
}
export interface DispatchMcpConfig {
readonly servers?: Readonly<Record<string, McpServerConfig>>;
}
export interface OpencodeJsonConfig {
readonly mcp?: Readonly<Record<string, McpServerConfig>>;
}
export function resolveServers(deps: ResolveServersDeps): ResolveResult {
const result = new Map<string, ResolvedMcpServer>();
// Parse opencode.json once — used both as the fallback source and to detect
// whether a present `.dispatch/mcp.json` silently shadows its `mcp` key.
let opencodeConfig: OpencodeJsonConfig | null = null;
if (deps.opencodeJson) {
try {
opencodeConfig = JSON.parse(deps.opencodeJson) as OpencodeJsonConfig;
} catch {
// ignore parse errors
}
}
const opencodeHasMcp = !!opencodeConfig?.mcp && Object.keys(opencodeConfig.mcp).length > 0;
// 1. cwd/.dispatch/mcp.json (highest precedence)
let dispatchHadServers = false;
if (deps.dispatchMcpJson) {
try {
const config = JSON.parse(deps.dispatchMcpJson) as DispatchMcpConfig;
if (config.servers) {
for (const [key, server] of Object.entries(config.servers)) {
const resolved = resolveServer(key, server, ".dispatch/mcp.json");
result.set(resolved.id, resolved);
}
dispatchHadServers = result.size > 0;
}
} catch {
// ignore parse errors
}
}
// 2. fallback cwd/opencode.json mcp key (only when dispatch yielded nothing)
if (result.size === 0 && opencodeConfig?.mcp) {
for (const [key, server] of Object.entries(opencodeConfig.mcp)) {
const resolved = resolveServer(key, server, "opencode.json");
result.set(resolved.id, resolved);
}
}
// No built-in servers — MCP has no built-in registry.
// `.dispatch/mcp.json` silently shadows `opencode.json`'s mcp key when both
// declare servers — the opencode entry is skipped with no warning otherwise.
const shadowed = dispatchHadServers && opencodeHasMcp;
return { servers: [...result.values()], shadowed };
}
function resolveServer(
key: string,
config: McpServerConfig,
configSource: ".dispatch/mcp.json" | "opencode.json",
): ResolvedMcpServer {
const command = [config.command, ...(config.args ?? [])];
const result: ResolvedMcpServer = {
id: key,
command,
configSource,
};
if (config.env) {
(result as { env?: Readonly<Record<string, string>> }).env = config.env;
}
return result;
}
|