bosun 0.36.2 → 0.36.3
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/analyze-agent-work-helpers.mjs +308 -0
- package/analyze-agent-work.mjs +926 -0
- package/autofix.mjs +2 -0
- package/codex-shell.mjs +85 -10
- package/git-editor-fix.mjs +273 -0
- package/mcp-registry.mjs +579 -0
- package/meeting-workflow-service.mjs +631 -0
- package/monitor.mjs +18 -103
- package/package.json +13 -2
- package/primary-agent.mjs +32 -12
- package/session-tracker.mjs +68 -0
- package/stream-resilience.mjs +17 -7
- package/ui/app.js +19 -4
- package/ui/components/chat-view.js +108 -5
- package/ui/components/session-list.js +1 -1
- package/ui/components/shared.js +188 -15
- package/ui/modules/icons.js +13 -0
- package/ui/modules/utils.js +44 -0
- package/ui/modules/voice.js +15 -6
- package/ui/styles/components.css +99 -3
- package/ui/styles/sessions.css +84 -12
- package/ui/tabs/chat.js +5 -1
- package/ui/tabs/control.js +16 -22
- package/ui/tabs/dashboard.js +85 -8
- package/ui/tabs/library.js +113 -17
- package/ui/tabs/settings.js +116 -2
- package/ui/tabs/tasks.js +388 -39
- package/ui/tabs/telemetry.js +0 -1
- package/ui/tabs/workflows.js +4 -0
- package/ui-server.mjs +193 -19
- package/update-check.mjs +41 -13
- package/voice-relay.mjs +816 -0
- package/voice-tools.mjs +679 -0
- package/workflow-templates/agents.mjs +6 -2
- package/workflow-templates/github.mjs +154 -12
- package/workflow-templates.mjs +3 -0
- package/github-reconciler.mjs +0 -506
- package/merge-strategy.mjs +0 -1210
- package/pr-cleanup-daemon.mjs +0 -992
- package/workspace-reaper.mjs +0 -405
package/mcp-registry.mjs
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-registry.mjs — Centralized MCP Server Registry & Management
|
|
3
|
+
*
|
|
4
|
+
* Provides a curated catalog of popular MCP servers that can be installed
|
|
5
|
+
* with one click, plus helpers to resolve installed servers for injection
|
|
6
|
+
* into agent launches (Codex config.toml, Copilot CLI args, Claude env).
|
|
7
|
+
*
|
|
8
|
+
* Storage: installed servers are persisted as `type: "mcp"` entries in the
|
|
9
|
+
* existing library system (`.bosun/mcp-servers/*.json` + `library.json`).
|
|
10
|
+
*
|
|
11
|
+
* EXPORTS:
|
|
12
|
+
* CURATED_MCP_CATALOG — frozen array of known-good MCP server defs
|
|
13
|
+
* listCatalog() — read-only catalog query
|
|
14
|
+
* getCatalogEntry(id) — single catalog entry by ID
|
|
15
|
+
* installMcpServer() — one-click install from catalog or custom def
|
|
16
|
+
* uninstallMcpServer() — remove an installed server
|
|
17
|
+
* listInstalledMcpServers() — all installed MCP servers
|
|
18
|
+
* getInstalledMcpServer() — single installed server by ID
|
|
19
|
+
* resolveMcpServersForAgent() — resolve IDs to full configs for SDK injection
|
|
20
|
+
* buildCodexMcpToml() — convert configs → Codex TOML blocks
|
|
21
|
+
* buildCopilotMcpJson() — convert configs → Copilot MCP JSON
|
|
22
|
+
* buildClaudeMcpEnv() — convert configs → Claude MCP env format
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { resolve, join } from "node:path";
|
|
26
|
+
import { existsSync, writeFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
27
|
+
import { randomUUID } from "node:crypto";
|
|
28
|
+
|
|
29
|
+
// Lazy-import library manager to avoid circular dependency at module load.
|
|
30
|
+
// Cached at module scope per AGENTS.md hard rules.
|
|
31
|
+
let _libManager = null;
|
|
32
|
+
async function getLibManager() {
|
|
33
|
+
if (!_libManager) {
|
|
34
|
+
_libManager = await import("./library-manager.mjs");
|
|
35
|
+
}
|
|
36
|
+
return _libManager;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const TAG = "[mcp-registry]";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Curated catalog of popular, reliable MCP servers.
|
|
45
|
+
* Each entry describes how to launch or connect to the server.
|
|
46
|
+
*
|
|
47
|
+
* Transport types:
|
|
48
|
+
* "stdio" — launch via command+args, communicate over stdin/stdout
|
|
49
|
+
* "url" — connect to a remote HTTP/SSE endpoint
|
|
50
|
+
*/
|
|
51
|
+
export const CURATED_MCP_CATALOG = Object.freeze([
|
|
52
|
+
// ── Official / Microsoft / GitHub ────────────────────────────────────────
|
|
53
|
+
{
|
|
54
|
+
id: "github",
|
|
55
|
+
name: "GitHub",
|
|
56
|
+
description: "GitHub API — repositories, issues, PRs, code search, actions",
|
|
57
|
+
transport: "stdio",
|
|
58
|
+
command: "npx",
|
|
59
|
+
args: ["-y", "@anthropic/mcp-github"],
|
|
60
|
+
env: { GITHUB_PERSONAL_ACCESS_TOKEN: "" },
|
|
61
|
+
tags: ["github", "vcs", "official"],
|
|
62
|
+
source: "catalog",
|
|
63
|
+
homepage: "https://github.com/anthropics/github-mcp-server",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "microsoft-docs",
|
|
67
|
+
name: "Microsoft Docs",
|
|
68
|
+
description: "Microsoft Learn documentation search and retrieval",
|
|
69
|
+
transport: "url",
|
|
70
|
+
url: "https://learn.microsoft.com/api/mcp",
|
|
71
|
+
tags: ["docs", "microsoft", "official"],
|
|
72
|
+
source: "catalog",
|
|
73
|
+
homepage: "https://learn.microsoft.com",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "context7",
|
|
77
|
+
name: "Context7",
|
|
78
|
+
description: "Up-to-date library documentation and code examples",
|
|
79
|
+
transport: "stdio",
|
|
80
|
+
command: "npx",
|
|
81
|
+
args: ["-y", "@upstash/context7-mcp"],
|
|
82
|
+
tags: ["docs", "libraries", "community"],
|
|
83
|
+
source: "catalog",
|
|
84
|
+
homepage: "https://github.com/nicepkg/context7",
|
|
85
|
+
},
|
|
86
|
+
// ── Official MCP Reference Servers ───────────────────────────────────────
|
|
87
|
+
{
|
|
88
|
+
id: "sequential-thinking",
|
|
89
|
+
name: "Sequential Thinking",
|
|
90
|
+
description: "Dynamic, reflective problem-solving through structured thought sequences",
|
|
91
|
+
transport: "stdio",
|
|
92
|
+
command: "npx",
|
|
93
|
+
args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
|
|
94
|
+
tags: ["reasoning", "official"],
|
|
95
|
+
source: "catalog",
|
|
96
|
+
homepage: "https://github.com/modelcontextprotocol/servers",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "filesystem",
|
|
100
|
+
name: "Filesystem",
|
|
101
|
+
description: "Secure file system operations with configurable access controls",
|
|
102
|
+
transport: "stdio",
|
|
103
|
+
command: "npx",
|
|
104
|
+
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
|
|
105
|
+
tags: ["filesystem", "official"],
|
|
106
|
+
source: "catalog",
|
|
107
|
+
homepage: "https://github.com/modelcontextprotocol/servers",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "fetch",
|
|
111
|
+
name: "Fetch",
|
|
112
|
+
description: "Web content fetching and conversion for efficient LLM usage",
|
|
113
|
+
transport: "stdio",
|
|
114
|
+
command: "npx",
|
|
115
|
+
args: ["-y", "@modelcontextprotocol/server-fetch"],
|
|
116
|
+
tags: ["web", "http", "official"],
|
|
117
|
+
source: "catalog",
|
|
118
|
+
homepage: "https://github.com/modelcontextprotocol/servers",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "memory",
|
|
122
|
+
name: "Memory",
|
|
123
|
+
description: "Knowledge graph-based persistent memory for entities and relations",
|
|
124
|
+
transport: "stdio",
|
|
125
|
+
command: "npx",
|
|
126
|
+
args: ["-y", "@modelcontextprotocol/server-memory"],
|
|
127
|
+
tags: ["memory", "knowledge-graph", "official"],
|
|
128
|
+
source: "catalog",
|
|
129
|
+
homepage: "https://github.com/modelcontextprotocol/servers",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "postgres",
|
|
133
|
+
name: "PostgreSQL",
|
|
134
|
+
description: "Read-only PostgreSQL database access with schema inspection",
|
|
135
|
+
transport: "stdio",
|
|
136
|
+
command: "npx",
|
|
137
|
+
args: ["-y", "@modelcontextprotocol/server-postgres"],
|
|
138
|
+
env: { POSTGRES_CONNECTION_STRING: "" },
|
|
139
|
+
tags: ["database", "postgres", "official"],
|
|
140
|
+
source: "catalog",
|
|
141
|
+
homepage: "https://github.com/modelcontextprotocol/servers",
|
|
142
|
+
},
|
|
143
|
+
// ── Browser / UI Testing ────────────────────────────────────────────────
|
|
144
|
+
{
|
|
145
|
+
id: "playwright",
|
|
146
|
+
name: "Playwright",
|
|
147
|
+
description: "Browser automation — navigate, interact, screenshot, test web apps",
|
|
148
|
+
transport: "stdio",
|
|
149
|
+
command: "npx",
|
|
150
|
+
args: ["-y", "@playwright/mcp@latest"],
|
|
151
|
+
tags: ["browser", "testing", "automation", "official"],
|
|
152
|
+
source: "catalog",
|
|
153
|
+
homepage: "https://github.com/nicholasrq/playwright-mcp",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "puppeteer",
|
|
157
|
+
name: "Puppeteer",
|
|
158
|
+
description: "Chrome automation via Puppeteer for browser control and screenshots",
|
|
159
|
+
transport: "stdio",
|
|
160
|
+
command: "npx",
|
|
161
|
+
args: ["-y", "@anthropic/mcp-puppeteer"],
|
|
162
|
+
tags: ["browser", "testing", "screenshot"],
|
|
163
|
+
source: "catalog",
|
|
164
|
+
homepage: "https://github.com/anthropics/puppeteer-mcp-server",
|
|
165
|
+
},
|
|
166
|
+
// ── Search / Knowledge ──────────────────────────────────────────────────
|
|
167
|
+
{
|
|
168
|
+
id: "brave-search",
|
|
169
|
+
name: "Brave Search",
|
|
170
|
+
description: "Web and local search using the Brave Search API",
|
|
171
|
+
transport: "stdio",
|
|
172
|
+
command: "npx",
|
|
173
|
+
args: ["-y", "@anthropic/mcp-brave-search"],
|
|
174
|
+
env: { BRAVE_API_KEY: "" },
|
|
175
|
+
tags: ["search", "web"],
|
|
176
|
+
source: "catalog",
|
|
177
|
+
homepage: "https://github.com/anthropics/brave-search-mcp-server",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "exa",
|
|
181
|
+
name: "Exa Search",
|
|
182
|
+
description: "Neural search engine for precise, up-to-date web results",
|
|
183
|
+
transport: "stdio",
|
|
184
|
+
command: "npx",
|
|
185
|
+
args: ["-y", "exa-mcp-server"],
|
|
186
|
+
env: { EXA_API_KEY: "" },
|
|
187
|
+
tags: ["search", "web", "ai"],
|
|
188
|
+
source: "catalog",
|
|
189
|
+
homepage: "https://github.com/nicepkg/exa-mcp-server",
|
|
190
|
+
},
|
|
191
|
+
// ── Productivity / Project Management ───────────────────────────────────
|
|
192
|
+
{
|
|
193
|
+
id: "linear",
|
|
194
|
+
name: "Linear",
|
|
195
|
+
description: "Linear project management — issues, projects, teams",
|
|
196
|
+
transport: "stdio",
|
|
197
|
+
command: "npx",
|
|
198
|
+
args: ["-y", "mcp-linear"],
|
|
199
|
+
env: { LINEAR_API_KEY: "" },
|
|
200
|
+
tags: ["project-management", "issues"],
|
|
201
|
+
source: "catalog",
|
|
202
|
+
homepage: "https://github.com/nicepkg/mcp-linear",
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: "slack",
|
|
206
|
+
name: "Slack",
|
|
207
|
+
description: "Slack workspace messaging, channels, and search",
|
|
208
|
+
transport: "stdio",
|
|
209
|
+
command: "npx",
|
|
210
|
+
args: ["-y", "@anthropic/mcp-slack"],
|
|
211
|
+
env: { SLACK_BOT_TOKEN: "", SLACK_TEAM_ID: "" },
|
|
212
|
+
tags: ["messaging", "slack"],
|
|
213
|
+
source: "catalog",
|
|
214
|
+
homepage: "https://github.com/anthropics/slack-mcp-server",
|
|
215
|
+
},
|
|
216
|
+
// ── Storage / Cloud ─────────────────────────────────────────────────────
|
|
217
|
+
{
|
|
218
|
+
id: "google-drive",
|
|
219
|
+
name: "Google Drive",
|
|
220
|
+
description: "Google Drive file search and retrieval",
|
|
221
|
+
transport: "stdio",
|
|
222
|
+
command: "npx",
|
|
223
|
+
args: ["-y", "@anthropic/mcp-google-drive"],
|
|
224
|
+
tags: ["cloud", "storage", "google"],
|
|
225
|
+
source: "catalog",
|
|
226
|
+
homepage: "https://github.com/anthropics/google-drive-mcp-server",
|
|
227
|
+
},
|
|
228
|
+
// ── Vibe Kanban (Bosun built-in) ────────────────────────────────────────
|
|
229
|
+
{
|
|
230
|
+
id: "vibe-kanban",
|
|
231
|
+
name: "Vibe Kanban",
|
|
232
|
+
description: "Bosun's integrated task and project management MCP server",
|
|
233
|
+
transport: "stdio",
|
|
234
|
+
command: "npx",
|
|
235
|
+
args: ["-y", "vibe-kanban@latest"],
|
|
236
|
+
env: { VK_BASE_URL: "http://127.0.0.1:54089" },
|
|
237
|
+
tags: ["kanban", "tasks", "bosun"],
|
|
238
|
+
source: "catalog",
|
|
239
|
+
homepage: "https://github.com/nicepkg/vibe-kanban",
|
|
240
|
+
},
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
// ── Catalog Queries ───────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* List all catalog entries, optionally filtered by tags.
|
|
247
|
+
* @param {{ tags?: string[] }} [options]
|
|
248
|
+
* @returns {ReadonlyArray<Object>}
|
|
249
|
+
*/
|
|
250
|
+
export function listCatalog({ tags } = {}) {
|
|
251
|
+
if (!tags || !tags.length) return CURATED_MCP_CATALOG;
|
|
252
|
+
const tagSet = new Set(tags.map((t) => t.toLowerCase()));
|
|
253
|
+
return CURATED_MCP_CATALOG.filter((entry) =>
|
|
254
|
+
entry.tags.some((t) => tagSet.has(t.toLowerCase())),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get a single catalog entry by ID.
|
|
260
|
+
* @param {string} id
|
|
261
|
+
* @returns {Object|null}
|
|
262
|
+
*/
|
|
263
|
+
export function getCatalogEntry(id) {
|
|
264
|
+
const normalized = String(id || "").trim().toLowerCase();
|
|
265
|
+
return CURATED_MCP_CATALOG.find((e) => e.id === normalized) || null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Install / Uninstall ───────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Install an MCP server from the catalog or a custom definition.
|
|
272
|
+
*
|
|
273
|
+
* Catalog install:
|
|
274
|
+
* installMcpServer(rootDir, "github")
|
|
275
|
+
*
|
|
276
|
+
* Custom install:
|
|
277
|
+
* installMcpServer(rootDir, { id: "my-server", name: "My Server", ... })
|
|
278
|
+
*
|
|
279
|
+
* @param {string} rootDir — workspace root
|
|
280
|
+
* @param {string|Object} catalogIdOrDef — catalog ID string or full server def
|
|
281
|
+
* @param {{ envOverrides?: Object }} [options]
|
|
282
|
+
* @returns {Promise<Object>} — the created library entry
|
|
283
|
+
*/
|
|
284
|
+
export async function installMcpServer(rootDir, catalogIdOrDef, { envOverrides } = {}) {
|
|
285
|
+
const lib = await getLibManager();
|
|
286
|
+
|
|
287
|
+
let serverDef;
|
|
288
|
+
if (typeof catalogIdOrDef === "string") {
|
|
289
|
+
const catalogEntry = getCatalogEntry(catalogIdOrDef);
|
|
290
|
+
if (!catalogEntry) {
|
|
291
|
+
throw new Error(`${TAG} MCP server "${catalogIdOrDef}" not found in catalog. Use listCatalog() to see available servers.`);
|
|
292
|
+
}
|
|
293
|
+
// Clone catalog entry so we don't mutate the frozen original
|
|
294
|
+
serverDef = { ...catalogEntry };
|
|
295
|
+
} else if (catalogIdOrDef && typeof catalogIdOrDef === "object") {
|
|
296
|
+
serverDef = { ...catalogIdOrDef };
|
|
297
|
+
if (!serverDef.id) serverDef.id = serverDef.name ? slugify(serverDef.name) : `custom-${randomUUID().slice(0, 8)}`;
|
|
298
|
+
if (!serverDef.name) serverDef.name = serverDef.id;
|
|
299
|
+
serverDef.source = serverDef.source || "custom";
|
|
300
|
+
} else {
|
|
301
|
+
throw new Error(`${TAG} installMcpServer requires a catalog ID string or server definition object`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Apply environment overrides
|
|
305
|
+
if (envOverrides && typeof envOverrides === "object") {
|
|
306
|
+
serverDef.env = { ...(serverDef.env || {}), ...envOverrides };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Persist via library manager as type "mcp"
|
|
310
|
+
const entry = lib.upsertEntry(rootDir, {
|
|
311
|
+
type: "mcp",
|
|
312
|
+
id: serverDef.id,
|
|
313
|
+
name: serverDef.name,
|
|
314
|
+
description: serverDef.description || "",
|
|
315
|
+
tags: serverDef.tags || [],
|
|
316
|
+
meta: {
|
|
317
|
+
transport: serverDef.transport || "stdio",
|
|
318
|
+
command: serverDef.command || null,
|
|
319
|
+
args: serverDef.args || [],
|
|
320
|
+
url: serverDef.url || null,
|
|
321
|
+
env: serverDef.env || {},
|
|
322
|
+
source: serverDef.source || "catalog",
|
|
323
|
+
homepage: serverDef.homepage || null,
|
|
324
|
+
},
|
|
325
|
+
}, serverDef);
|
|
326
|
+
|
|
327
|
+
console.log(`${TAG} Installed MCP server: ${entry.id} (${serverDef.transport})`);
|
|
328
|
+
return entry;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Uninstall an MCP server by ID.
|
|
333
|
+
* @param {string} rootDir
|
|
334
|
+
* @param {string} id
|
|
335
|
+
* @returns {Promise<boolean>}
|
|
336
|
+
*/
|
|
337
|
+
export async function uninstallMcpServer(rootDir, id) {
|
|
338
|
+
const lib = await getLibManager();
|
|
339
|
+
const removed = lib.deleteEntry(rootDir, id, { deleteFile: true });
|
|
340
|
+
if (removed) {
|
|
341
|
+
console.log(`${TAG} Uninstalled MCP server: ${id}`);
|
|
342
|
+
}
|
|
343
|
+
return removed;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* List all installed MCP servers.
|
|
348
|
+
* @param {string} rootDir
|
|
349
|
+
* @returns {Promise<Array<Object>>}
|
|
350
|
+
*/
|
|
351
|
+
export async function listInstalledMcpServers(rootDir) {
|
|
352
|
+
const lib = await getLibManager();
|
|
353
|
+
return lib.listEntries(rootDir, { type: "mcp" });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get a single installed MCP server by ID.
|
|
358
|
+
* @param {string} rootDir
|
|
359
|
+
* @param {string} id
|
|
360
|
+
* @returns {Promise<Object|null>}
|
|
361
|
+
*/
|
|
362
|
+
export async function getInstalledMcpServer(rootDir, id) {
|
|
363
|
+
const lib = await getLibManager();
|
|
364
|
+
const entry = lib.getEntry(rootDir, id);
|
|
365
|
+
if (!entry || entry.type !== "mcp") return null;
|
|
366
|
+
// Merge entry metadata with file content to get full config
|
|
367
|
+
const content = lib.getEntryContent(rootDir, entry);
|
|
368
|
+
return { ...entry, serverConfig: content };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── Agent Launch Resolution ───────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Resolve an array of MCP server IDs into full server configurations
|
|
375
|
+
* ready for injection into SDK adapters.
|
|
376
|
+
*
|
|
377
|
+
* @param {string} rootDir
|
|
378
|
+
* @param {string[]} mcpServerIds — array of installed server IDs
|
|
379
|
+
* @param {{ defaultServers?: string[], catalogOverrides?: Object }} [options]
|
|
380
|
+
* @returns {Promise<Array<Object>>} — resolved server configs
|
|
381
|
+
*/
|
|
382
|
+
export async function resolveMcpServersForAgent(rootDir, mcpServerIds = [], options = {}) {
|
|
383
|
+
const { defaultServers = [], catalogOverrides = {} } = options;
|
|
384
|
+
|
|
385
|
+
// Merge requested IDs with defaults (deduplicate)
|
|
386
|
+
const allIds = [...new Set([...defaultServers, ...mcpServerIds])];
|
|
387
|
+
if (!allIds.length) return [];
|
|
388
|
+
|
|
389
|
+
const lib = await getLibManager();
|
|
390
|
+
const resolved = [];
|
|
391
|
+
|
|
392
|
+
for (const id of allIds) {
|
|
393
|
+
const entry = lib.getEntry(rootDir, id);
|
|
394
|
+
if (entry && entry.type === "mcp") {
|
|
395
|
+
const content = lib.getEntryContent(rootDir, entry);
|
|
396
|
+
const config = content || entry.meta || {};
|
|
397
|
+
// Apply per-server catalog overrides (e.g. env vars)
|
|
398
|
+
if (catalogOverrides[id]) {
|
|
399
|
+
config.env = { ...(config.env || {}), ...catalogOverrides[id] };
|
|
400
|
+
}
|
|
401
|
+
resolved.push({
|
|
402
|
+
id: entry.id,
|
|
403
|
+
name: entry.name,
|
|
404
|
+
transport: config.transport || entry.meta?.transport || "stdio",
|
|
405
|
+
command: config.command || entry.meta?.command || null,
|
|
406
|
+
args: config.args || entry.meta?.args || [],
|
|
407
|
+
url: config.url || entry.meta?.url || null,
|
|
408
|
+
env: config.env || entry.meta?.env || {},
|
|
409
|
+
});
|
|
410
|
+
} else {
|
|
411
|
+
// Check catalog as fallback (auto-install from catalog)
|
|
412
|
+
const catalogEntry = getCatalogEntry(id);
|
|
413
|
+
if (catalogEntry) {
|
|
414
|
+
const config = { ...catalogEntry };
|
|
415
|
+
if (catalogOverrides[id]) {
|
|
416
|
+
config.env = { ...(config.env || {}), ...catalogOverrides[id] };
|
|
417
|
+
}
|
|
418
|
+
resolved.push({
|
|
419
|
+
id: config.id,
|
|
420
|
+
name: config.name,
|
|
421
|
+
transport: config.transport,
|
|
422
|
+
command: config.command || null,
|
|
423
|
+
args: config.args || [],
|
|
424
|
+
url: config.url || null,
|
|
425
|
+
env: config.env || {},
|
|
426
|
+
});
|
|
427
|
+
} else {
|
|
428
|
+
console.warn(`${TAG} MCP server "${id}" not found (installed or catalog), skipping`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return resolved;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ── SDK-Specific Format Builders ──────────────────────────────────────────────
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Convert resolved MCP server configs into Codex CLI config.toml format.
|
|
440
|
+
*
|
|
441
|
+
* Example output:
|
|
442
|
+
* [mcp_servers.github]
|
|
443
|
+
* command = "npx"
|
|
444
|
+
* args = ["-y", "@anthropic/mcp-github"]
|
|
445
|
+
* [mcp_servers.github.env]
|
|
446
|
+
* GITHUB_PERSONAL_ACCESS_TOKEN = "..."
|
|
447
|
+
*
|
|
448
|
+
* @param {Array<Object>} servers — resolved server configs
|
|
449
|
+
* @returns {string} — TOML string
|
|
450
|
+
*/
|
|
451
|
+
export function buildCodexMcpToml(servers) {
|
|
452
|
+
if (!servers || !servers.length) return "";
|
|
453
|
+
|
|
454
|
+
const lines = [];
|
|
455
|
+
for (const srv of servers) {
|
|
456
|
+
const safeId = String(srv.id).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
457
|
+
|
|
458
|
+
if (srv.transport === "url" && srv.url) {
|
|
459
|
+
lines.push("");
|
|
460
|
+
lines.push(`[mcp_servers.${safeId}]`);
|
|
461
|
+
lines.push(`url = ${toTomlString(srv.url)}`);
|
|
462
|
+
} else if (srv.command) {
|
|
463
|
+
lines.push("");
|
|
464
|
+
lines.push(`[mcp_servers.${safeId}]`);
|
|
465
|
+
lines.push(`command = ${toTomlString(srv.command)}`);
|
|
466
|
+
if (srv.args && srv.args.length) {
|
|
467
|
+
const argsStr = srv.args.map((a) => toTomlString(a)).join(", ");
|
|
468
|
+
lines.push(`args = [${argsStr}]`);
|
|
469
|
+
}
|
|
470
|
+
} else {
|
|
471
|
+
continue; // Skip servers without command or url
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Write env block if present (skip empty-value keys)
|
|
475
|
+
const envEntries = Object.entries(srv.env || {}).filter(
|
|
476
|
+
([, v]) => v != null && String(v).trim() !== "",
|
|
477
|
+
);
|
|
478
|
+
if (envEntries.length) {
|
|
479
|
+
lines.push(`[mcp_servers.${safeId}.env]`);
|
|
480
|
+
for (const [key, value] of envEntries) {
|
|
481
|
+
lines.push(`${key} = ${toTomlString(String(value))}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return lines.join("\n");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Convert resolved MCP server configs into Copilot SDK MCP JSON config.
|
|
491
|
+
* Written as a temp file and passed via --additional-mcp-config.
|
|
492
|
+
*
|
|
493
|
+
* @param {Array<Object>} servers — resolved server configs
|
|
494
|
+
* @returns {Object} — { mcpServers: { [id]: { command, args, env? } | { url } } }
|
|
495
|
+
*/
|
|
496
|
+
export function buildCopilotMcpJson(servers) {
|
|
497
|
+
if (!servers || !servers.length) return { mcpServers: {} };
|
|
498
|
+
|
|
499
|
+
const mcpServers = {};
|
|
500
|
+
for (const srv of servers) {
|
|
501
|
+
if (srv.transport === "url" && srv.url) {
|
|
502
|
+
mcpServers[srv.id] = { url: srv.url };
|
|
503
|
+
} else if (srv.command) {
|
|
504
|
+
const entry = { command: srv.command };
|
|
505
|
+
if (srv.args && srv.args.length) entry.args = srv.args;
|
|
506
|
+
const envEntries = Object.entries(srv.env || {}).filter(
|
|
507
|
+
([, v]) => v != null && String(v).trim() !== "",
|
|
508
|
+
);
|
|
509
|
+
if (envEntries.length) entry.env = Object.fromEntries(envEntries);
|
|
510
|
+
mcpServers[srv.id] = entry;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return { mcpServers };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Convert resolved MCP server configs into Claude Code MCP environment format.
|
|
519
|
+
* Claude Code uses CLAUDE_MCP_SERVERS env var (JSON) or .claude/mcp_servers.json.
|
|
520
|
+
*
|
|
521
|
+
* @param {Array<Object>} servers — resolved server configs
|
|
522
|
+
* @returns {{ envVar: string, fileContent: Object }} — env var value + file format
|
|
523
|
+
*/
|
|
524
|
+
export function buildClaudeMcpEnv(servers) {
|
|
525
|
+
if (!servers || !servers.length) {
|
|
526
|
+
return { envVar: "", fileContent: { mcpServers: {} } };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const mcpServers = {};
|
|
530
|
+
for (const srv of servers) {
|
|
531
|
+
if (srv.transport === "url" && srv.url) {
|
|
532
|
+
mcpServers[srv.id] = { type: "url", url: srv.url };
|
|
533
|
+
} else if (srv.command) {
|
|
534
|
+
const entry = { type: "stdio", command: srv.command };
|
|
535
|
+
if (srv.args && srv.args.length) entry.args = srv.args;
|
|
536
|
+
const envEntries = Object.entries(srv.env || {}).filter(
|
|
537
|
+
([, v]) => v != null && String(v).trim() !== "",
|
|
538
|
+
);
|
|
539
|
+
if (envEntries.length) entry.env = Object.fromEntries(envEntries);
|
|
540
|
+
mcpServers[srv.id] = entry;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
envVar: JSON.stringify({ mcpServers }),
|
|
546
|
+
fileContent: { mcpServers },
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Write a temporary MCP config file for Copilot --additional-mcp-config.
|
|
552
|
+
* Returns the path to the temp file.
|
|
553
|
+
*
|
|
554
|
+
* @param {string} rootDir
|
|
555
|
+
* @param {Array<Object>} servers
|
|
556
|
+
* @returns {string} — path to temp MCP config JSON
|
|
557
|
+
*/
|
|
558
|
+
export function writeTempCopilotMcpConfig(rootDir, servers) {
|
|
559
|
+
const mcpConfig = buildCopilotMcpJson(servers);
|
|
560
|
+
const dir = resolve(rootDir, ".bosun", ".tmp");
|
|
561
|
+
mkdirSync(dir, { recursive: true });
|
|
562
|
+
const filePath = resolve(dir, `mcp-config-${Date.now()}.json`);
|
|
563
|
+
writeFileSync(filePath, JSON.stringify(mcpConfig, null, 2), "utf8");
|
|
564
|
+
return filePath;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
568
|
+
|
|
569
|
+
function toTomlString(value) {
|
|
570
|
+
return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function slugify(name) {
|
|
574
|
+
return String(name || "")
|
|
575
|
+
.toLowerCase()
|
|
576
|
+
.trim()
|
|
577
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
578
|
+
.replace(/^-|-$/g, "");
|
|
579
|
+
}
|