fix(cc-config): MCP scope was inverted — expose user/local, not user/project
readMcpServers only ever parses ~/.claude.json (top-level = user scope, projects[<root>] = the panel's "projectScoped" bucket, which the CLI itself calls `local`). The previous user/project scope choice was the exact inverse: `--scope project` writes .mcp.json, which readMcpServers never reads (a successful add would vanish), and every "projectScoped" row was actually `local`, so Remove sent a scope the CLI could never match. - cc-mcp-cli.js: MCP_SCOPES -> ["user", "local"]; drop the local mutationQueue/serialize in favor of cc-plugin-cli.js's exported one, so a plugin install and an MCP add can't spawn `claude` concurrently; drop the String(v) coercion in env/header flags (a wrong-typed value is now a 400, not a written "[object Object]"); assertEmbeddedValue allows empty strings (a legitimate empty stdio arg was previously a 400). - cc-plugin-cli.js: export `serialize` for reuse. - CcConfig.tsx: CcMcpScope narrowed to user/local; the projectScoped bucket's McpCard now sends scope="local"; rows sourced from ~/.claude/settings.json (no CLI scope addresses them) no longer render a Remove button. Cleaned up i18n keys my own earlier change made dead (explain.mcp.addCmd/remove/removeButton) and gave the scope <select> its own short option labels instead of reusing full-sentence section headings.
This commit is contained in:
@@ -131,6 +131,8 @@
|
||||
"projectScope": "Project-scoped servers",
|
||||
"name": "Name",
|
||||
"scope": "Scope",
|
||||
"scopeUser": "User",
|
||||
"scopeLocal": "This project",
|
||||
"command": "Command",
|
||||
"args": "Args",
|
||||
"env": "Env names",
|
||||
@@ -138,7 +140,8 @@
|
||||
"headers": "Header names",
|
||||
"kind": "Kind",
|
||||
"source": "Source",
|
||||
"noServers": "No MCP servers found."
|
||||
"noServers": "No MCP servers found.",
|
||||
"notRemovable": "Configured in settings.json — remove it there; no CLI scope addresses this entry."
|
||||
},
|
||||
"hooks": {
|
||||
"noHooks": "No hooks configured.",
|
||||
@@ -277,10 +280,7 @@
|
||||
"body": "MCP server config lives inside `~/.claude.json`, the same file the live CLI writes to constantly. Concurrent writes from the dashboard would corrupt project state, so add/remove flow goes through Claude Code itself.",
|
||||
"list": "List configured servers:",
|
||||
"listCmd": "claude mcp list",
|
||||
"add": "Add a server:",
|
||||
"addCmd": "claude mcp add <name> <command> [args…]",
|
||||
"remove": "Remove this server:",
|
||||
"removeButton": "Copy remove command"
|
||||
"add": "Add a server:"
|
||||
},
|
||||
"marketplaces": {
|
||||
"title": "Marketplaces are where plugins come from",
|
||||
@@ -326,7 +326,7 @@
|
||||
"addMcpCommandPlaceholder": "npx -y @modelcontextprotocol/server-github",
|
||||
"addMcpUrlPlaceholder": "https://mcp.example.com",
|
||||
"addMcpButton": "Add",
|
||||
"addMcpHelp": "Runs `claude mcp add`. Scope user/project only — a `local`-scoped server is the CLI's per-checkout layer and isn't offered here.",
|
||||
"addMcpHelp": "Runs `claude mcp add`. \"This project\" scopes the server to the current checkout — the CLI's separate `project` scope (a shared `.mcp.json`) isn't offered here because this dashboard can't see servers stored there.",
|
||||
"mcpAdded": "Added MCP server {{name}}.",
|
||||
"mcpRemoved": "Removed MCP server {{name}}.",
|
||||
"hookEvent": "Event name (e.g. PreToolUse)",
|
||||
|
||||
@@ -126,6 +126,8 @@
|
||||
"projectScope": "Máy chủ phạm vi dự án",
|
||||
"name": "Tên",
|
||||
"scope": "Phạm vi",
|
||||
"scopeUser": "Người dùng",
|
||||
"scopeLocal": "Dự án này",
|
||||
"command": "Lệnh",
|
||||
"args": "Tham số",
|
||||
"env": "Tên env",
|
||||
@@ -133,7 +135,8 @@
|
||||
"headers": "Tên Header",
|
||||
"kind": "Loại",
|
||||
"source": "Nguồn",
|
||||
"noServers": "Không tìm thấy máy chủ MCP."
|
||||
"noServers": "Không tìm thấy máy chủ MCP.",
|
||||
"notRemovable": "Được cấu hình trong settings.json — hãy xóa ở đó; không có phạm vi CLI nào áp dụng cho mục này."
|
||||
},
|
||||
"hooks": {
|
||||
"noHooks": "Chưa cấu hình hook.",
|
||||
@@ -272,10 +275,7 @@
|
||||
"body": "Cấu hình MCP server nằm trong `~/.claude.json`, đúng tệp mà CLI đang chạy ghi liên tục. Ghi đồng thời từ dashboard sẽ phá hỏng trạng thái dự án, nên flow add/remove đi qua chính Claude Code.",
|
||||
"list": "Liệt kê server đã cấu hình:",
|
||||
"listCmd": "claude mcp list",
|
||||
"add": "Thêm server:",
|
||||
"addCmd": "claude mcp add <name> <command> [args…]",
|
||||
"remove": "Gỡ server này:",
|
||||
"removeButton": "Sao chép lệnh gỡ"
|
||||
"add": "Thêm server:"
|
||||
},
|
||||
"marketplaces": {
|
||||
"title": "Marketplace là nơi plugin đến từ đó",
|
||||
@@ -321,7 +321,7 @@
|
||||
"addMcpCommandPlaceholder": "npx -y @modelcontextprotocol/server-github",
|
||||
"addMcpUrlPlaceholder": "https://mcp.example.com",
|
||||
"addMcpButton": "Thêm",
|
||||
"addMcpHelp": "Chạy lệnh `claude mcp add`. Chỉ có phạm vi user/project — phạm vi `local` của CLI (theo từng checkout) không được cung cấp ở đây.",
|
||||
"addMcpHelp": "Chạy lệnh `claude mcp add`. \"Dự án này\" giới hạn server trong checkout hiện tại — phạm vi `project` riêng của CLI (một `.mcp.json` dùng chung) không được cung cấp ở đây vì dashboard không thể thấy các server lưu ở đó.",
|
||||
"mcpAdded": "Đã thêm máy chủ MCP {{name}}.",
|
||||
"mcpRemoved": "Đã xóa máy chủ MCP {{name}}.",
|
||||
"hookEvent": "Tên sự kiện (vd PreToolUse)",
|
||||
|
||||
@@ -2671,10 +2671,13 @@ export interface CcMcpResponse {
|
||||
projectScoped: CcMcpServer[];
|
||||
}
|
||||
|
||||
/** Scope the `claude mcp` CLI accepts from this form. Deliberately not the
|
||||
* three-value settings union — the CLI's own third scope (`local`) is a
|
||||
* different axis and is not offered here. */
|
||||
export type CcMcpScope = "user" | "project";
|
||||
/** Scope the `claude mcp` CLI accepts from this form — `user` and `local`
|
||||
* ONLY, matching what GET /mcp's `readMcpServers` can actually see:
|
||||
* `user` is `~/.claude.json` top-level, `local` is its `projects[<root>]`
|
||||
* block (the panel's `projectScoped` bucket, despite the name). The CLI's
|
||||
* `project` scope writes `.mcp.json`, which `readMcpServers` never reads —
|
||||
* offering it here would add a server the dashboard can never show exists. */
|
||||
export type CcMcpScope = "user" | "local";
|
||||
|
||||
/** Body of POST /api/cc-config/mcp/add. Either `url` (http/sse) or
|
||||
* `command`+`args` (stdio) is required, never both. `env` is stdio-only;
|
||||
|
||||
@@ -1903,8 +1903,8 @@ function McpPanel({
|
||||
aria-label={t("mcp.scope")}
|
||||
className="h-8 bg-surface-1 border border-border rounded px-2 text-[12px] text-fg-primary focus:outline-none focus:ring-1 focus:ring-accent/40"
|
||||
>
|
||||
<option value="user">{t("mcp.userScope")}</option>
|
||||
<option value="project">{t("mcp.projectScope")}</option>
|
||||
<option value="user">{t("mcp.scopeUser")}</option>
|
||||
<option value="local">{t("mcp.scopeLocal")}</option>
|
||||
</select>
|
||||
<div className="flex rounded-md border border-border overflow-hidden">
|
||||
{(["stdio", "http"] as const).map((k) => (
|
||||
@@ -1994,7 +1994,7 @@ function McpPanel({
|
||||
<McpCard
|
||||
key={`p:${s.name}:${s.source}`}
|
||||
server={s}
|
||||
scope="project"
|
||||
scope="local"
|
||||
run={run}
|
||||
busy={busy}
|
||||
t={t}
|
||||
@@ -2022,6 +2022,10 @@ function McpCard({
|
||||
}) {
|
||||
const [confirmRemove, setConfirmRemove] = useState(false);
|
||||
const busyId = `${scope}:${server.name}`;
|
||||
// Entries from ~/.claude/settings.json have no CLI scope that addresses
|
||||
// them (`claude mcp remove` only ever touches ~/.claude.json / .mcp.json)
|
||||
// — a Remove button here would look live and never work.
|
||||
const removable = server.source !== "~/.claude/settings.json";
|
||||
return (
|
||||
<div className="rounded-lg border border-border bg-surface-2 px-4 py-3">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
@@ -2063,29 +2067,33 @@ function McpCard({
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<ActionButton
|
||||
icon={Trash2}
|
||||
danger
|
||||
disabled={busy === busyId}
|
||||
onClick={() => {
|
||||
if (!confirmRemove) {
|
||||
setConfirmRemove(true);
|
||||
return;
|
||||
}
|
||||
setConfirmRemove(false);
|
||||
void run(
|
||||
busyId,
|
||||
() => api.ccConfig.mcpRemove(server.name, scope),
|
||||
t("manage.mcpRemoved", { name: server.name })
|
||||
);
|
||||
}}
|
||||
>
|
||||
{busy === busyId
|
||||
? t("manage.working")
|
||||
: confirmRemove
|
||||
? t("manage.confirmRemove")
|
||||
: t("manage.remove")}
|
||||
</ActionButton>
|
||||
{removable ? (
|
||||
<ActionButton
|
||||
icon={Trash2}
|
||||
danger
|
||||
disabled={busy === busyId}
|
||||
onClick={() => {
|
||||
if (!confirmRemove) {
|
||||
setConfirmRemove(true);
|
||||
return;
|
||||
}
|
||||
setConfirmRemove(false);
|
||||
void run(
|
||||
busyId,
|
||||
() => api.ccConfig.mcpRemove(server.name, scope),
|
||||
t("manage.mcpRemoved", { name: server.name })
|
||||
);
|
||||
}}
|
||||
>
|
||||
{busy === busyId
|
||||
? t("manage.working")
|
||||
: confirmRemove
|
||||
? t("manage.confirmRemove")
|
||||
: t("manage.remove")}
|
||||
</ActionButton>
|
||||
) : (
|
||||
<p className="text-[10px] text-fg-muted">{t("mcp.notRemovable")}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user