Skip to content

Commit f4e7fd4

Browse files
feat(mcp): add OpenCode, OpenClaw, DSH, ZCode, and WorkBuddy native MCP targets
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c957e2e commit f4e7fd4

8 files changed

Lines changed: 625 additions & 109 deletions

File tree

packages/commands/src/commands/config/inventory.ts

Lines changed: 99 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
import { inflateRawSync } from "node:zlib";
2020
import yaml from "yaml";
2121
import { parse as parseToml } from "smol-toml";
22-
import { qwenworkMcpPath } from "../mcp/agent-config.ts";
22+
import { qwenworkMcpPath, workbuddyMcpPaths } from "../mcp/agent-config.ts";
2323

2424
/**
2525
* Where an item comes from. Everything discovered on disk today is `local`;
@@ -398,7 +398,9 @@ function transportOf(entry: Record<string, unknown>): {
398398
const url = typeof entry.url === "string" ? entry.url : undefined;
399399
if (url) {
400400
const type = typeof entry.type === "string" ? entry.type.toLowerCase() : "";
401-
return { transport: type === "sse" ? "sse" : "http", detail: url };
401+
const transportField = typeof entry.transport === "string" ? entry.transport.toLowerCase() : "";
402+
if (type === "sse" || transportField === "sse") return { transport: "sse", detail: url };
403+
return { transport: "http", detail: url };
402404
}
403405
return { transport: "unknown", detail: "" };
404406
}
@@ -475,7 +477,44 @@ export function listMcpServers(home: string = homedir()): McpServerInfo[] {
475477
if (gemini) collectMcpMap(gemini.mcpServers, "gemini", "global", out, true);
476478

477479
const openclaw = readJsonSafe(join(home, ".openclaw", "openclaw.json"));
478-
if (openclaw) collectMcpMap(openclaw.mcpServers, "openclaw", "global", out, true);
480+
if (openclaw) {
481+
const mcp = asRecord(openclaw.mcp);
482+
collectMcpMap(mcp?.servers ?? openclaw.mcpServers, "openclaw", "global", out, true);
483+
}
484+
485+
const zcode = readJsonSafe(join(home, ".zcode", "cli", "config.json"));
486+
if (zcode) collectMcpMap(asRecord(zcode.mcp)?.servers, "zcode", "global", out, true);
487+
488+
for (const file of workbuddyMcpPaths(home)) {
489+
const workbuddy = readJsonSafe(file);
490+
if (workbuddy) collectMcpMap(workbuddy.mcpServers, "workbuddy", file, out, true);
491+
}
492+
493+
const dshPatch = readText(join(home, ".dsh", "cordis.patch.yml"));
494+
if (dshPatch) {
495+
try {
496+
const parsed = yaml.parse(dshPatch) as unknown;
497+
const items = Array.isArray(parsed) ? parsed : [];
498+
const dshServers: Record<string, unknown> = {};
499+
for (const item of items) {
500+
const entries = asRecord(item)?.insert;
501+
const list = Array.isArray(entries) ? entries : [item];
502+
for (const entry of list) {
503+
const record = asRecord(entry);
504+
const config = record ? asRecord(record.config) : undefined;
505+
if (
506+
record?.name === "@deepseek-ai/dsh-mcp-client" &&
507+
typeof config?.serverName === "string"
508+
) {
509+
dshServers[config.serverName] = config;
510+
}
511+
}
512+
}
513+
collectMcpMap(dshServers, "deepseek-harness", "global", out, false);
514+
} catch {
515+
/* ignore malformed yaml */
516+
}
517+
}
479518

480519
const claudeDesktop = readJsonSafe(claudeDesktopConfigPath(home));
481520
if (claudeDesktop) collectMcpMap(claudeDesktop.mcpServers, "claude-desktop", "global", out, true);
@@ -514,6 +553,8 @@ function claudeDesktopConfigPath(home: string): string {
514553
interface McpWriteTarget {
515554
file: string;
516555
mapKey: string;
556+
/** When set, the server map lives at parentKey.mapKey (e.g. mcp.servers). */
557+
parentKey?: string;
517558
/** Claude stores project-scoped servers under projects[scope][mapKey]. */
518559
projectScoped: boolean;
519560
}
@@ -581,6 +622,20 @@ function mcpWriteTarget(source: string, scope: string, home: string): McpWriteTa
581622
if (source === "openclaw")
582623
return {
583624
file: join(home, ".openclaw", "openclaw.json"),
625+
mapKey: "servers",
626+
parentKey: "mcp",
627+
projectScoped: false,
628+
};
629+
if (source === "zcode")
630+
return {
631+
file: join(home, ".zcode", "cli", "config.json"),
632+
mapKey: "servers",
633+
parentKey: "mcp",
634+
projectScoped: false,
635+
};
636+
if (source === "workbuddy")
637+
return {
638+
file: workbuddyMcpPaths(home)[0] ?? join(home, ".workbuddy-ai", "mcp.json"),
584639
mapKey: "mcpServers",
585640
projectScoped: false,
586641
};
@@ -623,6 +678,45 @@ function unmaskMcpConfig(submitted: unknown, stored: unknown): unknown {
623678
return submitted;
624679
}
625680

681+
function mcpMapContainer(
682+
root: Record<string, unknown>,
683+
target: McpWriteTarget,
684+
scope: string,
685+
): Record<string, unknown> | undefined {
686+
let container: Record<string, unknown> | undefined = root;
687+
if (target.projectScoped) {
688+
const projects = asRecord(root.projects);
689+
container = projects ? asRecord(projects[scope]) : undefined;
690+
}
691+
if (!container) return undefined;
692+
if (target.parentKey) {
693+
const parent = asRecord(container[target.parentKey]);
694+
return parent;
695+
}
696+
return container;
697+
}
698+
699+
function ensureMcpMapContainer(
700+
root: Record<string, unknown>,
701+
target: McpWriteTarget,
702+
scope: string,
703+
): Record<string, unknown> {
704+
let container: Record<string, unknown> = root;
705+
if (target.projectScoped) {
706+
const projects = asRecord(root.projects) ?? {};
707+
root.projects = projects;
708+
const proj = asRecord(projects[scope]) ?? {};
709+
projects[scope] = proj;
710+
container = proj;
711+
}
712+
if (target.parentKey) {
713+
const parent = asRecord(container[target.parentKey]) ?? {};
714+
container[target.parentKey] = parent;
715+
return parent;
716+
}
717+
return container;
718+
}
719+
626720
/** Create or update one MCP server entry, writing back to its source file. */
627721
export function writeMcpServer(
628722
source: string,
@@ -639,14 +733,7 @@ export function writeMcpServer(
639733
if (!cfg) throw new Error("Config must be a JSON object.");
640734

641735
const root = readJsonSafe(target.file) ?? {};
642-
let container: Record<string, unknown> = root;
643-
if (target.projectScoped) {
644-
const projects = asRecord(root.projects) ?? {};
645-
root.projects = projects;
646-
const proj = asRecord(projects[scope]) ?? {};
647-
projects[scope] = proj;
648-
container = proj;
649-
}
736+
const container = ensureMcpMapContainer(root, target, scope);
650737
const map = asRecord(container[target.mapKey]) ?? {};
651738
container[target.mapKey] = map;
652739

@@ -667,11 +754,7 @@ export function deleteMcpServer(
667754
if (!target) throw new Error("This MCP source is read-only and cannot be edited here.");
668755
const root = readJsonSafe(target.file);
669756
if (!root) throw new Error("Config file not found.");
670-
let container: Record<string, unknown> | undefined = root;
671-
if (target.projectScoped) {
672-
const projects = asRecord(root.projects);
673-
container = projects ? asRecord(projects[scope]) : undefined;
674-
}
757+
const container = mcpMapContainer(root, target, scope);
675758
const map = container ? asRecord(container[target.mapKey]) : undefined;
676759
if (!map || !(name in map)) throw new Error("Server not found: " + name);
677760
delete map[name];

packages/commands/src/commands/config/ui-html.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1639,7 +1639,10 @@ const PAGE_HTML = `<!doctype html>
16391639
{ id: 'gemini', label: 'Gemini' },
16401640
{ id: 'opencode', label: 'OpenCode' },
16411641
{ id: 'openclaw', label: 'OpenClaw' },
1642-
{ id: 'qoderwork', label: 'QoderWork' }
1642+
{ id: 'qoderwork', label: 'QoderWork' },
1643+
{ id: 'zcode', label: 'ZCode' },
1644+
{ id: 'workbuddy', label: 'WorkBuddy' },
1645+
{ id: 'deepseek-harness', label: 'DeepSeek Harness' }
16431646
];
16441647
function copyButton(getText) {
16451648
var b = uiEl('button', 'copy-btn', 'Copy'); b.type = 'button';

0 commit comments

Comments
 (0)