Skip to content

Commit 89db039

Browse files
committed
feat(mcp): add clerk mcp install/list/uninstall commands
Register the Clerk remote MCP server (https://mcp.clerk.com/mcp) in Claude Code, Cursor, VS Code, Windsurf, and Gemini, each with a JSON/agent mode and human-mode 'next steps' guidance (reload + sign-in). clerk doctor gains an MCP reachability check that probes the configured server via the MCP initialize handshake when an entry is installed (warns, never fails). URL resolution: --url > CLERK_MCP_URL > active env profile mcpUrl.
1 parent 79b09a9 commit 89db039

33 files changed

Lines changed: 2144 additions & 10 deletions

.changeset/mcp-install.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"clerk": minor
3+
---
4+
5+
Add `clerk mcp install`, `list`, and `uninstall` to register the Clerk remote MCP server (`https://mcp.clerk.com/mcp`) in Claude Code, Cursor, VS Code, Windsurf, and Gemini. `clerk doctor` gains an MCP reachability check that probes the configured server via the MCP `initialize` handshake when an entry is installed. The URL comes from the active env profile's new `mcpUrl` field (or the `CLERK_MCP_URL` override) and can be overridden per-invocation with `--url` for local worker development.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Commands:
4141
open Open Clerk resources in your browser
4242
apps Manage your Clerk applications
4343
users [options] Manage Clerk users
44+
mcp Manage the Clerk remote MCP server connection for AI editors and CLIs
4445
env Manage environment variables
4546
config Manage instance configuration
4647
enable Enable Clerk features on the linked instance

packages/cli-core/src/cli-program.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { link } from "./commands/link/index.ts";
1515
import { unlink } from "./commands/unlink/index.ts";
1616
import { apps as appsHandlers } from "./commands/apps/index.ts";
1717
import { users as usersHandlers } from "./commands/users/index.ts";
18+
import { mcp as mcpHandlers, CLIENT_IDS as MCP_CLIENT_IDS } from "./commands/mcp/index.ts";
1819
import { doctor } from "./commands/doctor/index.ts";
1920
import { switchEnv } from "./commands/switch-env/index.ts";
2021
import { openDashboard } from "./commands/open/index.ts";
@@ -473,6 +474,75 @@ Give AI agents better Clerk context: install the Clerk skills
473474
}),
474475
);
475476

477+
const mcp = program
478+
.command("mcp")
479+
.description("Manage the Clerk remote MCP server connection for AI editors and CLIs")
480+
.setExamples([
481+
{ command: "clerk mcp install", description: "Install into all detected MCP clients" },
482+
{ command: "clerk mcp install --client cursor", description: "Install into Cursor only" },
483+
{
484+
command: "clerk mcp install --url http://localhost:8787/mcp",
485+
description: "Use a local worker URL",
486+
},
487+
{ command: "clerk mcp list", description: "Show registered Clerk entries" },
488+
{ command: "clerk mcp uninstall", description: "Remove the Clerk entry from all clients" },
489+
]);
490+
491+
mcp
492+
.command("install")
493+
.description("Register the Clerk remote MCP server in supported clients")
494+
.addOption(
495+
createOption("--client <id>", "MCP client to target (repeatable). Default: all detected.")
496+
.choices([...MCP_CLIENT_IDS])
497+
.argParser(collectOptionValues)
498+
.default([] as string[]),
499+
)
500+
.option("--url <url>", "Override the MCP server URL (default: from active env profile)")
501+
.option("--name <name>", 'Entry name in the client config (default: "clerk")')
502+
.option("--all", "Install into every detected client without prompting")
503+
.option("--force", "Overwrite an existing entry pointing at a different URL")
504+
.option("--json", "Output as JSON")
505+
.setExamples([
506+
{
507+
command: "clerk mcp install",
508+
description: "Pick clients interactively (or all in agent mode)",
509+
},
510+
{ command: "clerk mcp install --all", description: "Install into every detected client" },
511+
{
512+
command: "clerk mcp install --client cursor --client vscode",
513+
description: "Install into specific clients",
514+
},
515+
{
516+
command: "clerk mcp install --url http://localhost:8787/mcp",
517+
description: "Target a local worker for development",
518+
},
519+
])
520+
.action((options) => mcpHandlers.install(options));
521+
522+
mcp
523+
.command("list")
524+
.description("List Clerk MCP entries registered across detected clients")
525+
.option("--json", "Output as JSON")
526+
.setExamples([{ command: "clerk mcp list", description: "List Clerk entries everywhere" }])
527+
.action((options) => mcpHandlers.list(options));
528+
529+
mcp
530+
.command("uninstall")
531+
.description("Remove the Clerk MCP entry from supported clients")
532+
.addOption(
533+
createOption("--client <id>", "MCP client to target (repeatable). Default: all clients.")
534+
.choices([...MCP_CLIENT_IDS])
535+
.argParser(collectOptionValues)
536+
.default([] as string[]),
537+
)
538+
.option("--name <name>", 'Entry name to remove (default: "clerk")')
539+
.option("--json", "Output as JSON")
540+
.setExamples([
541+
{ command: "clerk mcp uninstall", description: "Remove from every client" },
542+
{ command: "clerk mcp uninstall --client cursor", description: "Remove from Cursor only" },
543+
])
544+
.action((options) => mcpHandlers.uninstall(options));
545+
476546
const env = program
477547
.command("env")
478548
.description("Manage environment variables")

packages/cli-core/src/commands/doctor/README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,17 @@ clerk doctor --fix # Offer to auto-fix issues
2525

2626
## Checks
2727

28-
| Check | Category | What it verifies |
29-
| --------------------- | -------------- | ------------------------------------------------------------------ |
30-
| Authentication token | Authentication | Credential store has a stored token |
31-
| Token validity | Authentication | Token is still valid (calls `/oauth/userinfo`) |
32-
| Project linkage | Project | Current directory is linked to a Clerk app |
33-
| Linked application | Project | Linked application ID is accessible via the API |
34-
| Instances | Project | Configured dev/prod instance IDs match the application's instances |
35-
| Environment variables | Environment | .env.local or .env has Clerk keys |
36-
| CLI configuration | Configuration | CLI config file exists and parses |
37-
| Shell completion | Configuration | Shell autocompletion is installed for the detected shell |
28+
| Check | Category | What it verifies |
29+
| --------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
30+
| Authentication token | Authentication | Credential store has a stored token |
31+
| Token validity | Authentication | Token is still valid (calls `/oauth/userinfo`) |
32+
| Project linkage | Project | Current directory is linked to a Clerk app |
33+
| Linked application | Project | Linked application ID is accessible via the API |
34+
| Instances | Project | Configured dev/prod instance IDs match the application's instances |
35+
| Environment variables | Environment | .env.local or .env has Clerk keys |
36+
| CLI configuration | Configuration | CLI config file exists and parses |
37+
| Shell completion | Configuration | Shell autocompletion is installed for the detected shell |
38+
| MCP server | Integration | If a Clerk MCP entry is installed, the configured server answers the `initialize` handshake (skipped otherwise; warns, never fails) |
3839

3940
## Auto-Fix (`--fix`)
4041

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* `clerk doctor` MCP reachability check (folded in from the former
3+
* `clerk mcp doctor` subcommand).
4+
*
5+
* Kept in its own file — rather than `checks.ts` — so the doctor check graph
6+
* doesn't import `mcp/shared.ts` (env profiles, prompts) and the module cycle
7+
* that comes with it. Imports only the light `collect`/`probe` helpers.
8+
*/
9+
10+
import { collectEntries } from "../mcp/collect.ts";
11+
import { probeMcp } from "../mcp/probe.ts";
12+
import type { CheckResult } from "./types.ts";
13+
14+
const NAME = "MCP server";
15+
16+
export async function checkMcp(): Promise<CheckResult> {
17+
// Only meaningful if the user actually registered a Clerk MCP entry —
18+
// otherwise skip silently rather than probing a server they don't use.
19+
const entries = await collectEntries(process.cwd());
20+
if (entries.length === 0) {
21+
return { name: NAME, status: "pass", message: "Skipped (no Clerk MCP entry installed)" };
22+
}
23+
24+
const url = entries[0]!.url;
25+
const result = await probeMcp(url);
26+
if (result.ok) {
27+
return { name: NAME, status: "pass", message: `Reachable — ${result.serverName} (${url})` };
28+
}
29+
30+
const detail =
31+
result.error ?? (result.status !== undefined ? `HTTP ${result.status}` : "unknown");
32+
return {
33+
name: NAME,
34+
status: "warn",
35+
message: `Configured MCP server is not reachable (${url})`,
36+
detail,
37+
remedy: "Verify the server is running, or re-run `clerk mcp install` if the URL changed.",
38+
};
39+
}

packages/cli-core/src/commands/doctor/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
checkShellCompletion,
1717
checkCliVersion,
1818
} from "./checks.ts";
19+
import { checkMcp } from "./check-mcp.ts";
1920
import { formatCheckResult, formatJson } from "./format.ts";
2021
import type { CheckFn, CheckResult, DoctorContext, DoctorOptions } from "./types.ts";
2122

@@ -29,6 +30,7 @@ const BASE_CHECKS: CheckFn[] = [
2930
checkEnvVars,
3031
checkConfigFile,
3132
checkShellCompletion,
33+
checkMcp,
3234
];
3335

3436
function getChecks(): CheckFn[] {
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# `clerk mcp`
2+
3+
Manage the Clerk remote MCP server connection in supported AI clients.
4+
5+
The Clerk MCP server is hosted at `https://mcp.clerk.com/mcp` (source:
6+
[clerk/cloudflare-workers/workers/remote-mcp-server](https://github.com/clerk/cloudflare-workers/tree/main/workers/remote-mcp-server)).
7+
These subcommands register, list, remove, and probe that URL in each client's
8+
own config file. The URL is resolved in order: `--url` > the `CLERK_MCP_URL`
9+
environment variable > the active environment profile's `mcpUrl` field
10+
(`switch-env` carries the profile value automatically). `CLERK_MCP_URL` is the
11+
convenient override when developing the worker locally (e.g.
12+
`http://localhost:8787/mcp`).
13+
14+
No Clerk API endpoints are called. To verify the server is reachable, run
15+
`clerk doctor` — its MCP check performs the `initialize` handshake against the
16+
configured URL whenever a Clerk MCP entry is installed.
17+
18+
## Supported clients
19+
20+
| ID | Client | Scope | Config file |
21+
| ------------- | ------------------------ | ------- | ------------------------------------- |
22+
| `claude-code` | Claude Code | project | `<cwd>/.mcp.json` |
23+
| `cursor` | Cursor | project | `<cwd>/.cursor/mcp.json` |
24+
| `vscode` | VS Code (Copilot) | project | `<cwd>/.vscode/mcp.json` |
25+
| `windsurf` | Windsurf | user | `~/.codeium/windsurf/mcp_config.json` |
26+
| `gemini` | Gemini Code Assist / CLI | user | `~/.gemini/settings.json` |
27+
28+
## Subcommands
29+
30+
### `clerk mcp install`
31+
32+
Register the Clerk MCP server in one or more clients.
33+
34+
| Flag | Description |
35+
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
36+
| `--client <id>` | Target a specific client. Repeat for multiple. Default in agent mode: all detected. Default in human mode: interactive multiselect over detected clients. |
37+
| `--all` | Install into every detected client without prompting. |
38+
| `--url <url>` | Override the MCP URL. Defaults to the active env profile's `mcpUrl`. |
39+
| `--name <name>` | Entry key in the client config. Default: `clerk`. |
40+
| `--force` | Overwrite an entry already pointing at a different URL. Without it, the conflict is reported and skipped. |
41+
| `--json` | Emit a JSON summary on stdout instead of human-formatted output. |
42+
43+
**Conflict policy:** if an entry with the same `--name` already exists and
44+
points at the same URL, the install is a silent no-op (`status: unchanged`).
45+
If it points at a different URL, the install is skipped with a `reason`
46+
unless `--force` is passed.
47+
48+
**After install:** writing the config does not connect the server on its own.
49+
In human mode, `install` prints per-client next steps — the server only goes
50+
live once you **reload the editor**. If the server requires authentication, the
51+
editor opens a browser to **sign in** on first connect. Gemini additionally
52+
needs `npx` on `PATH`, since its entry launches `mcp-remote` as a stdio bridge.
53+
54+
### `clerk mcp list`
55+
56+
Print every Clerk-flavored MCP entry across all supported clients (entries
57+
named `clerk` or pointing at any `*.clerk.com` host).
58+
59+
### `clerk mcp uninstall`
60+
61+
Remove the named entry from each client. Throws `mcp_not_installed` (exit
62+
code 1) when nothing was removed. Removing the entry doesn't drop a live editor
63+
session, so (in human mode) it prints a next step to reload each affected editor.
64+
65+
> **Reachability:** there is no `mcp doctor` subcommand. Server health is part
66+
> of `clerk doctor`, which probes the configured MCP URL via the `initialize`
67+
> handshake when an entry is installed (warns, does not fail, when unreachable).
68+
69+
## Error codes
70+
71+
| Code | Meaning |
72+
| --------------------------- | --------------------------------------------------------------- |
73+
| `mcp_no_client_detected` | No supported client found on the system. |
74+
| `mcp_client_not_supported` | `--client <id>` is not in the supported list. |
75+
| `mcp_client_config_invalid` | An existing client config file is malformed. |
76+
| `mcp_url_required` | No `--url` provided and the active env profile has no `mcpUrl`. |
77+
| `mcp_not_installed` | `uninstall` removed nothing because no entry matched. |
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Claude Code MCP client integration.
3+
*
4+
* Writes to `.mcp.json` in the current working directory — the project-scope
5+
* config Claude Code reads automatically. Schema follows the MCP spec's HTTP
6+
* transport form: `{ type: "http", url: "<endpoint>" }`.
7+
*/
8+
9+
import { hasStringProp, makeJsonClient } from "./make-json-client.ts";
10+
import { pathExists, projectPath, userPath } from "./paths.ts";
11+
12+
export const claudeCodeClient = makeJsonClient({
13+
id: "claude-code",
14+
displayName: "Claude Code",
15+
scope: "project",
16+
activation: "Restart Claude Code, then run `/mcp` to connect (sign in if prompted).",
17+
topKey: "mcpServers",
18+
encode: (url) => ({ type: "http", url }),
19+
extractUrl: (d) => (hasStringProp(d, "url") ? d.url : undefined),
20+
configPath: (cwd) => projectPath(cwd, ".mcp.json"),
21+
detect: () => pathExists(userPath(".claude")),
22+
});

0 commit comments

Comments
 (0)