Skip to content

feat(studio): Connected agents and MCP client setup, RFC-0044 phase 3b (#144) - #179

Merged
Madraka merged 2 commits into
mainfrom
feat/mcp-oauth-connected-agents
Nov 10, 2026
Merged

Madraka merged 2 commits into
mainfrom
feat/mcp-oauth-connected-agents

Conversation

@Madraka

@Madraka Madraka commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

RFC-0044 Phase 3b (#144). This adds "Connected agents" and the Connect-by-URL setup in Studio, the e2e test against cloudhal studio --base-url, and the guides. Phase 3a (registration: CIMD, DCR and limits) was #178.

Brief → code → tests

Item Code Tests
Connected agents (own grants), Disconnect via the existing revoke apps/studio/src/components/ConnectedAgents.tsx, on /agents under the keys table (routes/AgentsPage.tsx); Disconnect → DeleteResource → api.agentGrants.revoke (DELETE /api/agent-grants/:id, unchanged) apps/studio/test/connected-agents.test.tsx "names each connection … disconnects with the revoke"
Admin view (every member's grants, agents:manage) the same component with all, under the All keys tab; GET /api/agent-grants?all=1 (exists since #176) connected-agents.test.tsx "lists every member's connections for an Administrator…"
Consent identity rules in the list: a CIMD host shown first, DCR labelled self-registered and not verified, names sanitized, look-alikes warned API: packages/api/src/auth/agent-grants.ts names each grant's client with the same client-identity.ts helpers the consent screen uses (clientKind, documentHost, lookalikeOf, displayName); UI: ClientIdentity in ConnectedAgents.tsx ("Identified by {host}" first, then the name) packages/api/src/testing-mcp-oauth-bounds.ts (sqlite and D1): a CIMD grant has {kind:"document", host, lookalike}, a DCR "Claude · from claude.ai" has {kind:"self-registered", lookalike:"Claude"} and its owner's role; Studio tests above
McpClientSetup tab: per-client steps from RFC §11, the deployment's /mcp URL, where a client needs DCR on, and where a hosted client cannot reach the URL components/ConnectMcpCard.tsx (moved out of AgentsPage.tsx): tabs Connect by URL / With an agent key; components/McpOauthSetup.tsx and lib/oauth-clients.ts (Claude, Claude Code, ChatGPT and Codex, Cursor, VS Code, MCP Inspector; Cursor and the Inspector are DCR-only); the URL and switches come from /api/instance features.mcpOauthClients (packages/api/src/app.ts). oauthClientStatus takes the resource: for Claude.ai and ChatGPT, a loopback, private, .local or plain-http /mcp gives needsPublicUrl and a banner pointing to --base-url, and the picker then starts on Claude Code connected-agents.test.tsx "shows the /mcp URL and each client's steps, and says when a client needs DCR" and "shows only the key setup where OAuth is off", "starts on Claude Code and warns about hosted clients where /mcp is local", and "tells hosted clients from local ones by the /mcp URL" (13 non-public URLs, 3 public ones); testing-mcp-oauth-bounds.ts asserts features.mcpOauthClients
E2E with the MCP TS SDK's auth() against cloudhal studio --base-url: discovery, registration, PKCE, programmatic consent, token, tools/call packages/cli/test/mcp-oauth-e2e.test.ts. Starts studio() on a free port with --base-url http://localhost:<port> and signs up the owner. auth() returns REDIRECT (checked: S256 and resource); authorize goes to /oauth/consent, then consent (POST) and the code exchange; auth() returns AUTHORIZED; callTool list_sites returns the site; Disconnect gives 204 and the old token gets 401 with resource_metadata. Runs in CI through turbo run test (it is in packages/cli/test/)
Docs docs/guides/mcp-oauth.md (new: connecting, Connected agents, operators, troubleshooting); docs/agents/clients.md (the docs site's MCP clients page: a "Connect by URL (OAuth)" section with a per-client table and troubleshooting); packages/mcp-server/README.md ("Authentication"); the docs overview links the guide (apps/docs/src/lib/site-pages.ts) apps/docs tests
i18n the new lazy namespace connectedAgents (src/i18n/lazy/connectedAgents.<locale>.ts), English by hand and 18 machine drafts via pnpm i18n:import Studio tests load every locale's catalog
Design rules PageLayout/SectionCard only, via @cloudhal/ui; the required blocks for the three new components are registered in test/design-rules.test.ts pnpm design:lint clean (406 files), design-rules 577 pass

Screenshots (pnpm design:shots, 1440 and 390 px)

These come from a throwaway cloudhal studio --base-url http://localhost:8899 with two connections made through the SDK's auth(): a DCR "Cursor" (full access, Edits) and a DCR "Claude", read-only, which triggers the look-alike warning. design:shots reported no sideways scroll and no console errors. The /mcp URL is localhost, so Connect by URL starts on Claude Code. The images live on the branch pr-assets/179-screenshots (not for merge); delete it after merging.

Your keys: Connected agents under the keys list. On phones each row stacks: agent, access, last use, with Disconnect beside it.

1440 px 390 px
Connected agents at 1440 px Connected agents at 390 px

All keys: "Everyone's connected agents", with the member (avatar, name, role). On phones the member stacks under the agent.

1440 px 390 px
Everyone's connected agents at 1440 px Everyone's connected agents at 390 px

Deviations from the RFC and the brief

  1. No mode change or elevation for grants. RFC §4 lists PATCH /api/agent-grants/:id (mode) and POST/DELETE :id/elevation with the key dialogs reused. The brief scoped this PR to list, admin view and Disconnect, and neither those routes nor their tests exist yet. Connected agents shows the mode read-only. To change it, disconnect and connect again; the consent screen sets the mode. This is a follow-up.
  2. The e2e uses DCR, not CIMD. CIMD needs a client metadata document on a public HTTPS host, which a CI runner cannot serve to itself. That path is covered by the shared suite's CIMD cases from feat(api): MCP OAuth client registration (CIMD and DCR), RFC-0044 phase 3a (#144) #178 (mocked fetch, sqlite and D1).
  3. One "Access" column holds what a connection can do (Read / Write / Publish and roll back, with scopes in the tooltip) plus its mode badge. Connected is a line under the agent's name rather than its own column. With the aside, the main column is about 690 px at 1440, and five or six columns pushed Last used out of view. Below sm (640 px) the member, access and last-use columns hide and their content stacks under the agent, as on Team (MembersPage), so nothing scrolls sideways. Disconnect is an icon button with a tooltip and the label "Disconnect {client}".
  4. Cursor's UI wording is unverified. Cursor's docs do not name the sign-in button, so the hint says "sign in when Cursor asks" and does not quote a label.
  5. Translations are machine drafts in 18 languages. Open points: Claude's menu names stay in English; "Access", the look-alike sentence and "Never" in ja/ko may need review; Turkish is a reviewed language, so connectedAgents.* in tr needs a human pass.
  6. The McpClientSetup card moved from AgentsPage.tsx into components/ConnectMcpCard.tsx (the size ratchet: AgentsPage 269 → 166 lines in scripts/size-baseline.json), and MyKeysTable was extracted beside it. Behaviour is unchanged where OAuth is off: only the key setup shows, as before.

Checks run locally (one package at a time)

  • pnpm lint (no errors), pnpm size:check, pnpm design:lint, turbo run typecheck --concurrency=1 (20/20), node scripts/mcp-tools-doc.ts --check
  • packages/api: 613 pass (sqlite and Miniflare D1)
  • apps/studio: 1322 pass after the review fixes (connected-agents 7, design-rules 577)
  • packages/cli: 444 pass, the new e2e included
  • packages/cloudflare: 57 pass; apps/docs: 35 pass, and astro build builds 123 pages

Review round 1 (9818f99)

  1. 390 px: rows stack on phones (MembersPage pattern), with no sideways scroll in the card. Both views were reshot at 1440 and 390 and are embedded above.
  2. Hosted clients and a local URL: needsPublicUrl for Claude.ai and ChatGPT, a new banner key (connectedAgents.setup.needsPublicUrl, 18 drafts), the picker defaults to Claude Code, with tests.
  3. Guide troubleshooting: three rows. rate_limited 429 for registration (10 per address per hour, 300 a day); temporarily_unavailable 429 for metadata fetches (20 a minute, wait); temporarily_unavailable 503 for the client caps (ask the operator; links the hardening section).
  4. "Stays out of the way": waits until the ["instance"] and ["agent-grants","mine"] queries have succeeded before asserting the card is absent.
  5. Design rules: ClipboardText is required in ConnectMcpCard, not AgentsPage. AgentsPage's unused imports are gone (ClipboardText, SectionCard, PlugsConnected, apiErrorText). AgentsPage's #layout rule no longer lists SectionCard, since its cards live in the two components that require it.

Changeset: patch for @cloudhal/api, @cloudhal/cli (it ships the Studio build) and @cloudhal/mcp-server (README).

🤖 Generated with Claude Code

Madraka and others added 2 commits November 10, 2026 02:25
#144)

"Connected agents" on /agents lists the apps a member connected by URL,
named as the consent screen named them: a metadata document's host
first, a self-registered client as not verified, a look-alike name with
a warning, names sanitized. Each row shows what the connection can do in
words with its mode, when it was last used and connected, and Disconnect,
which calls 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, per-client steps from RFC-0044 §11 (Claude, Claude Code, ChatGPT and
Codex, Cursor, VS Code, MCP Inspector), and a banner where a client needs
dynamic registration that is off. The key setup stays as its own tab.

The API names each grant's client (kind, host, lookalike) and each
owner's role, and /api/instance reports features.mcpOauthClients
(resource, cimd, dcr). An e2e test drives the MCP TS SDK's auth()
against `cloudhal studio --base-url`: discovery, DCR, PKCE, consent,
token, tools/call, then Disconnect and a 401.

Docs: docs/guides/mcp-oauth.md, the clients page's Connect by URL
section, and the MCP server README's Authentication section. The 18
non-English catalogs are machine drafts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com>
…f a local /mcp (#144)

Review of #179:
- Below `sm`, the member, access and last-use columns hide and stack
  under the agent (the MembersPage pattern), so a row fits next to
  Disconnect at 390 px without scrolling sideways.
- oauthClientStatus takes the /mcp resource and answers needsPublicUrl
  for Claude.ai and ChatGPT when it is loopback, private or plain http;
  Connect by URL starts on Claude Code there and shows a banner pointing
  to --base-url (new key, 18 machine drafts).
- The guide's troubleshooting table tells rate_limited (registration),
  temporarily_unavailable 429 (metadata fetches) and 503 (client caps)
  apart.
- The "stays out of the way" test waits for /api/instance and
  /api/agent-grants before asserting the card is absent.
- Design rules: ClipboardText is required in ConnectMcpCard, not
  AgentsPage; AgentsPage's unused imports are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com>
@Madraka
Madraka merged commit 6dd498c into main Nov 10, 2026
8 checks passed
@Madraka
Madraka deleted the feat/mcp-oauth-connected-agents branch November 10, 2026 00:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant