Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/mcp-oauth-connected-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@cloudhal/api": patch
"@cloudhal/cli": patch
"@cloudhal/mcp-server": patch
---

Studio shows "Connected agents" and how to connect an MCP client by URL (RFC-0044 Phase 3b). On `/agents`, each member sees the apps they connected through OAuth. A client is named as the consent screen named it: a metadata document's host first, a self-registered client as not verified, a name that looks like a well-known client with a warning. Each row shows what the connection can do in words, its mode and when it was last used, and has Disconnect, which uses the existing revoke. Administrators see every member's connections under **All keys**. "Connect an MCP client" gains a **Connect by URL** tab: the deployment's `/mcp` URL, steps for Claude, Claude Code, ChatGPT and Codex, Cursor, VS Code and the MCP Inspector, and a note where a client needs dynamic client registration that is off.

`GET /api/agent-grants` now returns `client` (`kind`, a document client's `host`, `lookalike`) with the name sanitized, and with `?all=1` each owner's `role`. `/api/instance` reports `features.mcpOauthClients`: the `/mcp` resource, and whether metadata documents and dynamic registration are on. The new guide is `docs/guides/mcp-oauth.md`, and the MCP server README has an Authentication section.
1 change: 1 addition & 0 deletions apps/docs/src/lib/site-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const docsIndex: SitePage = {

- [Agents and MCP](/docs/agents): local and remote servers, modes, approvals, skills.
- [Connect an agent](/docs/agents/clients): Claude Code, Codex, Cursor, VS Code, Claude Desktop and more.
- [Remote MCP clients by URL](/docs/guides/mcp-oauth): connect Claude, ChatGPT or Cursor without a key, see and disconnect connected agents, and the operator switches.
- [Docs for agents](/docs/agents/docs-for-agents): Markdown pages, \`llms.txt\` and the docs MCP server.
- [Harden a Cloudflare deployment](/docs/guides/cloudflare-hardening).

Expand Down
92 changes: 92 additions & 0 deletions apps/studio/src/components/ConnectMcpCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { ClipboardText, SectionCard, Tabs, Text } from "@cloudhal/ui";
import { PlugsConnected } from "@phosphor-icons/react";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { useT } from "../i18n/index.tsx";
import { api } from "../lib/api.ts";
import { McpClientSetup } from "./McpClientSetup.tsx";
import { McpOauthSetup } from "./McpOauthSetup.tsx";

/**
* The "Connect an MCP client" card (DESIGN.md §2a aside): with an agent key (endpoint, header and
* ready-to-paste client config) and, where the server runs OAuth for remote clients (RFC-0044
* §4), by URL (`McpOauthSetup`). Only the key tab where OAuth is off or not reported.
*/
export function ConnectMcpCard({ endpoint }: { endpoint: string }) {
const t = useT();
const c = useT("connectedAgents");
const instance = useQuery({ queryKey: ["instance"], queryFn: api.instance, retry: false });
const oauth = instance.data?.features.mcpOauth
? instance.data.features.mcpOauthClients
: undefined;
const [tab, setTab] = useState<"url" | "key">("url");
const shown = oauth ? tab : "key";
return (
<SectionCard
title={t("agents.connect.title")}
icon={<PlugsConnected size={16} />}
description={oauth ? c("connectedAgents.setup.description") : t("agents.connect.description")}
>
{oauth && <OauthTabs tab={tab} onTab={setTab} />}
{shown === "url" && oauth ? (
<McpOauthSetup server={oauth} />
) : (
<KeySetup endpoint={endpoint} />
)}
</SectionCard>
);
}

function OauthTabs({ tab, onTab }: { tab: "url" | "key"; onTab: (tab: "url" | "key") => void }) {
const c = useT("connectedAgents");
return (
<Tabs
variant="segmented"
value={tab}
onValueChange={(v) => onTab(v === "key" ? "key" : "url")}
tabs={[
{ value: "url", label: c("connectedAgents.setup.tabs.url") },
{ value: "key", label: c("connectedAgents.setup.tabs.key") },
]}
/>
);
}

function KeySetup({ endpoint }: { endpoint: string }) {
const t = useT();
return (
<>
<dl className="grid gap-4">
<div className="grid gap-1.5">
<dt>
<Text size="sm" bold as="span">
{t("agents.connect.endpoint")}
</Text>
</dt>
<dd className="min-w-0">
<ClipboardText
size="base"
text={endpoint}
tooltip={{ text: t("common.copy"), copiedText: t("common.copied") }}
/>
</dd>
</div>
<div className="grid gap-1.5">
<dt>
<Text size="sm" bold as="span">
{t("agents.connect.header")}
</Text>
</dt>
<dd className="min-w-0">
<ClipboardText
size="base"
text="Authorization: Bearer"
tooltip={{ text: t("common.copy"), copiedText: t("common.copied") }}
/>
</dd>
</div>
</dl>
<McpClientSetup endpoint={endpoint} local />
</>
);
}
Loading
Loading