feat(cc-config): add and remove MCP servers through the claude CLI

Mutation goes through `claude mcp add|remove`, never a direct edit of
~/.claude.json — the running CLI rewrites that file concurrently, so a
dashboard write would race it. Reads stay on readMcpServers.

The plan specified a --url flag; the real CLI (v2.1.266) has none —
commandOrUrl is positional, and --header must trail it or the variadic
flag swallows the url. Built against the verified shape.
This commit is contained in:
2026-09-09 09:15:53 +07:00
parent 22fd4797aa
commit fd4edaac98
11 changed files with 878 additions and 18 deletions
+104
View File
@@ -387,6 +387,18 @@ const schemas = {
},
},
/** Response of a successful mutating `claude mcp …` run. A non-zero CLI
* exit arrives as an HTTP 422 error instead of one of these. */
CcConfigCliResult: {
type: "object",
required: ["ok", "stdout", "stderr"],
properties: {
ok: { type: "boolean", enum: [true] },
stdout: { type: "string" },
stderr: { type: "string" },
},
},
// ── Hooks ────────────────────────────────────────────────────────────
CcConfigHookEntry: {
type: "object",
@@ -1580,6 +1592,98 @@ const paths = {
},
},
"/api/cc-config/mcp/add": {
post: {
tags: ["CcConfig"],
summary: "Add an MCP server",
description:
"Mutating. Runs `claude mcp add` — never a direct edit of ~/.claude.json, which the running CLI rewrites concurrently. Body carries either `url` (+ optional `transport: http|sse` and `headers`) for a remote server, or `command` + `args` (+ optional `env`) for a stdio server; exactly one of the two forms is required. `scope` is required and is `user`|`project` only — not the settings union's third `project-local` value, which is a different axis. A non-zero CLI exit is returned as 422, not 500. Emits a `cc_config_changed` websocket event on success.",
operationId: "ccConfigPostMcpAdd",
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["name", "scope"],
properties: {
name: { type: "string", example: "vercel" },
scope: { type: "string", enum: ["user", "project"], example: "user" },
transport: { type: "string", enum: ["http", "sse"] },
url: { type: "string", example: "https://mcp.vercel.com" },
headers: {
type: "object",
additionalProperties: { type: "string" },
example: { Authorization: "Bearer xyz" },
},
command: { type: "string", example: "npx" },
args: {
type: "array",
items: { type: "string" },
example: ["-y", "server-github"],
},
env: {
type: "object",
additionalProperties: { type: "string" },
example: { GITHUB_TOKEN: "xxx" },
},
},
},
},
},
},
responses: {
200: {
description: "The CLI's stdout/stderr on success.",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/CcConfigCliResult" },
},
},
},
400: { description: "Bad name/scope/args, or an incompatible combination." },
422: { description: "The CLI exited non-zero (e.g. the name is already taken)." },
},
},
},
"/api/cc-config/mcp/remove": {
post: {
tags: ["CcConfig"],
summary: "Remove an MCP server",
description:
"Mutating. Runs `claude mcp remove`. `scope` (`user`|`project`) narrows which config layer to remove from; omit it to let the CLI find the server wherever it exists. A non-zero CLI exit (e.g. the name does not exist) is returned as 422. Emits a `cc_config_changed` websocket event on success.",
operationId: "ccConfigPostMcpRemove",
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["name"],
properties: {
name: { type: "string", example: "vercel" },
scope: { type: "string", enum: ["user", "project"] },
},
},
},
},
},
responses: {
200: {
description: "The CLI's stdout/stderr on success.",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/CcConfigCliResult" },
},
},
},
400: { description: "Bad name/scope." },
422: { description: "The CLI exited non-zero (e.g. no such server)." },
},
},
},
"/api/cc-config/hooks": {
get: {
tags: ["CcConfig"],