Skip to content

Commit 576b0fa

Browse files
vibeguiclaude
andcommitted
fix(connect-studio): make the connect command actually work end-to-end
Two real bugs surfaced by testing the generated command against a live local Studio (Claude Code → HTTP 400, then 0 tools): 1. Aggregate org resolution (HTTP 400). `/api/:org/mcp` → handleVirtualMcpRequest resolved the org ONLY from x-org-id/x-org-slug headers, which external MCP clients (Claude Code/Desktop) never send — the org is in the URL path, already in ctx.organization via resolveOrgFromPath. Fall back to it so the endpoint stops 400ing with "Agent ID or organization ID is required". 2. Empty toolset. The bare aggregate resolves to the Decopilot agent, which is a pure orchestrator with connections:[] (routes via subtask) — so an external client sees ZERO tools. Point the connect URL at `/api/:org/mcp/self` instead: the org's real management surface (Library files, agents, connections, automations, brand, AI providers, secrets). Verified live: initialize + tools/list (142 tools) + a real ORGANIZATION_LIST call all succeed over the API-key command. Also: derive the MCP server name in the command from the host (`belo-horizonte.localhost` locally, `studio.decocms.com` in prod) so each deployment gets a distinct entry and adding two never collides. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 81265d8 commit 576b0fa

3 files changed

Lines changed: 38 additions & 14 deletions

File tree

apps/mesh/src/api/routes/virtual-mcp.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,13 @@ export async function handleVirtualMcpRequest(
4747
const ctx = c.get("meshContext");
4848

4949
try {
50-
// Prefer x-org-id header (no DB lookup) over x-org-slug (requires DB lookup)
50+
// Prefer x-org-id header (no DB lookup) over x-org-slug (requires DB lookup).
51+
// External MCP clients (Claude Code/Desktop) send NEITHER — the org is in
52+
// the URL path (`/api/:org/mcp`), already resolved into `ctx.organization`
53+
// by the resolveOrgFromPath middleware. Fall back to it so the aggregate
54+
// (Decopilot) endpoint works without the internal UI's x-org-* headers;
55+
// otherwise organizationId stays null and the request 400s with
56+
// "Agent ID or organization ID is required".
5157
const orgId = c.req.header("x-org-id");
5258
const orgSlug = c.req.header("x-org-slug");
5359

@@ -60,7 +66,7 @@ export async function handleVirtualMcpRequest(
6066
.where("slug", "=", orgSlug)
6167
.executeTakeFirst()
6268
.then((org) => org?.id)
63-
: null;
69+
: (ctx.organization?.id ?? null);
6470

6571
const virtualId = virtualMcpId
6672
? virtualMcpId

apps/mesh/src/web/components/connect/install-snippet.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Button } from "@deco/ui/components/button.tsx";
22
import { useCopy } from "@deco/ui/hooks/use-copy.ts";
33
import { Check, Copy01 } from "@untitledui/icons";
4+
import { connectServerName } from "@/web/components/connect/mcp-url";
45

56
export type ConnectClient =
67
| "claude-code"
@@ -11,8 +12,6 @@ export type ConnectClient =
1112

1213
export type ConnectMode = "oauth" | "api-key";
1314

14-
const SERVER_NAME = "studio";
15-
1615
interface SnippetBlock {
1716
language: string;
1817
code: string;
@@ -32,17 +31,18 @@ function buildSnippet({
3231
apiKey?: string;
3332
}): SnippetBlock {
3433
const key = apiKey ?? "<paste-your-api-key>";
34+
const serverName = connectServerName();
3535

3636
if (client === "claude-code") {
3737
if (mode === "oauth") {
3838
return {
3939
language: "bash",
40-
code: `claude mcp add --transport http --scope user ${SERVER_NAME} ${url}`,
40+
code: `claude mcp add --transport http --scope user ${serverName} ${url}`,
4141
};
4242
}
4343
return {
4444
language: "bash",
45-
code: `claude mcp add --transport http --scope user ${SERVER_NAME} ${url} \\\n --header "Authorization: Bearer ${key}"`,
45+
code: `claude mcp add --transport http --scope user ${serverName} ${url} \\\n --header "Authorization: Bearer ${key}"`,
4646
};
4747
}
4848

@@ -54,12 +54,12 @@ function buildSnippet({
5454
return {
5555
language: "json",
5656
pathHint: "~/.cursor/mcp.json",
57-
code: JSON.stringify({ mcpServers: { [SERVER_NAME]: server } }, null, 2),
57+
code: JSON.stringify({ mcpServers: { [serverName]: server } }, null, 2),
5858
};
5959
}
6060

6161
if (client === "codex") {
62-
const lines = [`[mcp_servers.${SERVER_NAME}]`, `url = "${url}"`];
62+
const lines = [`[mcp_servers.${serverName}]`, `url = "${url}"`];
6363
if (mode === "api-key") {
6464
lines.push(`http_headers = { "Authorization" = "Bearer ${key}" }`);
6565
}
@@ -78,7 +78,7 @@ function buildSnippet({
7878
return {
7979
language: "json",
8080
pathHint: "claude_desktop_config.json",
81-
code: JSON.stringify({ mcpServers: { [SERVER_NAME]: server } }, null, 2),
81+
code: JSON.stringify({ mcpServers: { [serverName]: server } }, null, 2),
8282
};
8383
}
8484

apps/mesh/src/web/components/connect/mcp-url.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,34 @@
44
* dialog and the full Connect settings page can't drift apart.
55
*/
66

7-
/** MCP server name registered in the client's config (e.g. `studio`). */
8-
const CONNECT_SERVER_NAME = "studio";
7+
/**
8+
* MCP server name registered in the client's config — derived from the current
9+
* host so each Studio deployment gets a distinct entry and adding two never
10+
* collides: `studio.decocms.com` in prod, `belo-horizonte.localhost` locally.
11+
* Falls back to `studio` during SSR (no `window`).
12+
*/
13+
export function connectServerName(): string {
14+
if (typeof window === "undefined") return "studio";
15+
return window.location.hostname || "studio";
16+
}
917

10-
/** The org-scoped unified MCP endpoint: `<origin>/api/<slug>/mcp`. */
18+
/**
19+
* The org-scoped MCP endpoint a client should connect to:
20+
* `<origin>/api/<slug>/mcp/self`.
21+
*
22+
* NOT the bare aggregate `/api/<slug>/mcp` — that resolves to the Decopilot
23+
* agent, which is a pure orchestrator with NO directly-callable tools (it routes
24+
* everything through sub-agents via `subtask`), so an external client sees zero
25+
* tools. `/mcp/self` is the org's own management surface: Library files, agents,
26+
* connections, automations, brand, AI providers, secrets — i.e. everything you
27+
* need to actually drive the org from Claude.
28+
*/
1129
export function mcpUrl(orgSlug: string): string {
1230
const origin =
1331
typeof window === "undefined"
1432
? "http://localhost:3000"
1533
: window.location.origin;
16-
return `${origin}/api/${orgSlug}/mcp`;
34+
return `${origin}/api/${orgSlug}/mcp/self`;
1735
}
1836

1937
/**
@@ -27,5 +45,5 @@ export function claudeCodeCommandWithKey(
2745
orgSlug: string,
2846
apiKey: string,
2947
): string {
30-
return `claude mcp add --transport http --scope user ${CONNECT_SERVER_NAME} ${mcpUrl(orgSlug)} --header "Authorization: Bearer ${apiKey}"`;
48+
return `claude mcp add --transport http --scope user ${connectServerName()} ${mcpUrl(orgSlug)} --header "Authorization: Bearer ${apiKey}"`;
3149
}

0 commit comments

Comments
 (0)