diff --git a/.changeset/mcp-oauth-connected-agents.md b/.changeset/mcp-oauth-connected-agents.md
new file mode 100644
index 00000000..e1009502
--- /dev/null
+++ b/.changeset/mcp-oauth-connected-agents.md
@@ -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.
diff --git a/apps/docs/src/lib/site-pages.ts b/apps/docs/src/lib/site-pages.ts
index a3d50474..549a84d4 100644
--- a/apps/docs/src/lib/site-pages.ts
+++ b/apps/docs/src/lib/site-pages.ts
@@ -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).
diff --git a/apps/studio/src/components/ConnectMcpCard.tsx b/apps/studio/src/components/ConnectMcpCard.tsx
new file mode 100644
index 00000000..691263a3
--- /dev/null
+++ b/apps/studio/src/components/ConnectMcpCard.tsx
@@ -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 (
+ }
+ description={oauth ? c("connectedAgents.setup.description") : t("agents.connect.description")}
+ >
+ {oauth && }
+ {shown === "url" && oauth ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+function OauthTabs({ tab, onTab }: { tab: "url" | "key"; onTab: (tab: "url" | "key") => void }) {
+ const c = useT("connectedAgents");
+ return (
+ 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 (
+ <>
+
+
+
+
+ {t("agents.connect.endpoint")}
+
+
+
+
+
+
+
+
+
+ {t("agents.connect.header")}
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/studio/src/components/ConnectedAgents.tsx b/apps/studio/src/components/ConnectedAgents.tsx
new file mode 100644
index 00000000..9c7c9669
--- /dev/null
+++ b/apps/studio/src/components/ConnectedAgents.tsx
@@ -0,0 +1,305 @@
+import {
+ Avatar,
+ Badge,
+ Button,
+ DeleteResource,
+ SectionCard,
+ SectionEmpty,
+ Table,
+ Text,
+ Tooltip,
+} from "@cloudhal/ui";
+import { LinkBreak, PlugsConnected, Warning } from "@phosphor-icons/react";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { useState } from "react";
+import { useT } from "../i18n/index.tsx";
+import { initials } from "../lib/account.ts";
+import { SCOPE_GROUPS, type ScopeGroup, scopeGroup, sortScopes } from "../lib/agents.ts";
+import { type AgentGrantSummary, type AgentScope, api } from "../lib/api.ts";
+import { ModeBadge } from "./AgentModes.tsx";
+import { deleteResourceProps } from "./deleteLabels.ts";
+import { ErrorBanner } from "./ErrorBanner.tsx";
+import { RelativeTime } from "./RelativeTime.tsx";
+import { RoleBadge } from "./Roles.tsx";
+import { TableSkeleton } from "./TableSkeleton.tsx";
+import { useToast } from "./Toast.ts";
+import { useMe } from "./useMe.ts";
+
+const GROUPS = Object.keys(SCOPE_GROUPS) as ScopeGroup[];
+const TONE: Record = {
+ read: "secondary",
+ write: "info",
+ release: "warning",
+};
+
+/**
+ * "Connected agents" on `/agents` (RFC-0044 §4): the apps connected by URL through OAuth, named
+ * as the consent screen named them (a document client by its host first; a self-registered one
+ * as not verified, with a look-alike warning), what each can do in words with its mode, when it
+ * was last used and connected, and Disconnect (`DELETE /api/agent-grants/:id`, the revoke that also
+ * ends the provider's consent and tokens). `all`: every member's connections (`agents:manage`).
+ */
+export function ConnectedAgents({ all = false }: { all?: boolean }) {
+ const t = useT();
+ const c = useT("connectedAgents");
+ const qc = useQueryClient();
+ const toast = useToast();
+ const grants = useQuery({
+ queryKey: ["agent-grants", all ? "all" : "mine"],
+ queryFn: all ? api.agentGrants.listAll : api.agentGrants.list,
+ });
+ const [toRevoke, setToRevoke] = useState();
+ const revoke = useMutation({
+ mutationFn: (grant: AgentGrantSummary) => api.agentGrants.revoke(grant.id),
+ onSuccess: async (_r, grant) => {
+ setToRevoke(undefined);
+ await qc.invalidateQueries({ queryKey: ["agent-grants"] });
+ toast.success(
+ c("connectedAgents.disconnected"),
+ c("connectedAgents.disconnectedDescription", { client: grant.clientName }),
+ );
+ },
+ });
+ const instance = useQuery({ queryKey: ["instance"], queryFn: api.instance, retry: false });
+ const items = grants.data ?? [];
+ const columns = all ? 5 : 4;
+ // Where the server runs no OAuth for remote clients, the section shows only connections left
+ // from when it did.
+ if (!instance.data?.features.mcpOauth && items.length === 0) return null;
+ return (
+ }
+ description={c(all ? "connectedAgents.allDescription" : "connectedAgents.description")}
+ actions={items.length > 0 ? {items.length} : undefined}
+ flush
+ >
+ {grants.error ? (
+
+ );
+}
+
+/** What it may do, then how (its mode): one column, so the table fits beside the aside. */
+function GrantAccess({ grant }: { grant: AgentGrantSummary }) {
+ return (
+
+
+
+
+ );
+}
+
+function GrantLastUsed({ grant }: { grant: AgentGrantSummary }) {
+ const c = useT("connectedAgents");
+ return grant.lastUsedAt ? (
+
+ ) : (
+
+ {c("connectedAgents.never")}
+
+ );
+}
+
+/** As the consent screen named the client: a document's host first, never the name alone. */
+function ClientIdentity({ grant }: { grant: AgentGrantSummary }) {
+ const c = useT("connectedAgents");
+ const { host, kind, lookalike } = grant.client;
+ return (
+
+ {host && (
+
+ {c("connectedAgents.identifiedBy", { host })}
+
+ )}
+ {grant.clientName}
+ {!host && kind === "self-registered" && (
+
+ {c("connectedAgents.selfRegistered")}
+
+ )}
+ {lookalike && (
+ }>
+ {c("connectedAgents.lookalike", { client: lookalike })}
+
+ )}
+ {/* When it was connected, here rather than in a column of its own: the table fits beside
+ the aside at 1440 px. */}
+
+
+ {c("connectedAgents.columns.connected")}
+
+
+
+
+ );
+}
+
+/** What a connection can do, in words: one pill per scope group, its scopes in the tooltip. */
+function GrantScopes({ scopes }: { scopes: readonly AgentScope[] }) {
+ const t = useT();
+ const sorted = sortScopes(scopes);
+ return (
+
+ );
+}
diff --git a/apps/studio/src/components/McpOauthSetup.tsx b/apps/studio/src/components/McpOauthSetup.tsx
new file mode 100644
index 00000000..5b9e961a
--- /dev/null
+++ b/apps/studio/src/components/McpOauthSetup.tsx
@@ -0,0 +1,96 @@
+import { Banner, ClipboardText, Select, Text } from "@cloudhal/ui";
+import { Warning } from "@phosphor-icons/react";
+import { useState } from "react";
+import { useT } from "../i18n/index.tsx";
+import {
+ defaultOauthClient,
+ OAUTH_CLIENTS,
+ type OauthClient,
+ type OauthClientStatus,
+ type OauthServerInfo,
+ oauthClientStatus,
+ oauthSnippet,
+ oauthSnippetLang,
+} from "../lib/oauth-clients.ts";
+import { CodeHighlighted } from "./CodeHighlighted.tsx";
+
+/**
+ * "Connect by URL" (RFC-0044 §4, §11): the deployment's `/mcp` URL and, for one client at a time,
+ * what to paste and where. No key: the client opens Studio to sign in and approve. A client that
+ * cannot connect says so: Cursor needs DCR, and Claude.ai and ChatGPT cannot reach a local or
+ * plain-http URL (the picker then starts on Claude Code).
+ */
+export function McpOauthSetup({
+ server,
+}: {
+ /** `features.mcpOauthClients`: the `/mcp` resource tokens are bound to, and the switches. */
+ server: OauthServerInfo;
+}) {
+ const t = useT();
+ const c = useT("connectedAgents");
+ const resource = server.resource;
+ const [client, setClient] = useState(() => defaultOauthClient(resource));
+ const snippet = oauthSnippet(client, resource);
+ const status = oauthClientStatus(client, server);
+ return (
+
+ );
+}
+
+function StatusBanner({
+ status,
+ client,
+ resource,
+}: {
+ status: Exclude;
+ client: OauthClient;
+ resource: string;
+}) {
+ const c = useT("connectedAgents");
+ const name = c(`connectedAgents.setup.clients.${client}`);
+ const text = {
+ needsDcr: () => c("connectedAgents.setup.needsDcr", { client: name }),
+ needsPublicUrl: () => c("connectedAgents.setup.needsPublicUrl", { url: resource }),
+ unavailable: () => c("connectedAgents.setup.unavailable"),
+ }[status]();
+ return } description={text} />;
+}
diff --git a/apps/studio/src/i18n/generated/params.ts b/apps/studio/src/i18n/generated/params.ts
index 0b755231..e718e1a2 100644
--- a/apps/studio/src/i18n/generated/params.ts
+++ b/apps/studio/src/i18n/generated/params.ts
@@ -226,6 +226,12 @@ export interface MessageParams {
"common.deleteResource.typeToConfirm": { name: string | number };
"common.deleteResource.warning": { name: string | number; type: string | number };
"common.rev": { rev: string | number };
+ "connectedAgents.disconnectLabel": { client: string | number };
+ "connectedAgents.disconnectedDescription": { client: string | number };
+ "connectedAgents.identifiedBy": { host: string | number };
+ "connectedAgents.lookalike": { client: string | number };
+ "connectedAgents.setup.needsDcr": { client: string | number };
+ "connectedAgents.setup.needsPublicUrl": { url: string | number };
"coverage.siteLanguage": { site: string | number; language: string | number };
"diff.assignments.added": { taxonomy: string | number; name: string | number };
"diff.assignments.primary": { taxonomy: string | number; name: string | number };
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.ar.ts b/apps/studio/src/i18n/lazy/connectedAgents.ar.ts
new file mode 100644
index 00000000..2fed6393
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.ar.ts
@@ -0,0 +1,75 @@
+// Generated by `pnpm i18n:import` (ADR-0034): ar `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: cbb941762f88c165
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "الوكلاء المتصلون",
+ description:
+ "تطبيقات ربطتها عبر URL، مثل Claude أو ChatGPT أو Cursor. يعمل كل منها باسمك، ولا يتجاوز دورك أبدًا، إلى أن تقطع اتصاله.",
+ allTitle: "الوكلاء المتصلون لدى الجميع",
+ allDescription: "اتصالات كل الأعضاء. قطع أحدها ينهيه فورًا، ويمكن لصاحبه من الأعضاء ربطه مجددًا.",
+ columns: {
+ agent: "الوكيل",
+ owner: "العضو",
+ access: "الوصول",
+ lastUsed: "آخر استخدام",
+ connected: "تاريخ الاتصال",
+ },
+ identifiedBy: "يُعرَّف عبر {host}",
+ selfRegistered: "مسجّل ذاتيًا، غير مُتحقَّق منه",
+ lookalike: "يبدو مثل {client}، ولم يُتحقَّق من أنه هو",
+ never: "أبدًا",
+ disconnect: "اقطع الاتصال",
+ disconnectLabel: "اقطع اتصال {client}",
+ resource: "اتصال",
+ disconnected: "انقطع الاتصال",
+ disconnectedDescription:
+ "لم يعد بإمكان {client} العمل في هذا الـStudio. إذا طلب الاتصال مجددًا، فستقرر من جديد.",
+ empty: "لا وكلاء متصلون عبر URL",
+ emptyDescription:
+ "أضف عنوان MCP URL لهذا الـStudio إلى Claude أو ChatGPT أو Cursor أو VS Code. توافق على كل اتصال هنا.",
+ allEmpty: "لم يربط أحد وكيلًا عبر URL بعد",
+ setup: {
+ description:
+ "أعطِ تطبيقًا عنوان MCP لهذا الـ Studio ووافق عليه عندما يطلب، أو أنشئ مفتاح وكيل للنصوص البرمجية والعملاء الذين لا يمكنهم تسجيل الدخول.",
+ tabs: {
+ key: "بمفتاح وكيل",
+ url: "الربط عبر URL",
+ },
+ url: "MCP URL",
+ urlDescription:
+ "يفتح التطبيق هذا الـStudio في متصفحك، فتسجّل الدخول وتوافق على ما يُسمح له به. لا مفتاح لنسخه.",
+ client: "التطبيق",
+ clients: {
+ claudeAi: "Claude.ai، سطح المكتب والجوال",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT وCodex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "في Claude، افتح Settings ثم Connectors، وأضف موصّلًا مخصصًا بهذا الـURL. يفتح Claude الـStudio للموافقة عليه.",
+ claudeCode:
+ "شغّله في طرفية، ثم اكتب /mcp داخل Claude Code واختر Authenticate. يفتح متصفحك الـStudio للموافقة عليه.",
+ codex:
+ "Codex: شغّل السطرين؛ يفتح الثاني الـStudio للموافقة عليه. ChatGPT: أضف موصّلًا بهذا الـURL في إعداداته.",
+ cursor:
+ "احفظه في ~/.cursor/mcp.json، أو .cursor/mcp.json في مشروع، ثم سجّل الدخول عندما يطلب Cursor ذلك. يفتح Studio للموافقة عليه.",
+ vscode:
+ "احفظه باسم .vscode/mcp.json في مشروعك، ثم شغّل الخادم. يطلب منك VS Code تسجيل الدخول ويفتح الـStudio.",
+ inspector:
+ "شغّله، واختر Streamable HTTP، والصق الـURL وافتح Auth Settings، ثم Quick OAuth Flow. مفيد للتحقق من اتصال.",
+ },
+ needsDcr:
+ "يسجّل {client} نفسه (التسجيل الديناميكي للعملاء)، وهذا معطّل على هذا الخادم. اطلب من مشغّله إعادة تفعيل CLOUDHAL_MCP_DCR، أو استخدم مفتاح وكيل.",
+ needsPublicUrl:
+ "يتصل Claude.ai وChatGPT من خوادمهما الخاصة، التي لا يمكنها الوصول إلى {url}. شغّل Studio مع --base-url مضبوطًا على عنوان https:// عام، أو استخدم تطبيقًا على هذا الكمبيوتر مثل Claude Code أو Codex.",
+ unavailable:
+ "لا توجد هنا أي طريقة مفعّلة يعرّف بها التطبيق نفسه (مستندات البيانات الوصفية والتسجيل الديناميكي). اسأل مشغّل هذا الخادم، أو استخدم مفتاح وكيل.",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.de.ts b/apps/studio/src/i18n/lazy/connectedAgents.de.ts
new file mode 100644
index 00000000..f5b35c8d
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.de.ts
@@ -0,0 +1,76 @@
+// Generated by `pnpm i18n:import` (ADR-0034): de `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: 60c90b90d7000fd8
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "Verbundene Agents",
+ description:
+ "Apps, die du per URL verbunden hast, etwa Claude, ChatGPT oder Cursor. Jede arbeitet als du, nie über deine Rolle hinaus, bis du sie trennst.",
+ allTitle: "Verbundene Agents aller Mitglieder",
+ allDescription:
+ "Die Verbindungen aller Mitglieder. Wird eine getrennt, endet sie sofort; das Mitglied kann sie wieder verbinden.",
+ columns: {
+ agent: "Agent",
+ owner: "Mitglied",
+ access: "Zugriff",
+ lastUsed: "Zuletzt verwendet",
+ connected: "Verbunden",
+ },
+ identifiedBy: "Identifiziert durch {host}",
+ selfRegistered: "Selbst registriert, nicht geprüft",
+ lookalike: "Sieht aus wie {client}, aber nicht als solches geprüft",
+ never: "Nie",
+ disconnect: "Trennen",
+ disconnectLabel: "{client} trennen",
+ resource: "Verbindung",
+ disconnected: "Getrennt",
+ disconnectedDescription:
+ "{client} kann in diesem Studio nicht mehr arbeiten. Bittet es erneut um eine Verbindung, entscheidest du neu.",
+ empty: "Keine per URL verbundenen Agents",
+ emptyDescription:
+ "Füge die MCP-URL dieses Studios zu Claude, ChatGPT, Cursor oder VS Code hinzu. Jede Verbindung bestätigst du hier.",
+ allEmpty: "Noch niemand hat einen Agent per URL verbunden",
+ setup: {
+ description:
+ "Gib einer App die MCP-URL dieses Studios und bestätige sie, wenn sie fragt, oder erstelle einen Agent-Schlüssel für Skripte und Clients, die sich nicht anmelden können.",
+ tabs: {
+ key: "Mit Agent-Schlüssel",
+ url: "Per URL verbinden",
+ },
+ url: "MCP-URL",
+ urlDescription:
+ "Die App öffnet dieses Studio in deinem Browser, du meldest dich an und bestätigst, was sie darf. Kein Schlüssel zum Kopieren.",
+ client: "App",
+ clients: {
+ claudeAi: "Claude.ai, Desktop und Mobil",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT und Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "Öffne in Claude Settings, dann Connectors, und füge einen benutzerdefinierten Connector mit dieser URL hinzu. Claude öffnet Studio zur Bestätigung.",
+ claudeCode:
+ "Führe es in einem Terminal aus, tippe dann in Claude Code /mcp ein und wähle Authenticate. Dein Browser öffnet Studio zur Bestätigung.",
+ codex:
+ "Codex: Führe beide Zeilen aus; die zweite öffnet Studio zur Bestätigung. ChatGPT: Füge in den Einstellungen einen Connector mit dieser URL hinzu.",
+ cursor:
+ "Speichere es als ~/.cursor/mcp.json oder in einem Projekt als .cursor/mcp.json und melde dich an, wenn Cursor danach fragt. Studio öffnet sich zum Bestätigen.",
+ vscode:
+ "Speichere es in deinem Projekt als .vscode/mcp.json und starte dann den Server. VS Code bittet dich, dich anzumelden, und öffnet Studio.",
+ inspector:
+ "Starte es, wähle Streamable HTTP, füge die URL ein und öffne Auth Settings, dann Quick OAuth Flow. Gut, um eine Verbindung zu prüfen.",
+ },
+ needsDcr:
+ "{client} registriert sich selbst (dynamische Client-Registrierung), und das ist auf diesem Server ausgeschaltet. Bitte den Betreiber, CLOUDHAL_MCP_DCR wieder einzuschalten, oder verwende einen Agent-Schlüssel.",
+ needsPublicUrl:
+ "Claude.ai und ChatGPT verbinden sich von ihren eigenen Servern aus, die {url} nicht erreichen. Starte Studio mit --base-url auf eine öffentliche https://-Adresse oder nutze eine App auf diesem Computer, etwa Claude Code oder Codex.",
+ unavailable:
+ "Keiner der Wege, auf denen sich eine App ausweisen kann, ist hier eingeschaltet (Metadaten-Dokumente und dynamische Registrierung). Frag den Betreiber dieses Servers oder verwende einen Agent-Schlüssel.",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.en.ts b/apps/studio/src/i18n/lazy/connectedAgents.en.ts
new file mode 100644
index 00000000..2d89f9d3
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.en.ts
@@ -0,0 +1,73 @@
+/**
+ * English `connectedAgents.*`, a lazy message namespace (`../namespaces.ts`, DESIGN.md §1): the
+ * agents connected by URL through OAuth on `/agents` and the "Connect by URL" setup (RFC-0044 §4,
+ * §11). The source of truth for its keys; the other languages are drafts from `pnpm i18n:import`.
+ */
+export const messages = {
+ connectedAgents: {
+ title: "Connected agents",
+ description:
+ "Apps you connected by URL, such as Claude, ChatGPT or Cursor. Each works as you, never beyond your role, until you disconnect it.",
+ allTitle: "Everyone's connected agents",
+ allDescription:
+ "Every member's connections. Disconnecting one ends it at once; its member can connect it again.",
+ columns: {
+ agent: "Agent",
+ owner: "Member",
+ access: "Access",
+ lastUsed: "Last used",
+ connected: "Connected",
+ },
+ identifiedBy: "Identified by {host}",
+ selfRegistered: "Self-registered, not verified",
+ lookalike: "Looks like {client}, not verified as it",
+ never: "Never",
+ disconnect: "Disconnect",
+ disconnectLabel: "Disconnect {client}",
+ resource: "connection",
+ disconnected: "Disconnected",
+ disconnectedDescription:
+ "{client} can no longer work in this Studio. If it asks to connect again, you decide again.",
+ empty: "No agents connected by URL",
+ emptyDescription:
+ "Add this Studio's MCP URL to Claude, ChatGPT, Cursor or VS Code. You approve each connection here.",
+ allEmpty: "Nobody has connected an agent by URL",
+ setup: {
+ description:
+ "Give an app this Studio's MCP URL and approve it when it asks, or create an agent key for scripts and clients that cannot sign in.",
+ tabs: { key: "With an agent key", url: "Connect by URL" },
+ url: "MCP URL",
+ urlDescription:
+ "The app opens this Studio in your browser, you sign in and approve what it may do. No key to copy.",
+ client: "App",
+ clients: {
+ claudeAi: "Claude.ai, Desktop and mobile",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT and Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "In Claude, open Settings, then Connectors, and add a custom connector with this URL. Claude opens Studio to approve it.",
+ claudeCode:
+ "Run it in a terminal, then type /mcp inside Claude Code and choose Authenticate. Your browser opens Studio to approve it.",
+ codex:
+ "Codex: run both lines; the second opens Studio to approve it. ChatGPT: add a connector with this URL in its settings.",
+ cursor:
+ "Save it as ~/.cursor/mcp.json, or .cursor/mcp.json in a project, then sign in when Cursor asks. Studio opens to approve it.",
+ vscode:
+ "Save it as .vscode/mcp.json in your project, then start the server. VS Code asks you to sign in and opens Studio.",
+ inspector:
+ "Run it, choose Streamable HTTP, paste the URL and open Auth Settings, then Quick OAuth Flow. Good for checking a connection.",
+ },
+ needsDcr:
+ "{client} registers itself (dynamic client registration), which is off on this server. Ask its operator to turn CLOUDHAL_MCP_DCR back on, or use an agent key.",
+ needsPublicUrl:
+ "Claude.ai and ChatGPT connect from their own servers, which cannot reach {url}. Start Studio with --base-url set to a public https:// address, or use an app on this computer, such as Claude Code or Codex.",
+ unavailable:
+ "Neither way an app can identify itself is on here (metadata documents and dynamic registration). Ask this server's operator, or use an agent key.",
+ },
+ },
+} as const;
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.es.ts b/apps/studio/src/i18n/lazy/connectedAgents.es.ts
new file mode 100644
index 00000000..4670dae6
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.es.ts
@@ -0,0 +1,76 @@
+// Generated by `pnpm i18n:import` (ADR-0034): es `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: 8cf81a3a03304828
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "Agentes conectados",
+ description:
+ "Apps que conectaste por URL, como Claude, ChatGPT o Cursor. Cada una trabaja como tú, nunca más allá de tu rol, hasta que la desconectes.",
+ allTitle: "Agentes conectados de todos",
+ allDescription:
+ "Las conexiones de cada miembro. Desconectar una la termina al instante; su miembro puede volver a conectarla.",
+ columns: {
+ agent: "Agente",
+ owner: "Miembro",
+ access: "Acceso",
+ lastUsed: "Último uso",
+ connected: "Conectado",
+ },
+ identifiedBy: "Identificado por {host}",
+ selfRegistered: "Autorregistrado, sin verificar",
+ lookalike: "Parece {client}, pero no se ha verificado que lo sea",
+ never: "Nunca",
+ disconnect: "Desconectar",
+ disconnectLabel: "Desconectar {client}",
+ resource: "conexión",
+ disconnected: "Desconectado",
+ disconnectedDescription:
+ "{client} ya no puede trabajar en este Studio. Si vuelve a pedir conectarse, decides de nuevo.",
+ empty: "No hay agentes conectados por URL",
+ emptyDescription:
+ "Añade la URL MCP de este Studio a Claude, ChatGPT, Cursor o VS Code. Cada conexión la apruebas aquí.",
+ allEmpty: "Nadie ha conectado un agente por URL",
+ setup: {
+ description:
+ "Da a una app la URL MCP de este Studio y apruébala cuando lo pida, o crea una clave de agente para scripts y clientes que no pueden iniciar sesión.",
+ tabs: {
+ key: "Con una clave de agente",
+ url: "Conectar por URL",
+ },
+ url: "URL MCP",
+ urlDescription:
+ "La app abre este Studio en tu navegador, inicias sesión y apruebas lo que puede hacer. Sin clave que copiar.",
+ client: "App",
+ clients: {
+ claudeAi: "Claude.ai, escritorio y móvil",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT y Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "En Claude, abre Settings y luego Connectors, y añade un conector personalizado con esta URL. Claude abre Studio para aprobarlo.",
+ claudeCode:
+ "Ejecútalo en una terminal, luego escribe /mcp dentro de Claude Code y elige Authenticate. Tu navegador abre Studio para aprobarlo.",
+ codex:
+ "Codex: ejecuta ambas líneas; la segunda abre Studio para aprobarlo. ChatGPT: añade un conector con esta URL en su configuración.",
+ cursor:
+ "Guárdalo como ~/.cursor/mcp.json, o .cursor/mcp.json en un proyecto, e inicia sesión cuando Cursor lo pida. Studio se abre para aprobarlo.",
+ vscode:
+ "Guárdalo como .vscode/mcp.json en tu proyecto y luego inicia el servidor. VS Code te pide iniciar sesión y abre Studio.",
+ inspector:
+ "Ejecútalo, elige Streamable HTTP, pega la URL y abre Auth Settings, luego Quick OAuth Flow. Útil para comprobar una conexión.",
+ },
+ needsDcr:
+ "{client} se registra por sí mismo (registro dinámico de clientes), que está desactivado en este servidor. Pide a su operador que vuelva a activar CLOUDHAL_MCP_DCR o usa una clave de agente.",
+ needsPublicUrl:
+ "Claude.ai y ChatGPT se conectan desde sus propios servidores, que no pueden llegar a {url}. Inicia Studio con --base-url apuntando a una dirección https:// pública, o usa una app en este equipo, como Claude Code o Codex.",
+ unavailable:
+ "Ninguna de las formas en que una app puede identificarse está activada aquí (documentos de metadatos y registro dinámico). Pregunta al operador de este servidor o usa una clave de agente.",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.fa.ts b/apps/studio/src/i18n/lazy/connectedAgents.fa.ts
new file mode 100644
index 00000000..9c2a778e
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.fa.ts
@@ -0,0 +1,76 @@
+// Generated by `pnpm i18n:import` (ADR-0034): fa `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: 8652b676cf654436
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "عاملهای متصل",
+ description:
+ "برنامههایی که با URL متصل کردهاید، مثل Claude، ChatGPT یا Cursor. هر کدام تا وقتی اتصالش را قطع نکنید، از طرف شما و هرگز فراتر از نقش شما کار میکند.",
+ allTitle: "عاملهای متصل همه",
+ allDescription:
+ "اتصالهای همه اعضا. قطع یک اتصال فوراً آن را پایان میدهد؛ عضو صاحب آن میتواند دوباره وصلش کند.",
+ columns: {
+ agent: "عامل",
+ owner: "عضو",
+ access: "دسترسی",
+ lastUsed: "آخرین استفاده",
+ connected: "زمان اتصال",
+ },
+ identifiedBy: "شناساییشده با {host}",
+ selfRegistered: "خودثبتشده، تأییدنشده",
+ lookalike: "شبیه {client} است، اما تأیید نشده که همان باشد",
+ never: "هرگز",
+ disconnect: "قطع اتصال",
+ disconnectLabel: "قطع اتصال {client}",
+ resource: "اتصال",
+ disconnected: "اتصال قطع شد",
+ disconnectedDescription:
+ "{client} دیگر نمیتواند در این Studio کار کند. اگر دوباره درخواست اتصال کند، دوباره شما تصمیم میگیرید.",
+ empty: "هیچ عاملی با URL متصل نیست",
+ emptyDescription:
+ "MCP URL این Studio را به Claude، ChatGPT، Cursor یا VS Code اضافه کنید. هر اتصال را همینجا تأیید میکنید.",
+ allEmpty: "هنوز کسی عاملی را با URL متصل نکرده است",
+ setup: {
+ description:
+ "نشانی MCP این Studio را به یک برنامه بده و وقتی درخواست کرد تأییدش کن، یا برای اسکریپتها و کلاینتهایی که نمیتوانند وارد شوند یک کلید agent بساز.",
+ tabs: {
+ key: "با کلید عامل",
+ url: "اتصال با URL",
+ },
+ url: "MCP URL",
+ urlDescription:
+ "برنامه این Studio را در مرورگرتان باز میکند، وارد میشوید و کارهای مجاز آن را تأیید میکنید. نیازی به کپی کلید نیست.",
+ client: "برنامه",
+ clients: {
+ claudeAi: "Claude.ai، دسکتاپ و موبایل",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT و Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "در Claude، Settings و سپس Connectors را باز کنید و یک کانکتور سفارشی با این URL اضافه کنید. Claude برای تأیید، Studio را باز میکند.",
+ claudeCode:
+ "آن را در ترمینال اجرا کنید، سپس در Claude Code عبارت /mcp را تایپ کنید و Authenticate را انتخاب کنید. مرورگرتان برای تأیید، Studio را باز میکند.",
+ codex:
+ "Codex: هر دو خط را اجرا کنید؛ خط دوم برای تأیید، Studio را باز میکند. ChatGPT: در تنظیماتش یک کانکتور با این URL اضافه کنید.",
+ cursor:
+ "آن را در ~/.cursor/mcp.json یا در یک پروژه در .cursor/mcp.json ذخیره کن، سپس وقتی Cursor خواست وارد شو. Studio برای تأیید باز میشود.",
+ vscode:
+ "آن را در پروژهتان بهصورت .vscode/mcp.json ذخیره کنید، سپس سرور را راه بیندازید. VS Code از شما میخواهد وارد شوید و Studio را باز میکند.",
+ inspector:
+ "اجرایش کنید، Streamable HTTP را انتخاب کنید، URL را بچسبانید و Auth Settings و سپس Quick OAuth Flow را باز کنید. برای بررسی یک اتصال مناسب است.",
+ },
+ needsDcr:
+ "{client} خودش را ثبت میکند (ثبت پویای کلاینت)، که روی این سرور خاموش است. از گرداننده سرور بخواهید CLOUDHAL_MCP_DCR را دوباره روشن کند، یا از کلید عامل استفاده کنید.",
+ needsPublicUrl:
+ "Claude.ai و ChatGPT از سرورهای خودشان وصل میشوند که به {url} دسترسی ندارند. Studio را با --base-url روی یک نشانی عمومی https:// اجرا کنید، یا از برنامهای روی همین رایانه مانند Claude Code یا Codex استفاده کنید.",
+ unavailable:
+ "هیچیک از راههایی که یک برنامه میتواند خودش را معرفی کند (اسناد فراداده و ثبت پویا) اینجا روشن نیست. از گرداننده این سرور بپرسید، یا از کلید عامل استفاده کنید.",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.fr.ts b/apps/studio/src/i18n/lazy/connectedAgents.fr.ts
new file mode 100644
index 00000000..280abda9
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.fr.ts
@@ -0,0 +1,76 @@
+// Generated by `pnpm i18n:import` (ADR-0034): fr `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: ddc411114a059144
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "Agents connectés",
+ description:
+ "Les apps que vous avez connectées par URL, comme Claude, ChatGPT ou Cursor. Chacune agit en votre nom, jamais au-delà de votre rôle, jusqu’à ce que vous la déconnectiez.",
+ allTitle: "Agents connectés de tous",
+ allDescription:
+ "Les connexions de chaque membre. En déconnecter une y met fin immédiatement ; son membre peut la reconnecter.",
+ columns: {
+ agent: "Agent",
+ owner: "Membre",
+ access: "Accès",
+ lastUsed: "Dernière utilisation",
+ connected: "Connecté",
+ },
+ identifiedBy: "Identifié par {host}",
+ selfRegistered: "Auto-enregistré, non vérifié",
+ lookalike: "Ressemble à {client}, sans être vérifié comme tel",
+ never: "Jamais",
+ disconnect: "Déconnecter",
+ disconnectLabel: "Déconnecter {client}",
+ resource: "connexion",
+ disconnected: "Déconnecté",
+ disconnectedDescription:
+ "{client} ne peut plus travailler dans ce Studio. S’il demande à se reconnecter, vous décidez à nouveau.",
+ empty: "Aucun agent connecté par URL",
+ emptyDescription:
+ "Ajoutez l’URL MCP de ce Studio à Claude, ChatGPT, Cursor ou VS Code. Vous approuvez chaque connexion ici.",
+ allEmpty: "Personne n’a connecté d’agent par URL",
+ setup: {
+ description:
+ "Donnez à une application l’URL MCP de ce Studio et approuvez-la quand elle le demande, ou créez une clé d’agent pour les scripts et les clients qui ne peuvent pas se connecter.",
+ tabs: {
+ key: "Avec une clé d’agent",
+ url: "Connecter par URL",
+ },
+ url: "URL MCP",
+ urlDescription:
+ "L’app ouvre ce Studio dans votre navigateur, vous vous connectez et approuvez ce qu’elle peut faire. Aucune clé à copier.",
+ client: "App",
+ clients: {
+ claudeAi: "Claude.ai, ordinateur et mobile",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT et Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "Dans Claude, ouvrez Settings, puis Connectors, et ajoutez un connecteur personnalisé avec cette URL. Claude ouvre Studio pour l’approuver.",
+ claudeCode:
+ "Exécutez-la dans un terminal, puis tapez /mcp dans Claude Code et choisissez Authenticate. Votre navigateur ouvre Studio pour l’approuver.",
+ codex:
+ "Codex : exécutez les deux lignes ; la seconde ouvre Studio pour l’approuver. ChatGPT : ajoutez un connecteur avec cette URL dans ses paramètres.",
+ cursor:
+ "Enregistrez-le dans ~/.cursor/mcp.json, ou .cursor/mcp.json dans un projet, puis connectez-vous quand Cursor le demande. Studio s’ouvre pour l’approuver.",
+ vscode:
+ "Enregistrez-le sous .vscode/mcp.json dans votre projet, puis démarrez le serveur. VS Code vous demande de vous connecter et ouvre Studio.",
+ inspector:
+ "Lancez-le, choisissez Streamable HTTP, collez l’URL et ouvrez Auth Settings, puis Quick OAuth Flow. Pratique pour vérifier une connexion.",
+ },
+ needsDcr:
+ "{client} s’enregistre lui-même (enregistrement dynamique de clients), ce qui est désactivé sur ce serveur. Demandez à son opérateur de réactiver CLOUDHAL_MCP_DCR, ou utilisez une clé d’agent.",
+ needsPublicUrl:
+ "Claude.ai et ChatGPT se connectent depuis leurs propres serveurs, qui ne peuvent pas joindre {url}. Lance Studio avec --base-url sur une adresse https:// publique, ou utilise une app sur cet ordinateur, comme Claude Code ou Codex.",
+ unavailable:
+ "Aucun des moyens par lesquels une app peut s’identifier n’est activé ici (documents de métadonnées et enregistrement dynamique). Demandez à l’opérateur de ce serveur, ou utilisez une clé d’agent.",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.he.ts b/apps/studio/src/i18n/lazy/connectedAgents.he.ts
new file mode 100644
index 00000000..a3715ccc
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.he.ts
@@ -0,0 +1,76 @@
+// Generated by `pnpm i18n:import` (ADR-0034): he `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: 4a1dab408d25ca9e
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "סוכנים מחוברים",
+ description:
+ "אפליקציות שחיברת באמצעות URL, כמו Claude, ChatGPT או Cursor. כל אחת פועלת בשמך, אף פעם לא מעבר לתפקיד שלך, עד שתנתק אותה.",
+ allTitle: "הסוכנים המחוברים של כולם",
+ allDescription:
+ "החיבורים של כל חברי הצוות. ניתוק חיבור מסיים אותו מיד; חבר הצוות שלו יכול לחבר אותו שוב.",
+ columns: {
+ agent: "סוכן",
+ owner: "חבר צוות",
+ access: "גישה",
+ lastUsed: "שימוש אחרון",
+ connected: "חובר",
+ },
+ identifiedBy: "מזוהה על ידי {host}",
+ selfRegistered: "נרשם בעצמו, לא מאומת",
+ lookalike: "נראה כמו {client}, אך לא אומת ככזה",
+ never: "אף פעם",
+ disconnect: "ניתוק",
+ disconnectLabel: "ניתוק {client}",
+ resource: "חיבור",
+ disconnected: "נותק",
+ disconnectedDescription:
+ "{client} כבר לא יכול לעבוד ב-Studio הזה. אם יבקש להתחבר שוב, ההחלטה תהיה שוב שלך.",
+ empty: "אין סוכנים שחוברו באמצעות URL",
+ emptyDescription:
+ "כדאי להוסיף את ה-MCP URL של ה-Studio הזה ל-Claude, ChatGPT, Cursor או VS Code. כל חיבור מאושר כאן.",
+ allEmpty: "אף אחד עדיין לא חיבר סוכן באמצעות URL",
+ setup: {
+ description:
+ "תן לאפליקציה את כתובת ה-MCP של ה-Studio הזה ואשר אותה כשהיא מבקשת, או צור מפתח סוכן לסקריפטים וללקוחות שלא יכולים להתחבר.",
+ tabs: {
+ key: "עם מפתח סוכן",
+ url: "חיבור באמצעות URL",
+ },
+ url: "MCP URL",
+ urlDescription:
+ "האפליקציה פותחת את ה-Studio הזה בדפדפן, ואז נכנסים ומאשרים מה מותר לה לעשות. אין מפתח להעתיק.",
+ client: "אפליקציה",
+ clients: {
+ claudeAi: "Claude.ai, מחשב ונייד",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT ו-Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "ב-Claude פותחים את Settings ואז את Connectors, ומוסיפים מחבר מותאם אישית עם ה-URL הזה. Claude פותח את Studio לאישור.",
+ claudeCode:
+ "מריצים בטרמינל, ואז מקלידים /mcp בתוך Claude Code ובוחרים Authenticate. הדפדפן פותח את Studio לאישור.",
+ codex:
+ "Codex: מריצים את שתי השורות; השנייה פותחת את Studio לאישור. ChatGPT: מוסיפים בהגדרות שלו מחבר עם ה-URL הזה.",
+ cursor:
+ "שמור אותו בתור ~/.cursor/mcp.json, או .cursor/mcp.json בפרויקט, ואז התחבר כש-Cursor מבקש. Studio נפתח לאישור.",
+ vscode:
+ "שומרים בשם .vscode/mcp.json בפרויקט, ואז מפעילים את השרת. VS Code מבקש להתחבר ופותח את Studio.",
+ inspector:
+ "מפעילים, בוחרים Streamable HTTP, מדביקים את ה-URL ופותחים את Auth Settings ואז את Quick OAuth Flow. שימושי לבדיקת חיבור.",
+ },
+ needsDcr:
+ "{client} רושם את עצמו (רישום לקוחות דינמי), ובשרת הזה האפשרות כבויה. אפשר לבקש ממפעיל השרת להפעיל שוב את CLOUDHAL_MCP_DCR, או להשתמש במפתח סוכן.",
+ needsPublicUrl:
+ "Claude.ai ו-ChatGPT מתחברים מהשרתים שלהם, שאינם יכולים להגיע אל {url}. הפעילו את Studio עם --base-url שמוגדר לכתובת https:// ציבורית, או השתמשו באפליקציה במחשב הזה, כמו Claude Code או Codex.",
+ unavailable:
+ "אף אחת מהדרכים שבהן אפליקציה יכולה לזהות את עצמה לא מופעלת כאן (מסמכי מטא-נתונים ורישום דינמי). אפשר לפנות למפעיל השרת הזה, או להשתמש במפתח סוכן.",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.hi.ts b/apps/studio/src/i18n/lazy/connectedAgents.hi.ts
new file mode 100644
index 00000000..a83aa8d8
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.hi.ts
@@ -0,0 +1,76 @@
+// Generated by `pnpm i18n:import` (ADR-0034): hi `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: 1e125b5a50bf91f2
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "कनेक्ट किए गए एजेंट",
+ description:
+ "वे ऐप जिन्हें आपने URL से कनेक्ट किया है, जैसे Claude, ChatGPT या Cursor। जब तक आप डिस्कनेक्ट न करें, हर ऐप आपकी ओर से काम करता है, कभी आपकी भूमिका से आगे नहीं।",
+ allTitle: "सभी के कनेक्ट किए गए एजेंट",
+ allDescription:
+ "हर सदस्य के कनेक्शन। किसी को डिस्कनेक्ट करने से वह तुरंत खत्म हो जाता है; उसका सदस्य उसे फिर से कनेक्ट कर सकता है।",
+ columns: {
+ agent: "एजेंट",
+ owner: "सदस्य",
+ access: "पहुँच",
+ lastUsed: "आखिरी उपयोग",
+ connected: "कनेक्ट किया गया",
+ },
+ identifiedBy: "{host} से पहचाना गया",
+ selfRegistered: "स्वयं पंजीकृत, सत्यापित नहीं",
+ lookalike: "{client} जैसा दिखता है, पर उसके रूप में सत्यापित नहीं",
+ never: "कभी नहीं",
+ disconnect: "डिस्कनेक्ट करें",
+ disconnectLabel: "{client} को डिस्कनेक्ट करें",
+ resource: "कनेक्शन",
+ disconnected: "डिस्कनेक्ट किया गया",
+ disconnectedDescription:
+ "{client} अब इस Studio में काम नहीं कर सकता। अगर वह फिर से कनेक्ट होने को कहे, तो आप फिर से तय करेंगे।",
+ empty: "URL से कोई एजेंट कनेक्ट नहीं है",
+ emptyDescription:
+ "इस Studio का MCP URL Claude, ChatGPT, Cursor या VS Code में जोड़ें। हर कनेक्शन को आप यहीं मंज़ूरी देते हैं।",
+ allEmpty: "किसी ने भी URL से एजेंट कनेक्ट नहीं किया है",
+ setup: {
+ description:
+ "किसी ऐप को इस Studio का MCP URL दें और उसके पूछने पर स्वीकृत करें, या उन स्क्रिप्ट और क्लाइंट के लिए एजेंट कुंजी बनाएं जो साइन इन नहीं कर सकते।",
+ tabs: {
+ key: "एजेंट कुंजी से",
+ url: "URL से कनेक्ट करें",
+ },
+ url: "MCP URL",
+ urlDescription:
+ "ऐप आपके ब्राउज़र में यह Studio खोलता है, आप साइन इन करके मंज़ूरी देते हैं कि वह क्या कर सकता है। कॉपी करने के लिए कोई कुंजी नहीं।",
+ client: "ऐप",
+ clients: {
+ claudeAi: "Claude.ai, डेस्कटॉप और मोबाइल",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT और Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "Claude में Settings, फिर Connectors खोलें और इस URL के साथ एक कस्टम कनेक्टर जोड़ें। Claude मंज़ूरी के लिए Studio खोलता है।",
+ claudeCode:
+ "इसे टर्मिनल में चलाएँ, फिर Claude Code में /mcp टाइप करें और Authenticate चुनें। आपका ब्राउज़र मंज़ूरी के लिए Studio खोलता है।",
+ codex:
+ "Codex: दोनों लाइनें चलाएँ; दूसरी मंज़ूरी के लिए Studio खोलती है। ChatGPT: उसकी सेटिंग्स में इस URL के साथ एक कनेक्टर जोड़ें।",
+ cursor:
+ "इसे ~/.cursor/mcp.json या किसी प्रोजेक्ट में .cursor/mcp.json के रूप में सहेजें, फिर Cursor के कहने पर साइन इन करें। स्वीकृति के लिए Studio खुलता है।",
+ vscode:
+ "इसे अपने प्रोजेक्ट में .vscode/mcp.json के रूप में सहेजें, फिर सर्वर शुरू करें। VS Code आपसे साइन इन करने को कहता है और Studio खोलता है।",
+ inspector:
+ "इसे चलाएँ, Streamable HTTP चुनें, URL पेस्ट करें और Auth Settings, फिर Quick OAuth Flow खोलें। किसी कनेक्शन को जाँचने के लिए अच्छा है।",
+ },
+ needsDcr:
+ "{client} खुद को पंजीकृत करता है (डायनैमिक क्लाइंट रजिस्ट्रेशन), जो इस सर्वर पर बंद है। इसके ऑपरेटर से CLOUDHAL_MCP_DCR फिर से चालू करने को कहें, या एजेंट कुंजी का उपयोग करें।",
+ needsPublicUrl:
+ "Claude.ai और ChatGPT अपने सर्वर से जुड़ते हैं, जो {url} तक नहीं पहुँच सकते। Studio को --base-url के साथ किसी सार्वजनिक https:// पते पर चलाएँ, या इसी कंप्यूटर के किसी ऐप का उपयोग करें, जैसे Claude Code या Codex।",
+ unavailable:
+ "यहाँ कोई भी ऐसा तरीका चालू नहीं है जिससे ऐप अपनी पहचान दे सके (मेटाडेटा दस्तावेज़ और डायनैमिक रजिस्ट्रेशन)। इस सर्वर के ऑपरेटर से पूछें, या एजेंट कुंजी का उपयोग करें।",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.id.ts b/apps/studio/src/i18n/lazy/connectedAgents.id.ts
new file mode 100644
index 00000000..90a5db68
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.id.ts
@@ -0,0 +1,76 @@
+// Generated by `pnpm i18n:import` (ADR-0034): id `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: f87f8dcc71491397
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "Agen terhubung",
+ description:
+ "Aplikasi yang Anda hubungkan lewat URL, seperti Claude, ChatGPT, atau Cursor. Masing-masing bekerja sebagai Anda, tidak pernah melampaui peran Anda, sampai Anda memutuskannya.",
+ allTitle: "Agen terhubung milik semua orang",
+ allDescription:
+ "Koneksi setiap anggota. Memutuskan salah satunya langsung mengakhirinya; anggotanya dapat menghubungkannya lagi.",
+ columns: {
+ agent: "Agen",
+ owner: "Anggota",
+ access: "Akses",
+ lastUsed: "Terakhir dipakai",
+ connected: "Terhubung",
+ },
+ identifiedBy: "Diidentifikasi oleh {host}",
+ selfRegistered: "Terdaftar sendiri, belum diverifikasi",
+ lookalike: "Tampak seperti {client}, tetapi belum diverifikasi sebagai aplikasi tersebut",
+ never: "Tidak pernah",
+ disconnect: "Putuskan",
+ disconnectLabel: "Putuskan {client}",
+ resource: "koneksi",
+ disconnected: "Terputus",
+ disconnectedDescription:
+ "{client} tidak dapat bekerja lagi di Studio ini. Jika meminta terhubung lagi, Anda memutuskan lagi.",
+ empty: "Tidak ada agen yang terhubung lewat URL",
+ emptyDescription:
+ "Tambahkan URL MCP Studio ini ke Claude, ChatGPT, Cursor, atau VS Code. Anda menyetujui setiap koneksi di sini.",
+ allEmpty: "Belum ada yang menghubungkan agen lewat URL",
+ setup: {
+ description:
+ "Berikan URL MCP Studio ini ke aplikasi dan setujui saat diminta, atau buat kunci agen untuk skrip dan klien yang tidak bisa masuk.",
+ tabs: {
+ key: "Dengan kunci agen",
+ url: "Hubungkan lewat URL",
+ },
+ url: "URL MCP",
+ urlDescription:
+ "Aplikasi membuka Studio ini di browser Anda, Anda masuk dan menyetujui apa yang boleh dilakukannya. Tidak ada kunci untuk disalin.",
+ client: "Aplikasi",
+ clients: {
+ claudeAi: "Claude.ai, desktop, dan seluler",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT dan Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "Di Claude, buka Settings, lalu Connectors, dan tambahkan konektor kustom dengan URL ini. Claude membuka Studio untuk menyetujuinya.",
+ claudeCode:
+ "Jalankan di terminal, lalu ketik /mcp di dalam Claude Code dan pilih Authenticate. Browser Anda membuka Studio untuk menyetujuinya.",
+ codex:
+ "Codex: jalankan kedua baris; baris kedua membuka Studio untuk menyetujuinya. ChatGPT: tambahkan konektor dengan URL ini di pengaturannya.",
+ cursor:
+ "Simpan sebagai ~/.cursor/mcp.json, atau .cursor/mcp.json di proyek, lalu masuk saat Cursor memintanya. Studio terbuka untuk menyetujuinya.",
+ vscode:
+ "Simpan sebagai .vscode/mcp.json di proyek Anda, lalu jalankan server. VS Code meminta Anda masuk dan membuka Studio.",
+ inspector:
+ "Jalankan, pilih Streamable HTTP, tempel URL, dan buka Auth Settings, lalu Quick OAuth Flow. Berguna untuk memeriksa koneksi.",
+ },
+ needsDcr:
+ "{client} mendaftarkan dirinya sendiri (pendaftaran klien dinamis), yang dimatikan di server ini. Minta operatornya menyalakan lagi CLOUDHAL_MCP_DCR, atau gunakan kunci agen.",
+ needsPublicUrl:
+ "Claude.ai dan ChatGPT terhubung dari server mereka sendiri, yang tidak dapat menjangkau {url}. Jalankan Studio dengan --base-url berisi alamat https:// publik, atau gunakan aplikasi di komputer ini, seperti Claude Code atau Codex.",
+ unavailable:
+ "Tidak satu pun cara aplikasi mengidentifikasi dirinya aktif di sini (dokumen metadata dan pendaftaran dinamis). Tanyakan operator server ini, atau gunakan kunci agen.",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.it.ts b/apps/studio/src/i18n/lazy/connectedAgents.it.ts
new file mode 100644
index 00000000..40c9e1ed
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.it.ts
@@ -0,0 +1,76 @@
+// Generated by `pnpm i18n:import` (ADR-0034): it `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: 441130c100a07796
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "Agenti connessi",
+ description:
+ "App che hai collegato tramite URL, come Claude, ChatGPT o Cursor. Ognuna lavora come te, mai oltre il tuo ruolo, finché non la scolleghi.",
+ allTitle: "Agenti connessi di tutti",
+ allDescription:
+ "Le connessioni di ogni membro. Scollegarne una la chiude subito; il suo membro può collegarla di nuovo.",
+ columns: {
+ agent: "Agente",
+ owner: "Membro",
+ access: "Accesso",
+ lastUsed: "Ultimo uso",
+ connected: "Collegato",
+ },
+ identifiedBy: "Identificato da {host}",
+ selfRegistered: "Registrato da sé, non verificato",
+ lookalike: "Sembra {client}, ma non è verificato come tale",
+ never: "Mai",
+ disconnect: "Scollega",
+ disconnectLabel: "Scollega {client}",
+ resource: "connessione",
+ disconnected: "Scollegato",
+ disconnectedDescription:
+ "{client} non può più lavorare in questo Studio. Se chiede di nuovo di collegarsi, decidi di nuovo tu.",
+ empty: "Nessun agente collegato tramite URL",
+ emptyDescription:
+ "Aggiungi l’URL MCP di questo Studio a Claude, ChatGPT, Cursor o VS Code. Approvi ogni connessione qui.",
+ allEmpty: "Nessuno ha collegato un agente tramite URL",
+ setup: {
+ description:
+ "Dai a un’app l’URL MCP di questo Studio e approvala quando lo chiede, oppure crea una chiave agente per script e client che non possono accedere.",
+ tabs: {
+ key: "Con una chiave agente",
+ url: "Collega tramite URL",
+ },
+ url: "URL MCP",
+ urlDescription:
+ "L’app apre questo Studio nel tuo browser, accedi e approvi cosa può fare. Nessuna chiave da copiare.",
+ client: "App",
+ clients: {
+ claudeAi: "Claude.ai, desktop e mobile",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT e Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "In Claude, apri Settings, poi Connectors, e aggiungi un connettore personalizzato con questo URL. Claude apre Studio per approvarlo.",
+ claudeCode:
+ "Eseguilo in un terminale, poi digita /mcp in Claude Code e scegli Authenticate. Il browser apre Studio per approvarlo.",
+ codex:
+ "Codex: esegui entrambe le righe; la seconda apre Studio per approvarlo. ChatGPT: aggiungi un connettore con questo URL nelle sue impostazioni.",
+ cursor:
+ "Salvalo come ~/.cursor/mcp.json, o .cursor/mcp.json in un progetto, poi accedi quando Cursor lo chiede. Studio si apre per approvarlo.",
+ vscode:
+ "Salvalo come .vscode/mcp.json nel tuo progetto, poi avvia il server. VS Code ti chiede di accedere e apre Studio.",
+ inspector:
+ "Avvialo, scegli Streamable HTTP, incolla l’URL e apri Auth Settings, poi Quick OAuth Flow. Utile per verificare una connessione.",
+ },
+ needsDcr:
+ "{client} si registra da sé (registrazione dinamica dei client), che su questo server è disattivata. Chiedi al suo gestore di riattivare CLOUDHAL_MCP_DCR o usa una chiave agente.",
+ needsPublicUrl:
+ "Claude.ai e ChatGPT si collegano dai propri server, che non possono raggiungere {url}. Avvia Studio con --base-url impostato su un indirizzo https:// pubblico, oppure usa un'app su questo computer, come Claude Code o Codex.",
+ unavailable:
+ "Nessuno dei modi con cui un’app può identificarsi è attivo qui (documenti di metadati e registrazione dinamica). Chiedi al gestore di questo server o usa una chiave agente.",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.ja.ts b/apps/studio/src/i18n/lazy/connectedAgents.ja.ts
new file mode 100644
index 00000000..1162cd7d
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.ja.ts
@@ -0,0 +1,76 @@
+// Generated by `pnpm i18n:import` (ADR-0034): ja `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: 462771123be166f9
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "接続済みのエージェント",
+ description:
+ "Claude、ChatGPT、Cursor など、URL で接続したアプリです。接続を解除するまで、それぞれがあなたとして、あなたのロールの範囲内で動作します。",
+ allTitle: "全員の接続済みエージェント",
+ allDescription:
+ "すべてのメンバーの接続です。接続を解除するとすぐに終了します。そのメンバーは再び接続できます。",
+ columns: {
+ agent: "エージェント",
+ owner: "メンバー",
+ access: "アクセス",
+ lastUsed: "最終使用",
+ connected: "接続日時",
+ },
+ identifiedBy: "{host} で識別",
+ selfRegistered: "自己登録、未確認",
+ lookalike: "{client} に見えますが、本物かは確認されていません",
+ never: "なし",
+ disconnect: "接続を解除",
+ disconnectLabel: "{client} の接続を解除",
+ resource: "接続",
+ disconnected: "接続を解除しました",
+ disconnectedDescription:
+ "{client} はこの Studio で作業できなくなりました。再び接続を求めてきた場合は、改めて判断します。",
+ empty: "URL で接続されたエージェントはありません",
+ emptyDescription:
+ "この Studio の MCP URL を Claude、ChatGPT、Cursor、VS Code に追加してください。各接続はここで承認します。",
+ allEmpty: "URL でエージェントを接続したメンバーはいません",
+ setup: {
+ description:
+ "アプリにこの Studio の MCP URL を渡し、求められたら承認します。サインインできないスクリプトやクライアントにはエージェントキーを作成します。",
+ tabs: {
+ key: "エージェントキーで",
+ url: "URL で接続",
+ },
+ url: "MCP URL",
+ urlDescription:
+ "アプリがブラウザでこの Studio を開きます。サインインして、許可する操作を承認します。キーをコピーする必要はありません。",
+ client: "アプリ",
+ clients: {
+ claudeAi: "Claude.ai、デスクトップ、モバイル",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT と Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "Claude で Settings、Connectors の順に開き、この URL でカスタムコネクタを追加します。Claude が承認のために Studio を開きます。",
+ claudeCode:
+ "ターミナルで実行し、Claude Code で /mcp と入力して Authenticate を選びます。ブラウザが承認のために Studio を開きます。",
+ codex:
+ "Codex:両方の行を実行します。2 行目で承認のために Studio が開きます。ChatGPT:設定でこの URL のコネクタを追加します。",
+ cursor:
+ "~/.cursor/mcp.json、またはプロジェクトの .cursor/mcp.json として保存し、Cursor に求められたらサインインします。承認のために Studio が開きます。",
+ vscode:
+ "プロジェクトに .vscode/mcp.json として保存し、サーバーを起動します。VS Code がサインインを求め、Studio を開きます。",
+ inspector:
+ "起動して Streamable HTTP を選び、URL を貼り付けて Auth Settings を開き、Quick OAuth Flow を実行します。接続の確認に便利です。",
+ },
+ needsDcr:
+ "{client} は自身を登録します(動的クライアント登録)が、このサーバーではオフになっています。運営者に CLOUDHAL_MCP_DCR を再びオンにするよう依頼するか、エージェントキーを使ってください。",
+ needsPublicUrl:
+ "Claude.ai と ChatGPT は自社のサーバーから接続するため、{url} に到達できません。--base-url に公開 https:// アドレスを指定して Studio を起動するか、Claude Code や Codex など、このコンピューター上のアプリを使ってください。",
+ unavailable:
+ "アプリが自身を識別する方法(メタデータドキュメントと動的登録)がどちらもここではオンになっていません。このサーバーの運営者に問い合わせるか、エージェントキーを使ってください。",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.ko.ts b/apps/studio/src/i18n/lazy/connectedAgents.ko.ts
new file mode 100644
index 00000000..1bd6d311
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.ko.ts
@@ -0,0 +1,76 @@
+// Generated by `pnpm i18n:import` (ADR-0034): ko `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: 974c07fee923464b
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "연결된 에이전트",
+ description:
+ "Claude, ChatGPT, Cursor처럼 URL로 연결한 앱입니다. 연결을 해제할 때까지 각 앱은 내 역할을 넘지 않는 범위에서 나로서 작업합니다.",
+ allTitle: "모든 멤버의 연결된 에이전트",
+ allDescription:
+ "모든 멤버의 연결입니다. 연결을 해제하면 즉시 끝나며, 해당 멤버는 다시 연결할 수 있습니다.",
+ columns: {
+ agent: "에이전트",
+ owner: "멤버",
+ access: "액세스",
+ lastUsed: "마지막 사용",
+ connected: "연결일",
+ },
+ identifiedBy: "{host}(으)로 식별됨",
+ selfRegistered: "자체 등록됨, 확인되지 않음",
+ lookalike: "{client}처럼 보이지만 확인되지 않음",
+ never: "없음",
+ disconnect: "연결 해제",
+ disconnectLabel: "{client} 연결 해제",
+ resource: "연결",
+ disconnected: "연결 해제됨",
+ disconnectedDescription:
+ "{client}은(는) 더 이상 이 Studio에서 작업할 수 없습니다. 다시 연결을 요청하면 다시 결정하게 됩니다.",
+ empty: "URL로 연결된 에이전트가 없습니다",
+ emptyDescription:
+ "이 Studio의 MCP URL을 Claude, ChatGPT, Cursor 또는 VS Code에 추가하세요. 각 연결은 여기에서 승인합니다.",
+ allEmpty: "아직 아무도 URL로 에이전트를 연결하지 않았습니다",
+ setup: {
+ description:
+ "앱에 이 Studio의 MCP URL을 주고 요청하면 승인하세요. 로그인할 수 없는 스크립트와 클라이언트에는 에이전트 키를 만드세요.",
+ tabs: {
+ key: "에이전트 키로",
+ url: "URL로 연결",
+ },
+ url: "MCP URL",
+ urlDescription:
+ "앱이 브라우저에서 이 Studio를 열면 로그인하고 앱이 할 수 있는 일을 승인합니다. 복사할 키가 없습니다.",
+ client: "앱",
+ clients: {
+ claudeAi: "Claude.ai, 데스크톱 및 모바일",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT 및 Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "Claude에서 Settings를 열고 Connectors로 이동한 뒤 이 URL로 사용자 지정 커넥터를 추가하세요. Claude가 승인을 위해 Studio를 엽니다.",
+ claudeCode:
+ "터미널에서 실행한 다음 Claude Code에서 /mcp를 입력하고 Authenticate를 선택하세요. 브라우저가 승인을 위해 Studio를 엽니다.",
+ codex:
+ "Codex: 두 줄을 모두 실행하세요. 두 번째 줄이 승인을 위해 Studio를 엽니다. ChatGPT: 설정에서 이 URL로 커넥터를 추가하세요.",
+ cursor:
+ "~/.cursor/mcp.json 또는 프로젝트의 .cursor/mcp.json으로 저장한 뒤 Cursor가 요청하면 로그인하세요. 승인을 위해 Studio가 열립니다.",
+ vscode:
+ "프로젝트에 .vscode/mcp.json으로 저장한 다음 서버를 시작하세요. VS Code가 로그인을 요청하고 Studio를 엽니다.",
+ inspector:
+ "실행한 뒤 Streamable HTTP를 선택하고 URL을 붙여 넣은 다음 Auth Settings를 열고 Quick OAuth Flow를 실행하세요. 연결을 확인할 때 유용합니다.",
+ },
+ needsDcr:
+ "{client}은(는) 스스로 등록하는데(동적 클라이언트 등록), 이 서버에서는 꺼져 있습니다. 운영자에게 CLOUDHAL_MCP_DCR을 다시 켜 달라고 요청하거나 에이전트 키를 사용하세요.",
+ needsPublicUrl:
+ "Claude.ai와 ChatGPT는 자체 서버에서 연결하므로 {url}에 접근할 수 없습니다. --base-url을 공개 https:// 주소로 설정해 Studio를 시작하거나, Claude Code나 Codex처럼 이 컴퓨터의 앱을 사용하세요.",
+ unavailable:
+ "앱이 자신을 식별하는 두 가지 방법(메타데이터 문서와 동적 등록)이 모두 여기에서 꺼져 있습니다. 이 서버의 운영자에게 문의하거나 에이전트 키를 사용하세요.",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.nl.ts b/apps/studio/src/i18n/lazy/connectedAgents.nl.ts
new file mode 100644
index 00000000..1eb9febf
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.nl.ts
@@ -0,0 +1,76 @@
+// Generated by `pnpm i18n:import` (ADR-0034): nl `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: ca3a5fc6ceb92dc5
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "Gekoppelde agents",
+ description:
+ "Apps die je via een URL hebt gekoppeld, zoals Claude, ChatGPT of Cursor. Elke app werkt namens jou, nooit buiten je rol, tot je hem ontkoppelt.",
+ allTitle: "Gekoppelde agents van iedereen",
+ allDescription:
+ "De koppelingen van elk lid. Een koppeling verbreken beëindigt haar meteen; het lid kan haar opnieuw koppelen.",
+ columns: {
+ agent: "Agent",
+ owner: "Lid",
+ access: "Toegang",
+ lastUsed: "Laatst gebruikt",
+ connected: "Gekoppeld",
+ },
+ identifiedBy: "Geïdentificeerd door {host}",
+ selfRegistered: "Zelf geregistreerd, niet geverifieerd",
+ lookalike: "Lijkt op {client}, maar niet als zodanig geverifieerd",
+ never: "Nooit",
+ disconnect: "Ontkoppelen",
+ disconnectLabel: "{client} ontkoppelen",
+ resource: "koppeling",
+ disconnected: "Ontkoppeld",
+ disconnectedDescription:
+ "{client} kan niet meer in deze Studio werken. Vraagt hij opnieuw om te koppelen, dan beslis je opnieuw.",
+ empty: "Geen agents via een URL gekoppeld",
+ emptyDescription:
+ "Voeg de MCP-URL van deze Studio toe aan Claude, ChatGPT, Cursor of VS Code. Elke koppeling keur je hier goed.",
+ allEmpty: "Niemand heeft een agent via een URL gekoppeld",
+ setup: {
+ description:
+ "Geef een app de MCP-URL van deze Studio en keur hem goed als hij erom vraagt, of maak een agentsleutel voor scripts en clients die niet kunnen inloggen.",
+ tabs: {
+ key: "Met een agentsleutel",
+ url: "Koppelen via URL",
+ },
+ url: "MCP-URL",
+ urlDescription:
+ "De app opent deze Studio in je browser, je logt in en keurt goed wat hij mag doen. Geen sleutel om te kopiëren.",
+ client: "App",
+ clients: {
+ claudeAi: "Claude.ai, desktop en mobiel",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT en Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "Open in Claude Settings en daarna Connectors, en voeg een aangepaste connector met deze URL toe. Claude opent Studio om hem goed te keuren.",
+ claudeCode:
+ "Voer het uit in een terminal, typ daarna /mcp in Claude Code en kies Authenticate. Je browser opent Studio om het goed te keuren.",
+ codex:
+ "Codex: voer beide regels uit; de tweede opent Studio om het goed te keuren. ChatGPT: voeg in de instellingen een connector met deze URL toe.",
+ cursor:
+ "Sla het op als ~/.cursor/mcp.json, of .cursor/mcp.json in een project, en log in wanneer Cursor daarom vraagt. Studio opent om het goed te keuren.",
+ vscode:
+ "Sla het op als .vscode/mcp.json in je project en start dan de server. VS Code vraagt je in te loggen en opent Studio.",
+ inspector:
+ "Start het, kies Streamable HTTP, plak de URL en open Auth Settings, daarna Quick OAuth Flow. Handig om een koppeling te controleren.",
+ },
+ needsDcr:
+ "{client} registreert zichzelf (dynamische clientregistratie), en dat staat uit op deze server. Vraag de beheerder om CLOUDHAL_MCP_DCR weer aan te zetten, of gebruik een agentsleutel.",
+ needsPublicUrl:
+ "Claude.ai en ChatGPT verbinden vanaf hun eigen servers, die {url} niet kunnen bereiken. Start Studio met --base-url op een openbaar https://-adres, of gebruik een app op deze computer, zoals Claude Code of Codex.",
+ unavailable:
+ "Geen van beide manieren waarop een app zich kan identificeren staat hier aan (metadatadocumenten en dynamische registratie). Vraag de beheerder van deze server, of gebruik een agentsleutel.",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.pl.ts b/apps/studio/src/i18n/lazy/connectedAgents.pl.ts
new file mode 100644
index 00000000..b0675335
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.pl.ts
@@ -0,0 +1,76 @@
+// Generated by `pnpm i18n:import` (ADR-0034): pl `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: 9a3f7aad53cf6811
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "Połączone agenty",
+ description:
+ "Aplikacje połączone przez ciebie za pomocą URL, takie jak Claude, ChatGPT czy Cursor. Każda działa jako ty, nigdy poza twoją rolą, dopóki jej nie odłączysz.",
+ allTitle: "Połączone agenty wszystkich",
+ allDescription:
+ "Połączenia każdego członka. Odłączenie jednego natychmiast je kończy; jego członek może połączyć je ponownie.",
+ columns: {
+ agent: "Agent",
+ owner: "Członek",
+ access: "Dostęp",
+ lastUsed: "Ostatnio użyty",
+ connected: "Połączono",
+ },
+ identifiedBy: "Identyfikowany przez {host}",
+ selfRegistered: "Zarejestrowany samodzielnie, niezweryfikowany",
+ lookalike: "Wygląda jak {client}, ale nie zweryfikowano, że nim jest",
+ never: "Nigdy",
+ disconnect: "Odłącz",
+ disconnectLabel: "Odłącz {client}",
+ resource: "połączenie",
+ disconnected: "Odłączono",
+ disconnectedDescription:
+ "{client} nie może już pracować w tym Studio. Jeśli znowu poprosi o połączenie, zdecydujesz od nowa.",
+ empty: "Brak agentów połączonych przez URL",
+ emptyDescription:
+ "Dodaj URL MCP tego Studio do Claude, ChatGPT, Cursor lub VS Code. Każde połączenie zatwierdzasz tutaj.",
+ allEmpty: "Nikt nie połączył agenta przez URL",
+ setup: {
+ description:
+ "Podaj aplikacji adres URL MCP tego Studio i zatwierdź ją, gdy poprosi, albo utwórz klucz agenta dla skryptów i klientów, które nie mogą się zalogować.",
+ tabs: {
+ key: "Kluczem agenta",
+ url: "Połącz przez URL",
+ },
+ url: "URL MCP",
+ urlDescription:
+ "Aplikacja otwiera to Studio w twojej przeglądarce, logujesz się i zatwierdzasz, co może robić. Bez kopiowania klucza.",
+ client: "Aplikacja",
+ clients: {
+ claudeAi: "Claude.ai, komputer i urządzenia mobilne",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT i Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "W Claude otwórz Settings, potem Connectors, i dodaj niestandardowy konektor z tym URL. Claude otworzy Studio, aby go zatwierdzić.",
+ claudeCode:
+ "Uruchom to w terminalu, potem wpisz /mcp w Claude Code i wybierz Authenticate. Przeglądarka otworzy Studio, aby to zatwierdzić.",
+ codex:
+ "Codex: uruchom obie linie; druga otworzy Studio, aby to zatwierdzić. ChatGPT: dodaj w jego ustawieniach konektor z tym URL.",
+ cursor:
+ "Zapisz jako ~/.cursor/mcp.json lub .cursor/mcp.json w projekcie, a potem zaloguj się, gdy Cursor o to poprosi. Studio otworzy się do zatwierdzenia.",
+ vscode:
+ "Zapisz to jako .vscode/mcp.json w projekcie, a potem uruchom serwer. VS Code poprosi o zalogowanie i otworzy Studio.",
+ inspector:
+ "Uruchom go, wybierz Streamable HTTP, wklej URL i otwórz Auth Settings, potem Quick OAuth Flow. Przydatne do sprawdzenia połączenia.",
+ },
+ needsDcr:
+ "{client} rejestruje się sam (dynamiczna rejestracja klientów), a ta jest na tym serwerze wyłączona. Poproś jego operatora o ponowne włączenie CLOUDHAL_MCP_DCR albo użyj klucza agenta.",
+ needsPublicUrl:
+ "Claude.ai i ChatGPT łączą się ze swoich serwerów, które nie mają dostępu do {url}. Uruchom Studio z --base-url ustawionym na publiczny adres https:// albo użyj aplikacji na tym komputerze, np. Claude Code lub Codex.",
+ unavailable:
+ "Żaden ze sposobów, w jaki aplikacja może się przedstawić, nie jest tu włączony (dokumenty metadanych i dynamiczna rejestracja). Zapytaj operatora tego serwera albo użyj klucza agenta.",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.pt-BR.ts b/apps/studio/src/i18n/lazy/connectedAgents.pt-BR.ts
new file mode 100644
index 00000000..6dbda482
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.pt-BR.ts
@@ -0,0 +1,76 @@
+// Generated by `pnpm i18n:import` (ADR-0034): pt-BR `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: 8cace7b8c526facb
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "Agentes conectados",
+ description:
+ "Apps que você conectou por URL, como Claude, ChatGPT ou Cursor. Cada um trabalha como você, nunca além da sua função, até você desconectá-lo.",
+ allTitle: "Agentes conectados de todos",
+ allDescription:
+ "As conexões de cada membro. Desconectar uma a encerra na hora; o membro pode conectá-la de novo.",
+ columns: {
+ agent: "Agente",
+ owner: "Membro",
+ access: "Acesso",
+ lastUsed: "Último uso",
+ connected: "Conectado",
+ },
+ identifiedBy: "Identificado por {host}",
+ selfRegistered: "Autorregistrado, não verificado",
+ lookalike: "Parece {client}, mas não foi verificado como tal",
+ never: "Nunca",
+ disconnect: "Desconectar",
+ disconnectLabel: "Desconectar {client}",
+ resource: "conexão",
+ disconnected: "Desconectado",
+ disconnectedDescription:
+ "{client} não pode mais trabalhar neste Studio. Se pedir para conectar de novo, você decide de novo.",
+ empty: "Nenhum agente conectado por URL",
+ emptyDescription:
+ "Adicione a URL MCP deste Studio ao Claude, ChatGPT, Cursor ou VS Code. Você aprova cada conexão aqui.",
+ allEmpty: "Ninguém conectou um agente por URL",
+ setup: {
+ description:
+ "Dê a um app a URL MCP deste Studio e aprove quando ele pedir, ou crie uma chave de agente para scripts e clientes que não podem entrar.",
+ tabs: {
+ key: "Com uma chave de agente",
+ url: "Conectar por URL",
+ },
+ url: "URL MCP",
+ urlDescription:
+ "O app abre este Studio no seu navegador, você entra e aprova o que ele pode fazer. Nenhuma chave para copiar.",
+ client: "App",
+ clients: {
+ claudeAi: "Claude.ai, desktop e celular",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT e Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "No Claude, abra Settings, depois Connectors, e adicione um conector personalizado com esta URL. O Claude abre o Studio para aprovar.",
+ claudeCode:
+ "Execute em um terminal, depois digite /mcp no Claude Code e escolha Authenticate. Seu navegador abre o Studio para aprovar.",
+ codex:
+ "Codex: execute as duas linhas; a segunda abre o Studio para aprovar. ChatGPT: adicione um conector com esta URL nas configurações dele.",
+ cursor:
+ "Salve como ~/.cursor/mcp.json, ou .cursor/mcp.json em um projeto, e entre quando o Cursor pedir. O Studio abre para aprovar.",
+ vscode:
+ "Salve como .vscode/mcp.json no seu projeto e depois inicie o servidor. O VS Code pede que você entre e abre o Studio.",
+ inspector:
+ "Execute, escolha Streamable HTTP, cole a URL e abra Auth Settings, depois Quick OAuth Flow. Bom para verificar uma conexão.",
+ },
+ needsDcr:
+ "{client} se registra sozinho (registro dinâmico de clientes), o que está desativado neste servidor. Peça ao operador dele para reativar CLOUDHAL_MCP_DCR ou use uma chave de agente.",
+ needsPublicUrl:
+ "Claude.ai e ChatGPT se conectam a partir dos próprios servidores, que não alcançam {url}. Inicie o Studio com --base-url apontando para um endereço https:// público, ou use um app neste computador, como Claude Code ou Codex.",
+ unavailable:
+ "Nenhuma das formas de um app se identificar está ativada aqui (documentos de metadados e registro dinâmico). Pergunte ao operador deste servidor ou use uma chave de agente.",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.ru.ts b/apps/studio/src/i18n/lazy/connectedAgents.ru.ts
new file mode 100644
index 00000000..96f7909e
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.ru.ts
@@ -0,0 +1,76 @@
+// Generated by `pnpm i18n:import` (ADR-0034): ru `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: 54b520270734ea3a
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "Подключённые агенты",
+ description:
+ "Приложения, которые вы подключили по URL, например Claude, ChatGPT или Cursor. Каждое работает от вашего имени, никогда не выходя за рамки вашей роли, пока вы его не отключите.",
+ allTitle: "Подключённые агенты всех участников",
+ allDescription:
+ "Подключения всех участников. Отключение сразу завершает подключение; участник может подключить его снова.",
+ columns: {
+ agent: "Агент",
+ owner: "Участник",
+ access: "Доступ",
+ lastUsed: "Последнее использование",
+ connected: "Подключён",
+ },
+ identifiedBy: "Идентифицирован через {host}",
+ selfRegistered: "Зарегистрирован сам, не проверен",
+ lookalike: "Похож на {client}, но не подтверждён как он",
+ never: "Никогда",
+ disconnect: "Отключить",
+ disconnectLabel: "Отключить {client}",
+ resource: "подключение",
+ disconnected: "Отключено",
+ disconnectedDescription:
+ "{client} больше не может работать в этом Studio. Если он снова попросит подключиться, вы решите заново.",
+ empty: "Нет агентов, подключённых по URL",
+ emptyDescription:
+ "Добавьте MCP URL этого Studio в Claude, ChatGPT, Cursor или VS Code. Каждое подключение вы одобряете здесь.",
+ allEmpty: "Никто ещё не подключил агента по URL",
+ setup: {
+ description:
+ "Дайте приложению MCP URL этого Studio и одобрите его, когда оно попросит, или создайте ключ агента для скриптов и клиентов, которые не могут войти.",
+ tabs: {
+ key: "С ключом агента",
+ url: "Подключить по URL",
+ },
+ url: "MCP URL",
+ urlDescription:
+ "Приложение открывает этот Studio в вашем браузере, вы входите и разрешаете, что ему можно делать. Копировать ключ не нужно.",
+ client: "Приложение",
+ clients: {
+ claudeAi: "Claude.ai, компьютер и телефон",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT и Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "В Claude откройте Settings, затем Connectors и добавьте пользовательский коннектор с этим URL. Claude откроет Studio для одобрения.",
+ claudeCode:
+ "Выполните это в терминале, затем введите /mcp в Claude Code и выберите Authenticate. Браузер откроет Studio для одобрения.",
+ codex:
+ "Codex: выполните обе строки; вторая откроет Studio для одобрения. ChatGPT: добавьте в его настройках коннектор с этим URL.",
+ cursor:
+ "Сохраните как ~/.cursor/mcp.json или .cursor/mcp.json в проекте, затем войдите, когда Cursor попросит. Studio откроется для подтверждения.",
+ vscode:
+ "Сохраните это как .vscode/mcp.json в проекте, затем запустите сервер. VS Code попросит вас войти и откроет Studio.",
+ inspector:
+ "Запустите его, выберите Streamable HTTP, вставьте URL и откройте Auth Settings, затем Quick OAuth Flow. Удобно для проверки подключения.",
+ },
+ needsDcr:
+ "{client} регистрируется сам (динамическая регистрация клиентов), а она на этом сервере выключена. Попросите оператора сервера снова включить CLOUDHAL_MCP_DCR или используйте ключ агента.",
+ needsPublicUrl:
+ "Claude.ai и ChatGPT подключаются со своих серверов, которые не могут достучаться до {url}. Запустите Studio с --base-url, указывающим на публичный адрес https://, или используйте приложение на этом компьютере, например Claude Code или Codex.",
+ unavailable:
+ "Здесь не включён ни один способ, которым приложение может себя идентифицировать (документы метаданных и динамическая регистрация). Обратитесь к оператору этого сервера или используйте ключ агента.",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.tr.ts b/apps/studio/src/i18n/lazy/connectedAgents.tr.ts
new file mode 100644
index 00000000..3da516fb
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.tr.ts
@@ -0,0 +1,76 @@
+// Generated by `pnpm i18n:import` (ADR-0034): tr `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: 41a34e329c2ef121
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "Bağlı agent'lar",
+ description:
+ "URL ile bağladığın Claude, ChatGPT veya Cursor gibi uygulamalar. Her biri sen bağlantısını kesene kadar senin adına, rolünün ötesine geçmeden çalışır.",
+ allTitle: "Herkesin bağlı agent'ları",
+ allDescription:
+ "Tüm üyelerin bağlantıları. Birinin bağlantısını kesmek onu hemen sonlandırır; üyesi yeniden bağlayabilir.",
+ columns: {
+ agent: "Agent",
+ owner: "Üye",
+ access: "Erişim",
+ lastUsed: "Son kullanım",
+ connected: "Bağlanma",
+ },
+ identifiedBy: "{host} ile tanımlanıyor",
+ selfRegistered: "Kendini kaydetmiş, doğrulanmamış",
+ lookalike: "{client} gibi görünüyor, öyle olduğu doğrulanmadı",
+ never: "Hiç",
+ disconnect: "Bağlantıyı kes",
+ disconnectLabel: "{client} bağlantısını kes",
+ resource: "bağlantı",
+ disconnected: "Bağlantı kesildi",
+ disconnectedDescription:
+ "{client} artık bu Studio'da çalışamaz. Yeniden bağlanmak isterse yine sen karar verirsin.",
+ empty: "URL ile bağlı agent yok",
+ emptyDescription:
+ "Bu Studio'nun MCP URL'sini Claude, ChatGPT, Cursor veya VS Code'a ekle. Her bağlantıyı burada onaylarsın.",
+ allEmpty: "Henüz kimse URL ile agent bağlamadı",
+ setup: {
+ description:
+ "Bir uygulamaya bu Studio'nun MCP URL'sini ver ve istediğinde onayla ya da oturum açamayan betikler ve istemciler için bir agent anahtarı oluştur.",
+ tabs: {
+ key: "Agent anahtarıyla",
+ url: "URL ile bağlan",
+ },
+ url: "MCP URL'si",
+ urlDescription:
+ "Uygulama bu Studio'yu tarayıcında açar; giriş yapar ve neler yapabileceğini onaylarsın. Kopyalanacak anahtar yok.",
+ client: "Uygulama",
+ clients: {
+ claudeAi: "Claude.ai, masaüstü ve mobil",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT ve Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "Claude'da Settings'i, ardından Connectors'ı aç ve bu URL ile özel bir bağlayıcı (custom connector) ekle. Claude onay için Studio'yu açar.",
+ claudeCode:
+ "Bir terminalde çalıştır, sonra Claude Code içinde /mcp yaz ve Authenticate'i seç. Tarayıcın onay için Studio'yu açar.",
+ codex:
+ "Codex: iki satırı da çalıştır; ikincisi onay için Studio'yu açar. ChatGPT: ayarlarında bu URL ile bir bağlayıcı ekle.",
+ cursor:
+ "~/.cursor/mcp.json ya da bir projede .cursor/mcp.json olarak kaydet, sonra Cursor isteyince oturum aç. Onaylaman için Studio açılır.",
+ vscode:
+ "Projende .vscode/mcp.json olarak kaydet, sonra sunucuyu başlat. VS Code giriş yapmanı ister ve Studio'yu açar.",
+ inspector:
+ "Çalıştır, Streamable HTTP'yi seç, URL'yi yapıştır ve Auth Settings'i, ardından Quick OAuth Flow'u aç. Bir bağlantıyı denemek için iyidir.",
+ },
+ needsDcr:
+ "{client} kendini kaydeder (dinamik istemci kaydı) ve bu sunucuda bu özellik kapalı. Sunucunun yöneticisinden CLOUDHAL_MCP_DCR'ı yeniden açmasını iste ya da bir agent anahtarı kullan.",
+ needsPublicUrl:
+ "Claude.ai ve ChatGPT kendi sunucularından bağlanır; bu sunucular {url} adresine erişemez. Studio'yu --base-url ile herkese açık bir https:// adresine ayarlayarak başlat ya da bu bilgisayardaki Claude Code veya Codex gibi bir uygulama kullan.",
+ unavailable:
+ "Bir uygulamanın kendini tanıtabileceği iki yol da (meta veri belgeleri ve dinamik kayıt) burada kapalı. Bu sunucunun yöneticisine sor ya da bir agent anahtarı kullan.",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.uk.ts b/apps/studio/src/i18n/lazy/connectedAgents.uk.ts
new file mode 100644
index 00000000..74280bac
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.uk.ts
@@ -0,0 +1,76 @@
+// Generated by `pnpm i18n:import` (ADR-0034): uk `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: ce83ca13a5de2c10
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "Під'єднані агенти",
+ description:
+ "Застосунки, які ви під'єднали за URL, як-от Claude, ChatGPT чи Cursor. Кожен працює від вашого імені, ніколи не виходячи за межі вашої ролі, доки ви його не від'єднаєте.",
+ allTitle: "Під'єднані агенти всіх учасників",
+ allDescription:
+ "Під'єднання всіх учасників. Від'єднання одразу його завершує; учасник може під'єднати його знову.",
+ columns: {
+ agent: "Агент",
+ owner: "Учасник",
+ access: "Доступ",
+ lastUsed: "Останнє використання",
+ connected: "Під'єднано",
+ },
+ identifiedBy: "Ідентифікований через {host}",
+ selfRegistered: "Зареєструвався сам, не перевірений",
+ lookalike: "Схожий на {client}, але не підтверджений як він",
+ never: "Ніколи",
+ disconnect: "Від'єднати",
+ disconnectLabel: "Від'єднати {client}",
+ resource: "під'єднання",
+ disconnected: "Від'єднано",
+ disconnectedDescription:
+ "{client} більше не може працювати в цьому Studio. Якщо він знову попросить під'єднатися, ви вирішите заново.",
+ empty: "Немає агентів, під'єднаних за URL",
+ emptyDescription:
+ "Додайте MCP URL цього Studio до Claude, ChatGPT, Cursor або VS Code. Кожне під'єднання ви схвалюєте тут.",
+ allEmpty: "Ніхто ще не під'єднав агента за URL",
+ setup: {
+ description:
+ "Дайте застосунку MCP URL цього Studio і схваліть його, коли він попросить, або створіть ключ агента для скриптів і клієнтів, які не можуть увійти.",
+ tabs: {
+ key: "З ключем агента",
+ url: "Під'єднати за URL",
+ },
+ url: "MCP URL",
+ urlDescription:
+ "Застосунок відкриває цей Studio у вашому браузері, ви входите й схвалюєте, що йому можна робити. Копіювати ключ не потрібно.",
+ client: "Застосунок",
+ clients: {
+ claudeAi: "Claude.ai, комп'ютер і телефон",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT і Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "У Claude відкрийте Settings, потім Connectors і додайте власний конектор із цим URL. Claude відкриє Studio для схвалення.",
+ claudeCode:
+ "Виконайте це в терміналі, потім введіть /mcp у Claude Code й виберіть Authenticate. Браузер відкриє Studio для схвалення.",
+ codex:
+ "Codex: виконайте обидва рядки; другий відкриє Studio для схвалення. ChatGPT: додайте в його налаштуваннях конектор із цим URL.",
+ cursor:
+ "Збережіть як ~/.cursor/mcp.json або .cursor/mcp.json у проєкті, потім увійдіть, коли Cursor попросить. Studio відкриється для схвалення.",
+ vscode:
+ "Збережіть це як .vscode/mcp.json у проєкті, потім запустіть сервер. VS Code попросить вас увійти й відкриє Studio.",
+ inspector:
+ "Запустіть його, виберіть Streamable HTTP, вставте URL і відкрийте Auth Settings, потім Quick OAuth Flow. Зручно для перевірки під'єднання.",
+ },
+ needsDcr:
+ "{client} реєструється сам (динамічна реєстрація клієнтів), а її на цьому сервері вимкнено. Попросіть оператора сервера знову ввімкнути CLOUDHAL_MCP_DCR або використайте ключ агента.",
+ needsPublicUrl:
+ "Claude.ai і ChatGPT підключаються зі своїх серверів, які не можуть дістатися до {url}. Запустіть Studio з --base-url, що вказує на публічну адресу https://, або скористайтеся застосунком на цьому комп'ютері, як-от Claude Code чи Codex.",
+ unavailable:
+ "Тут не ввімкнено жодного способу, яким застосунок може себе ідентифікувати (документи метаданих і динамічна реєстрація). Зверніться до оператора цього сервера або використайте ключ агента.",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/lazy/connectedAgents.zh-Hans.ts b/apps/studio/src/i18n/lazy/connectedAgents.zh-Hans.ts
new file mode 100644
index 00000000..31da0019
--- /dev/null
+++ b/apps/studio/src/i18n/lazy/connectedAgents.zh-Hans.ts
@@ -0,0 +1,75 @@
+// Generated by `pnpm i18n:import` (ADR-0034): zh-Hans `connectedAgents.*` messages.
+// Do not edit: change the translation file and import it; only English is edited here.
+// hash: 54f7c54194ab51b3
+import type { NamespaceMessages } from "../namespaces.ts";
+
+export const messages: NamespaceMessages<"connectedAgents"> = {
+ connectedAgents: {
+ title: "已连接的智能体",
+ description:
+ "你通过 URL 连接的应用,例如 Claude、ChatGPT 或 Cursor。在你断开连接之前,每个应用都以你的身份工作,绝不超出你的角色。",
+ allTitle: "所有人的已连接智能体",
+ allDescription: "每位成员的连接。断开某个连接会立即结束它;该成员可以重新连接。",
+ columns: {
+ agent: "智能体",
+ owner: "成员",
+ access: "访问权限",
+ lastUsed: "上次使用",
+ connected: "连接时间",
+ },
+ identifiedBy: "由 {host} 标识",
+ selfRegistered: "自行注册,未经验证",
+ lookalike: "看起来像 {client},但未验证其身份",
+ never: "从未",
+ disconnect: "断开连接",
+ disconnectLabel: "断开 {client}",
+ resource: "连接",
+ disconnected: "已断开连接",
+ disconnectedDescription:
+ "{client} 已无法再在这个 Studio 中工作。如果它再次请求连接,将由你重新决定。",
+ empty: "没有通过 URL 连接的智能体",
+ emptyDescription:
+ "将这个 Studio 的 MCP URL 添加到 Claude、ChatGPT、Cursor 或 VS Code。每个连接都在这里批准。",
+ allEmpty: "还没有人通过 URL 连接智能体",
+ setup: {
+ description:
+ "把此 Studio 的 MCP URL 交给应用,并在它请求时批准;或为无法登录的脚本和客户端创建智能体密钥。",
+ tabs: {
+ key: "使用智能体密钥",
+ url: "通过 URL 连接",
+ },
+ url: "MCP URL",
+ urlDescription:
+ "应用会在你的浏览器中打开这个 Studio,你登录后批准它可以做什么。无需复制密钥。",
+ client: "应用",
+ clients: {
+ claudeAi: "Claude.ai、桌面端和移动端",
+ claudeCode: "Claude Code",
+ codex: "ChatGPT 和 Codex",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ inspector: "MCP Inspector",
+ },
+ hints: {
+ claudeAi:
+ "在 Claude 中打开 Settings,然后打开 Connectors,用这个 URL 添加自定义连接器。Claude 会打开 Studio 以进行批准。",
+ claudeCode:
+ "在终端中运行,然后在 Claude Code 中输入 /mcp 并选择 Authenticate。浏览器会打开 Studio 以进行批准。",
+ codex:
+ "Codex:运行这两行;第二行会打开 Studio 以进行批准。ChatGPT:在其设置中用这个 URL 添加连接器。",
+ cursor:
+ "将其保存为 ~/.cursor/mcp.json,或项目中的 .cursor/mcp.json,然后在 Cursor 要求时登录。Studio 会打开以供批准。",
+ vscode:
+ "在项目中保存为 .vscode/mcp.json,然后启动服务器。VS Code 会要求你登录并打开 Studio。",
+ inspector:
+ "运行它,选择 Streamable HTTP,粘贴 URL 并打开 Auth Settings,然后运行 Quick OAuth Flow。适合用来检查连接。",
+ },
+ needsDcr:
+ "{client} 会自行注册(动态客户端注册),但此服务器已关闭该功能。请让服务器运营者重新开启 CLOUDHAL_MCP_DCR,或使用智能体密钥。",
+ needsPublicUrl:
+ "Claude.ai 和 ChatGPT 从它们自己的服务器连接,无法访问 {url}。请使用 --base-url 将 Studio 设为公开的 https:// 地址后启动,或使用这台电脑上的应用,例如 Claude Code 或 Codex。",
+ unavailable:
+ "应用识别自身的两种方式(元数据文档和动态注册)在这里都未开启。请询问此服务器的运营者,或使用智能体密钥。",
+ },
+ },
+};
diff --git a/apps/studio/src/i18n/namespaces.ts b/apps/studio/src/i18n/namespaces.ts
index 8916d462..c2bff8ac 100644
--- a/apps/studio/src/i18n/namespaces.ts
+++ b/apps/studio/src/i18n/namespaces.ts
@@ -21,6 +21,7 @@ import type { messages as build } from "./lazy/build.en.ts";
import type { messages as collections } from "./lazy/collections.en.ts";
import type { messages as comments } from "./lazy/comments.en.ts";
import type { messages as commentsSettings } from "./lazy/commentsSettings.en.ts";
+import type { messages as connectedAgents } from "./lazy/connectedAgents.en.ts";
import type { messages as fields } from "./lazy/fields.en.ts";
import type { messages as listings } from "./lazy/listings.en.ts";
import type { messages as media } from "./lazy/media.en.ts";
@@ -44,6 +45,7 @@ export interface NamespaceCatalogs {
collections: DeepString<(typeof collections)["collections"]>;
comments: DeepString<(typeof comments)["comments"]>;
commentsSettings: DeepString<(typeof commentsSettings)["commentsSettings"]>;
+ connectedAgents: DeepString<(typeof connectedAgents)["connectedAgents"]>;
fields: DeepString<(typeof fields)["fields"]>;
listings: DeepString<(typeof listings)["listings"]>;
media: DeepString<(typeof media)["media"]>;
diff --git a/apps/studio/src/lib/api/agent-grants.ts b/apps/studio/src/lib/api/agent-grants.ts
index 972bb991..1419d919 100644
--- a/apps/studio/src/lib/api/agent-grants.ts
+++ b/apps/studio/src/lib/api/agent-grants.ts
@@ -5,6 +5,7 @@
*/
import type { AgentScope, KeyMode } from "./agents.ts";
import { json, listOf, request } from "./http.ts";
+import type { Role } from "./team.ts";
/** One requested scope and what the member's role turns it into (`null`: not in the role). */
export interface ConsentScope {
@@ -39,14 +40,23 @@ export interface AgentGrantSummary {
id: string;
agentId: string;
clientId: string;
+ /** Self-asserted, made safe to show by the server (plain text, at most 100 characters). */
clientName: string;
+ /** How the server knows the client, as the consent screen showed it. */
+ client: {
+ kind: "document" | "self-registered" | "server";
+ /** A metadata-document client's host: its identity. */
+ host?: string;
+ /** The well-known client the name looks like, when this is not that client. */
+ lookalike?: string;
+ };
scopes: AgentScope[];
effectiveScopes: AgentScope[];
mode: KeyMode;
effectiveMode: KeyMode;
createdAt: string;
lastUsedAt: string | null;
- owner?: { userId: string; name: string | null };
+ owner?: { userId: string; name: string | null; role: Role | null };
}
export const agentGrantsApi = {
@@ -60,6 +70,9 @@ export const agentGrantsApi = {
...json(input),
}),
list: () => request("/api/agent-grants").then(listOf),
+ /** Every member's connections, with their owner (`agents:manage`). */
+ listAll: () =>
+ request("/api/agent-grants?all=1").then(listOf),
revoke: (id: string) =>
request(`/api/agent-grants/${encodeURIComponent(id)}`, { method: "DELETE" }),
};
diff --git a/apps/studio/src/lib/api/team.ts b/apps/studio/src/lib/api/team.ts
index 619f79e7..f262b550 100644
--- a/apps/studio/src/lib/api/team.ts
+++ b/apps/studio/src/lib/api/team.ts
@@ -16,6 +16,11 @@ export interface InstanceInfo {
mcp: boolean;
/** RFC-0044: remote MCP clients connect by URL through OAuth. Older servers omit it. */
mcpOauth?: boolean;
+ /**
+ * With OAuth on: the `/mcp` resource clients are given and which registrations work here
+ * (RFC-0044 §3, §11: CIMD documents, dynamic client registration).
+ */
+ mcpOauthClients?: { resource: string; cimd: boolean; dcr: boolean };
activity: boolean;
previewLinks: boolean;
deployTransfer: boolean;
@@ -227,7 +232,7 @@ export type SignUpPolicy = InstanceInfo["signUp"];
/** The on/off features (`analytics` is an object of its own). */
export type InstanceFeature = Exclude<
keyof InstanceInfo["features"],
- "analytics" | "comments" | "schedules" | "ai" | "playground"
+ "analytics" | "comments" | "schedules" | "ai" | "playground" | "mcpOauthClients"
>;
export const teamApi = {
diff --git a/apps/studio/src/lib/oauth-clients.ts b/apps/studio/src/lib/oauth-clients.ts
new file mode 100644
index 00000000..7f62d5e2
--- /dev/null
+++ b/apps/studio/src/lib/oauth-clients.ts
@@ -0,0 +1,111 @@
+/**
+ * RFC-0044 §11: the MCP clients that connect to this Studio by URL (OAuth), what each one's setup
+ * looks like, and how each identifies itself (docs/guides/mcp-oauth.md). A client with a Client ID
+ * Metadata Document works when CIMD is on, and falls back to dynamic client registration (DCR)
+ * when that is on; Cursor and the MCP Inspector only register themselves (DCR).
+ */
+
+export const OAUTH_CLIENTS = [
+ "claudeAi",
+ "claudeCode",
+ "codex",
+ "cursor",
+ "vscode",
+ "inspector",
+] as const;
+export type OauthClient = (typeof OAUTH_CLIENTS)[number];
+
+/** How a client gets a `client_id`: a metadata document (with a DCR fallback), or DCR only. */
+const REGISTRATION: Record = {
+ claudeAi: "document",
+ claudeCode: "document",
+ codex: "document",
+ cursor: "dcr",
+ vscode: "document",
+ inspector: "dcr",
+};
+
+/**
+ * Clients that connect from their provider's servers, not from this computer: they cannot reach
+ * a loopback or private `/mcp` URL. (The "codex" entry covers ChatGPT, hosted, and the Codex CLI,
+ * local; the warning names ChatGPT.)
+ */
+const HOSTED: ReadonlySet = new Set(["claudeAi", "codex"]);
+
+/** The server's `features.mcpOauthClients`: the `/mcp` resource and the registration switches. */
+export interface OauthServerInfo {
+ readonly resource: string;
+ readonly cimd: boolean;
+ readonly dcr: boolean;
+}
+
+export type OauthClientStatus = "ok" | "needsDcr" | "needsPublicUrl" | "unavailable";
+
+const PRIVATE_V4 = [
+ [10, 0, 255],
+ [127, 0, 255],
+ [169, 254, 254],
+ [172, 16, 31],
+ [192, 168, 168],
+] as const;
+
+/**
+ * Whether a hosted client could reach `resource`: `https:` on a name or address that is not
+ * loopback, link-local, private or a local-only name (`localhost`, `*.localhost`, `*.local`,
+ * `*.internal`). Anything that does not parse counts as not public.
+ */
+export function isPublicMcpUrl(resource: string): boolean {
+ let url: URL;
+ try {
+ url = new URL(resource);
+ } catch {
+ return false;
+ }
+ if (url.protocol !== "https:") return false;
+ const host = url.hostname.toLowerCase();
+ // IPv6: loopback, unique-local (fc00::/7) and link-local (fe80::/10) are not public.
+ if (host.startsWith("[")) return host !== "[::1]" && !/^\[(fc|fd|fe8|fe9|fea|feb)/.test(host);
+ if (host === "localhost" || !host.includes(".")) return false;
+ if ([".localhost", ".local", ".internal"].some((end) => host.endsWith(end))) return false;
+ const parts = host.split(".").map(Number);
+ if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n))) return true;
+ const [a, b] = parts as [number, number, number, number];
+ return !PRIVATE_V4.some(([first, from, to]) => a === first && b >= from && b <= to);
+}
+
+/**
+ * Whether the client can connect to this server: `ok`; `unavailable` (no registration way is on);
+ * `needsPublicUrl` (a hosted client, and `/mcp` is local or plain http, so its servers cannot
+ * reach it); `needsDcr` (it only registers itself and DCR is off).
+ */
+export function oauthClientStatus(client: OauthClient, server: OauthServerInfo): OauthClientStatus {
+ if (!server.cimd && !server.dcr) return "unavailable";
+ if (HOSTED.has(client) && !isPublicMcpUrl(server.resource)) return "needsPublicUrl";
+ if (REGISTRATION[client] === "dcr" && !server.dcr) return "needsDcr";
+ return "ok";
+}
+
+/** The client the picker starts on: Claude.ai, or Claude Code where hosted clients cannot reach. */
+export function defaultOauthClient(resource: string): OauthClient {
+ return isPublicMcpUrl(resource) ? "claudeAi" : "claudeCode";
+}
+
+const json = (value: unknown) => JSON.stringify(value, null, 2);
+
+/** What the person pastes for each client; `undefined`: the client's own dialog takes the URL. */
+const SNIPPETS: Record string) | undefined> = {
+ claudeAi: undefined,
+ claudeCode: (url) => `claude mcp add --transport http cloudhal ${url}`,
+ codex: (url) => `codex mcp add cloudhal --url ${url}\ncodex mcp login cloudhal`,
+ cursor: (url) => json({ mcpServers: { cloudhal: { url } } }),
+ vscode: (url) => json({ servers: { cloudhal: { type: "http", url } } }),
+ inspector: () => "npx @modelcontextprotocol/inspector",
+};
+
+export function oauthSnippet(client: OauthClient, url: string): string | undefined {
+ return SNIPPETS[client]?.(url);
+}
+
+export function oauthSnippetLang(client: OauthClient): "json" | "bash" {
+ return client === "cursor" || client === "vscode" ? "json" : "bash";
+}
diff --git a/apps/studio/src/router.tsx b/apps/studio/src/router.tsx
index 83b3a135..6fa719f5 100644
--- a/apps/studio/src/router.tsx
+++ b/apps/studio/src/router.tsx
@@ -29,7 +29,12 @@ const AnalyticsPage = lazyRouteComponent(
);
const ActivityPage = lazyRouteComponent(() => import("./routes/ActivityPage.tsx"), "ActivityPage");
const AgentsPage = lazyRouteComponent(
- () => withMessages(import("./routes/AgentsPage.tsx"), ["analyticsSettings", "commentsSettings"]),
+ () =>
+ withMessages(import("./routes/AgentsPage.tsx"), [
+ "analyticsSettings",
+ "commentsSettings",
+ "connectedAgents",
+ ]),
"AgentsPage",
);
const ApprovalsPage = lazyRouteComponent(
diff --git a/apps/studio/src/routes/AgentsPage.tsx b/apps/studio/src/routes/AgentsPage.tsx
index 7075a3d9..7a740d76 100644
--- a/apps/studio/src/routes/AgentsPage.tsx
+++ b/apps/studio/src/routes/AgentsPage.tsx
@@ -4,14 +4,12 @@ import {
Badge,
Banner,
Button,
- ClipboardText,
DeleteResource,
DropdownMenu,
Empty,
LayerCard,
PageLayout,
Pagination,
- SectionCard,
SectionEmpty,
Table,
Text,
@@ -24,7 +22,6 @@ import {
DotsThree,
Info,
Key,
- PlugsConnected,
Plus,
Robot,
SlidersHorizontal,
@@ -38,6 +35,8 @@ import { HowModesWorkCard } from "../components/AgentModes.tsx";
import { AllowAnalytics } from "../components/AnalyticsKeyAccess.tsx";
import { ChangeAgentModeDialog } from "../components/ChangeAgentModeDialog.tsx";
import { AllowComments } from "../components/CommentKeyAccess.tsx";
+import { ConnectedAgents } from "../components/ConnectedAgents.tsx";
+import { ConnectMcpCard } from "../components/ConnectMcpCard.tsx";
import { CreateAgentKeyDialog } from "../components/CreateAgentKeyDialog.tsx";
import { Crumbs } from "../components/Crumbs.tsx";
import { deleteResourceProps } from "../components/deleteLabels.ts";
@@ -51,7 +50,6 @@ import {
} from "../components/Elevation.tsx";
import { ErrorBanner } from "../components/ErrorBanner.tsx";
import { ListToolbar } from "../components/ListToolbar.tsx";
-import { McpClientSetup } from "../components/McpClientSetup.tsx";
import { RelativeTime } from "../components/RelativeTime.tsx";
import { RoleBadge } from "../components/Roles.tsx";
import { ScopeBadge } from "../components/ScopeBadge.tsx";
@@ -66,7 +64,6 @@ import { listOf, useI18n, useT } from "../i18n/index.tsx";
import { initials } from "../lib/account.ts";
import { ownModeOf, sameScopes, sortScopes } from "../lib/agents.ts";
import { type AgentKeySummary, api, isNotEnabled, mcpEndpoint } from "../lib/api.ts";
-import { apiErrorText } from "../lib/api-errors.ts";
import { activeElevation, modesAbove } from "../lib/elevation.ts";
const matches = (k: AgentKeySummary, q: string, fold: Fold) =>
@@ -146,50 +143,6 @@ function KeyScopes({ agentKey }: { agentKey: AgentKeySummary }) {
);
}
-/** The "Connect an MCP client" card: endpoint, header and ready-to-paste client config. */
-export function ConnectMcpCard({ endpoint }: { endpoint: string }) {
- const t = useT();
- return (
- }
- description={t("agents.connect.description")}
- >
-
-
- )}
- {paging.total > paging.perPage && (
- setCreateOpen(true)}
+ onChangeMode={setToChange}
+ onElevate={setToElevate}
+ onEndElevation={(k) => endElevation.mutate([k])}
+ onRevoke={setToRevoke}
/>
)}
+
>
)}
diff --git a/apps/studio/test/connected-agents.test.tsx b/apps/studio/test/connected-agents.test.tsx
new file mode 100644
index 00000000..aebecb02
--- /dev/null
+++ b/apps/studio/test/connected-agents.test.tsx
@@ -0,0 +1,226 @@
+/**
+ * RFC-0044 §4, §11 (Phase 3b): "Connected agents" on `/agents` and the "Connect by URL" setup.
+ * A connection is named as the consent screen named it (a document client by its host first, a
+ * self-registered one as not verified, a look-alike warned about), what it can do reads in words,
+ * Disconnect revokes it, the admin view lists every member's; the setup tab shows the deployment's
+ * `/mcp` URL and says where a client needs dynamic registration, or a public URL (Claude.ai and
+ * ChatGPT connect from their servers). On phones a row's columns stack under the agent; jsdom
+ * applies no CSS, so both copies render here and each check reads its own column (`data-cell`).
+ */
+import { TooltipProvider } from "@cloudhal/ui";
+import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { ConnectedAgents } from "../src/components/ConnectedAgents.tsx";
+import { ConnectMcpCard } from "../src/components/ConnectMcpCard.tsx";
+import type { AgentGrantSummary } from "../src/lib/api.ts";
+import { isPublicMcpUrl, oauthClientStatus } from "../src/lib/oauth-clients.ts";
+import { asRole, INSTANCE, on, serve, testQueryClient, wrap } from "./roles-fixtures.tsx";
+
+const oauthAt = (resource: string) => ({
+ status: 200,
+ body: {
+ ...INSTANCE,
+ features: {
+ ...INSTANCE.features,
+ mcpOauth: true,
+ mcpOauthClients: { resource, cimd: true, dcr: false },
+ },
+ },
+});
+const OAUTH = oauthAt("https://studio.example.com/mcp");
+/** One column of a row (`GrantRow` renders a phone-only copy under the agent too). */
+const cell = (row: HTMLElement, name: string) =>
+ row.querySelector(`[data-cell="${name}"]`) as HTMLElement;
+
+const GRANT: AgentGrantSummary = {
+ id: "g1",
+ agentId: "agent_g1",
+ clientId: "https://claude.ai/oauth/claude-code-client-metadata",
+ clientName: "Claude Code",
+ client: { kind: "document", host: "claude.ai" },
+ scopes: ["site:read", "content:read", "content:write"],
+ effectiveScopes: ["site:read", "content:read", "content:write:own"],
+ mode: "edits",
+ effectiveMode: "edits",
+ createdAt: "2026-11-01T10:00:00.000Z",
+ lastUsedAt: null,
+};
+const FAKE: AgentGrantSummary = {
+ ...GRANT,
+ id: "g2",
+ agentId: "agent_g2",
+ clientId: "dcr_client_1",
+ clientName: "Claude",
+ client: { kind: "self-registered", lookalike: "Claude" },
+ effectiveScopes: ["site:read", "site:publish"],
+ owner: { userId: "u-editor", name: "Eda Editor", role: "editor" },
+};
+
+afterEach(() => {
+ cleanup();
+ vi.unstubAllGlobals();
+});
+
+const show = (node: React.ReactNode) => render(wrap({node}));
+
+describe("Connected agents", () => {
+ it("names each connection as the consent screen did and disconnects with the revoke", async () => {
+ const fetch = serve(
+ on("GET", "/api/instance", OAUTH),
+ ...asRole("author"),
+ on("GET", "/api/agent-grants", { status: 200, body: [GRANT] }),
+ on("DELETE", "/api/agent-grants/g1", { status: 204 }),
+ );
+ show();
+ const row = (await screen.findByText("Identified by claude.ai")).closest("tr") as HTMLElement;
+ expect(row).toHaveAttribute("data-client-kind", "document");
+ expect(within(row).getByText("Claude Code")).toBeInTheDocument();
+ // What it can do, in words: one pill per group (its scopes in the tooltip), then its mode.
+ const access = cell(row, "access");
+ expect(within(access).getByText("Read")).toBeInTheDocument();
+ expect(within(access).getByText("Write")).toBeInTheDocument();
+ expect(within(access).queryByText("Publish and roll back")).toBeNull();
+ expect(within(access).getByText("Edits")).toBeInTheDocument();
+ expect(within(cell(row, "lastUsed")).getByText("Never")).toBeInTheDocument();
+ // The phone copy stacks the same under the agent: access, then last use.
+ const narrow = row.querySelector("[data-narrow]") as HTMLElement;
+ expect(within(narrow).getByText("Write")).toBeInTheDocument();
+ expect(within(narrow).getByText("Last used")).toBeInTheDocument();
+ fireEvent.click(within(row).getByRole("button", { name: "Disconnect Claude Code" }));
+ const dialog = await screen.findByRole("dialog");
+ fireEvent.change(within(dialog).getByRole("textbox"), { target: { value: "Claude Code" } });
+ fireEvent.click(within(dialog).getByRole("button", { name: "Disconnect" }));
+ await waitFor(() =>
+ expect(
+ fetch.mock.calls.some(
+ ([u, i]) =>
+ String(u).endsWith("/api/agent-grants/g1") &&
+ (i as RequestInit | undefined)?.method === "DELETE",
+ ),
+ ).toBe(true),
+ );
+ });
+
+ it("lists every member's connections for an Administrator, a self-registered one as unverified", async () => {
+ serve(
+ on("GET", "/api/instance", OAUTH),
+ ...asRole("administrator"),
+ on("GET", "/api/agent-grants?all=1", { status: 200, body: [FAKE] }),
+ );
+ show();
+ const row = (await screen.findByText("Self-registered, not verified")).closest(
+ "tr",
+ ) as HTMLElement;
+ expect(within(row).getByText("Claude")).toBeInTheDocument();
+ expect(within(row).getByText("Looks like Claude, not verified as it")).toBeInTheDocument();
+ expect(within(cell(row, "owner")).getByText("Eda Editor")).toBeInTheDocument();
+ expect(within(cell(row, "access")).getByText("Publish and roll back")).toBeInTheDocument();
+ // On phones the member stacks under the agent too.
+ const narrow = row.querySelector("[data-narrow]") as HTMLElement;
+ expect(within(narrow).getByText("Eda Editor")).toBeInTheDocument();
+ expect(screen.getByText("Everyone's connected agents")).toBeInTheDocument();
+ });
+
+ it("stays out of the way where the server runs no OAuth and nothing is connected", async () => {
+ serve(
+ on("GET", "/api/instance", { status: 200, body: INSTANCE }),
+ ...asRole("owner"),
+ on("GET", "/api/agent-grants", { status: 200, body: [] }),
+ );
+ const qc = testQueryClient();
+ const { container } = render(
+ wrap(
+
+
+ ,
+ "en",
+ qc,
+ ),
+ );
+ // Both answers in (OAuth off, nothing connected) before checking the card is absent.
+ await waitFor(() => {
+ expect(qc.getQueryState(["instance"])?.status).toBe("success");
+ expect(qc.getQueryState(["agent-grants", "mine"])?.status).toBe("success");
+ });
+ expect(container.querySelector("#connected-agents")).toBeNull();
+ expect(screen.queryByText("Connected agents")).toBeNull();
+ });
+});
+
+describe("Connect by URL", () => {
+ it("shows the /mcp URL and each client's steps, and says when a client needs DCR", async () => {
+ serve(on("GET", "/api/instance", OAUTH), ...asRole("owner"));
+ show();
+ const setup = await screen.findByTestId("mcp-oauth-setup");
+ expect(within(setup).getByText("https://studio.example.com/mcp")).toBeInTheDocument();
+ expect(within(setup).getByText(/add a custom connector with this URL/)).toBeInTheDocument();
+ // Cursor only registers itself; this server has DCR off.
+ fireEvent.click(within(setup).getByRole("combobox"));
+ const cursor = await screen.findByRole("option", { name: "Cursor" });
+ fireEvent.pointerDown(cursor, { pointerType: "mouse" });
+ fireEvent.click(cursor);
+ expect(
+ await within(setup).findByText(/registers itself \(dynamic client registration\)/),
+ ).toBeInTheDocument();
+ // The key tab stays.
+ fireEvent.click(screen.getByRole("tab", { name: "With an agent key" }));
+ expect(await screen.findByTestId("mcp-client-setup")).toBeInTheDocument();
+ });
+
+ it("starts on Claude Code and warns about hosted clients where /mcp is local", async () => {
+ serve(on("GET", "/api/instance", oauthAt("http://localhost:8787/mcp")), ...asRole("owner"));
+ show();
+ const setup = await screen.findByTestId("mcp-oauth-setup");
+ expect(setup).toHaveAttribute("data-client", "claudeCode");
+ expect(setup).toHaveAttribute("data-status", "ok");
+ // Claude.ai connects from its own servers: it cannot reach localhost.
+ fireEvent.click(within(setup).getByRole("combobox"));
+ const claudeAi = await screen.findByRole("option", { name: "Claude.ai, Desktop and mobile" });
+ fireEvent.pointerDown(claudeAi, { pointerType: "mouse" });
+ fireEvent.click(claudeAi);
+ expect(
+ await within(setup).findByText(/connect from their own servers, which cannot reach/),
+ ).toBeInTheDocument();
+ expect(within(setup).getByText(/--base-url/)).toBeInTheDocument();
+ expect(setup).toHaveAttribute("data-status", "needsPublicUrl");
+ });
+
+ it("tells hosted clients from local ones by the /mcp URL", () => {
+ const both = { cimd: true, dcr: true };
+ for (const url of [
+ "http://localhost:8787/mcp",
+ "https://localhost/mcp",
+ "https://studio.localhost/mcp",
+ "https://127.0.0.1/mcp",
+ "https://192.168.1.20/mcp",
+ "https://10.0.0.5/mcp",
+ "https://172.20.0.2/mcp",
+ "https://[::1]/mcp",
+ "https://[fd00::1]/mcp",
+ "https://nas.local/mcp",
+ "https://intranet/mcp",
+ "http://studio.example.com/mcp",
+ "not a url",
+ ]) {
+ expect(isPublicMcpUrl(url), url).toBe(false);
+ expect(oauthClientStatus("claudeAi", { resource: url, ...both }), url).toBe("needsPublicUrl");
+ expect(oauthClientStatus("codex", { resource: url, ...both }), url).toBe("needsPublicUrl");
+ expect(oauthClientStatus("claudeCode", { resource: url, ...both }), url).toBe("ok");
+ }
+ for (const url of [
+ "https://studio.example.com/mcp",
+ "https://172.32.0.1/mcp",
+ "https://[2001:db8::1]/mcp",
+ ]) {
+ expect(isPublicMcpUrl(url), url).toBe(true);
+ expect(oauthClientStatus("claudeAi", { resource: url, ...both }), url).toBe("ok");
+ }
+ });
+
+ it("shows only the key setup where OAuth is off", async () => {
+ serve(...asRole("owner"));
+ show();
+ expect(await screen.findByTestId("mcp-client-setup")).toBeInTheDocument();
+ expect(screen.queryByRole("tab", { name: "Connect by URL" })).toBeNull();
+ });
+});
diff --git a/apps/studio/test/design-rules.test.ts b/apps/studio/test/design-rules.test.ts
index e5fdad7a..67c1a49f 100644
--- a/apps/studio/test/design-rules.test.ts
+++ b/apps/studio/test/design-rules.test.ts
@@ -156,12 +156,28 @@ const REQUIRED_IMPORTS: Record = {
"Empty",
"Pagination",
"DeleteResource",
- "ClipboardText",
- "McpClientSetup",
+ "ConnectMcpCard",
+ "ConnectedAgents",
"CreateAgentKeyDialog",
],
// Per-client MCP setup (docs/agents/clients.md), on the page and in the one-time key reveal.
"components/McpClientSetup.tsx": ["Select", "CodeHighlighted"],
+ // RFC-0044 §4, §11: connecting by URL next to the key setup, and the connections it made.
+ "components/ConnectMcpCard.tsx": [
+ "SectionCard",
+ "Tabs",
+ "ClipboardText",
+ "McpClientSetup",
+ "McpOauthSetup",
+ ],
+ "components/McpOauthSetup.tsx": ["ClipboardText", "Select", "CodeHighlighted", "Banner"],
+ "components/ConnectedAgents.tsx": [
+ "SectionCard",
+ "Table",
+ "SectionEmpty",
+ "DeleteResource",
+ "ModeBadge",
+ ],
// Navigation menus (§2t, RFC-0003 §8): the MenuTree editor with its preview, picker and delta.
"routes/NavigationPage.tsx": [
"PageLayout",
@@ -244,7 +260,8 @@ const REQUIRED_IMPORTS: Record = {
"Table",
],
// Side panels are SectionCards; summaries inside dialogs are InsetPanels (DESIGN.md §1).
- "routes/AgentsPage.tsx#layout": ["Crumbs", "SectionCard", "SectionEmpty"],
+ // Its SectionCards live in ConnectMcpCard and ConnectedAgents (required there).
+ "routes/AgentsPage.tsx#layout": ["Crumbs", "SectionEmpty"],
"routes/SkillsPage.tsx#layout": ["Crumbs", "SectionCard", "SectionEmpty"],
"components/CreateAgentKeyDialog.tsx#layout": ["InsetPanel"],
"components/ChangeAgentModeDialog.tsx#layout": ["InsetPanel"],
diff --git a/docs/agents/clients.md b/docs/agents/clients.md
index 5bc13d3d..accd5fb1 100644
--- a/docs/agents/clients.md
+++ b/docs/agents/clients.md
@@ -3,7 +3,8 @@
Every MCP client connects to Cloudhal in one of two ways ([which one?](/docs/agents#two-ways-to-connect)):
- **Local server:** the client starts `cloudhal mcp` in your project folder. Nothing to authenticate; the agent works on the project's local store.
-- **Remote server:** the client calls `https:///mcp` on a deployment, sending an agent key created in Studio under **Agents** as `Authorization: Bearer ` or in the `x-api-key` header. Both work; use whichever your client supports. Bearer needs a deployment on Cloudhal 0.3.4 or later; on older ones use `x-api-key`, or upgrade and run `cloudhal deploy`. Studio shows each client's setup with the key filled in right after you create it.
+- **Remote server by URL (OAuth):** you give the client only `https:///mcp`. It opens Studio in your browser, you sign in and approve what it may do, and it gets its own access, never beyond your role. Claude (claude.ai, Desktop, mobile), Claude Code, ChatGPT and Codex, Cursor, VS Code and the MCP Inspector all work this way: see [Connect by URL](#connect-by-url-oauth). Studio lists these connections under **Agents › Connected agents**, where you disconnect them.
+- **Remote server with a key:** the client calls `https:///mcp` on a deployment, sending an agent key created in Studio under **Agents** as `Authorization: Bearer ` or in the `x-api-key` header. Both work; use whichever your client supports. Bearer needs a deployment on Cloudhal 0.3.4 or later; on older ones use `x-api-key`, or upgrade and run `cloudhal deploy`. Studio shows each client's setup with the key filled in right after you create it.
`cloudhal mcp` serves the project folder it starts in. Configure local servers **per project** (the project-scoped files below), or start the client from the project folder.
@@ -28,6 +29,23 @@ For the remote examples, put the key in an environment variable instead of the f
export CLOUDHAL_API_KEY=""
```
+## Connect by URL (OAuth)
+
+Every remote client below can connect without a key. Give it the MCP URL, `https:///mcp` (Studio shows it under **Agents › Connect an MCP client › Connect by URL**). The client opens Studio in your browser; sign in, check the client and what it asks for, choose its mode, and approve. Its access is narrowed to your role, and it ends when you disconnect it under **Agents › Connected agents** or when you leave the Studio. How it works, and what an operator can switch: [the OAuth guide](/docs/guides/mcp-oauth).
+
+| Client | Where the URL goes | How it identifies itself |
+|---|---|---|
+| Claude (claude.ai, Desktop, mobile, Cowork) | **Settings › Connectors › Add custom connector** | metadata document, else self-registration |
+| Claude Code | `claude mcp add --transport http cloudhal https://studio.example.com/mcp`, then `/mcp` › **Authenticate** | metadata document |
+| ChatGPT and Codex | ChatGPT: a connector with the URL. Codex: `codex mcp add cloudhal --url https://studio.example.com/mcp`, then `codex mcp login cloudhal` | metadata document |
+| Cursor | `{ "mcpServers": { "cloudhal": { "url": "https://studio.example.com/mcp" } } }` in `mcp.json`, then sign in when Cursor asks | **self-registration only** (needs DCR on) |
+| VS Code | `{ "servers": { "cloudhal": { "type": "http", "url": "https://studio.example.com/mcp" } } }` in `.vscode/mcp.json` | metadata document |
+| MCP Inspector | `npx @modelcontextprotocol/inspector`, Streamable HTTP, the URL, **Auth Settings › Quick OAuth Flow** | self-registration |
+
+The consent screen names the client the way the server knows it: a client with a metadata document by the host the document is served from (`claude.ai`); a self-registered one as **Self-registered, not verified**. If a self-registered name looks like a well-known client's, Studio warns you. Approve only connections you started yourself.
+
+Self-hosted (`cloudhal studio`): connecting by URL needs `--base-url` (`CLOUDHAL_BASE_URL`) with an `https://` origin, or `http://localhost` for clients on the same machine. Without it, Studio logs why OAuth is off, and keys still work.
+
## Claude Code
Local, from the project folder. `--scope project` writes `.mcp.json`, which you can commit so everyone on the project gets the server:
@@ -152,7 +170,7 @@ Remote. VS Code asks for the key once and stores it securely:
## Claude Desktop and claude.ai
-Use the remote server: **Settings › Connectors › Add custom connector**, the URL `https://studio.example.com/mcp`, and a request header `Authorization` with the value `Bearer `. Claude connects from Anthropic's cloud, so this needs a deployment; `localhost` is not reachable. For a local project, use Claude Code.
+Use the remote server: **Settings › Connectors › Add custom connector** and the URL `https://studio.example.com/mcp`. Claude opens Studio to approve the connection ([by URL](#connect-by-url-oauth)). On a deployment without OAuth, add a request header `Authorization` with the value `Bearer ` instead. Claude connects from Anthropic's cloud, so this needs a deployment; `localhost` is not reachable. For a local project, use Claude Code.
## Windsurf
@@ -215,6 +233,10 @@ A client that cannot send headers can reach the remote server through [`mcp-remo
- **The client lists no tools.** Run the command yourself in the project folder: `npx cloudhal mcp` should start and keep running (it waits for a client on stdin). An error there (Node older than 24, not a Cloudhal project) is what the client sees.
- **The agent edits a different site.** The local server serves the folder it started in. Use a project-scoped config, or start the client from the project.
+- **Connecting by URL fails at "authorization server metadata" or "OAuth is not supported".** The deployment runs no OAuth: on `cloudhal studio`, set `--base-url`; an operator may also have set `CLOUDHAL_MCP_OAUTH=off`. Use a key.
+- **Cursor says registration failed.** Cursor registers itself, and the operator has turned dynamic client registration off (`CLOUDHAL_MCP_DCR=off`). Use a key, or ask the operator.
+- **"invalid redirect uri" after approving.** The client sends you back to an address the operator's host list (`CLOUDHAL_MCP_CLIENT_HOSTS`) does not include, or one that is not https or this computer.
+- **A connected client stops working.** It was disconnected in Studio, its member left, its 90-day limit passed, or the operator rotated `BETTER_AUTH_SECRET`. Connect it again.
- **`401 unauthorized` remotely.** The key is missing, revoked, or sent in another header. Cloudhal reads `Authorization: Bearer ` and `x-api-key`; on `/mcp` a bearer must be an agent key, not the deployment token.
- **`403 not_a_member`.** The key's owner no longer belongs to the deployment; create a new key.
- **A call says it is waiting for approval.** Accept it in your client, or in Studio under **Approvals** on a deployment.
diff --git a/docs/guides/mcp-oauth.md b/docs/guides/mcp-oauth.md
new file mode 100644
index 00000000..48651034
--- /dev/null
+++ b/docs/guides/mcp-oauth.md
@@ -0,0 +1,44 @@
+# Remote MCP clients by URL: a guide for members and operators
+
+Claude, ChatGPT, Cursor, VS Code and other MCP clients can connect to a Cloudhal deployment with nothing but its URL, `https:///mcp`. The client asks the deployment to authorize it (OAuth 2.1), Studio asks you, and the client gets access of its own: never more than your role allows, recorded in the activity log like any agent, and ended the moment you disconnect it. Agent keys keep working next to it. The design is [RFC-0044](../rfcs/0044-mcp-oauth-authorization.md) and [ADR-0045](../adr/0045-mcp-authorization-server.md). The setup for each client is in [Connect an agent](../agents/clients.md#connect-by-url-oauth).
+
+## Connecting a client
+
+1. In the client, add a remote MCP server with the URL. Studio shows it under **Agents › Connect an MCP client › Connect by URL**, with the steps for each client.
+2. The client opens Studio in your browser. Sign in if you are not.
+3. The consent screen shows:
+ - **Who is asking.** A client with a metadata document is named by the host that serves it, first in the title ("claude.ai: connect Claude Code"). A client that registered itself is labelled **Self-registered, not verified**. Its name is shown as plain text and at most 100 characters long. A name that looks like a well-known client's (Claude, ChatGPT, Codex, VS Code), when the client is not that client's document on its real host, gets a warning.
+ - **Where it sends you back:** a web host, or an app on this computer. When every address is on this computer, a warning says so: approve only if you started the connection here.
+ - **What it may do:** the permissions it asks for, each as your role turns it. What your role does not give is shown but left out, and you can untick the rest.
+ - **How it works:** the agent mode (Plan, Manual, Edits or Auto, capped by your role). Auto asks for an explicit confirmation.
+4. Approve. The browser goes back to the client, which can now call the MCP tools as you.
+
+Asking again later for the same or fewer permissions skips the screen. Asking for more (a tool needs a permission the connection lacks) shows it again, with today's permissions ticked and the new ones marked.
+
+## Connected agents
+
+**Agents › Connected agents** lists your connections: the client as the consent screen named it, what it can do (Read, Write, Publish and roll back; the details are in the tooltip), its mode, when it was last used, and when you connected it. **Disconnect** ends a connection at once: its access tokens stop working on their next call, its refresh tokens and stored consent are deleted, and if it asks again you decide again.
+
+Administrators (and anyone with `agents:manage`) see **Everyone's connected agents** under the **All keys** tab, with each member, and can disconnect any of them. Removing a member from the Studio ends their connections in the same step. Changing a member's role narrows their connections on the next call, as it narrows their keys.
+
+A connection lasts at most 90 days. After that the client asks you to approve it again.
+
+## For operators
+
+- **Where it runs.** On Cloudflare, the admin Worker is the authorization server at its own origin. On your own server, `cloudhal studio` needs `--base-url` (`CLOUDHAL_BASE_URL`) with an `https://` origin, or `http://localhost` for clients on the same machine. Without it, OAuth stays off, Studio logs why, and keys still work. Behind a reverse proxy, also set `CLOUDHAL_BEHIND_PROXY=1` ([self-hosting](self-host.md)).
+- **How clients identify themselves.** Most (Claude, Claude Code, Codex, VS Code) name a Client ID Metadata Document, an HTTPS URL the server fetches. Cursor and the MCP Inspector register themselves (dynamic client registration). Both are on by default.
+- **Switches:** `CLOUDHAL_MCP_OAUTH`, `CLOUDHAL_MCP_CIMD`, `CLOUDHAL_MCP_DCR`, `CLOUDHAL_MCP_CLIENT_HOSTS` and `CLOUDHAL_MCP_REDIRECT_SCHEMES`. Their limits, the fetch and registration budgets, and what turning each off does to existing connections are in [hardening](cloudflare-hardening.md#remote-mcp-clients-oauth).
+- **Rotating `BETTER_AUTH_SECRET`** also invalidates the key that signs access tokens. Delete the `jwks` rows afterwards, as the [hardening guide](cloudflare-hardening.md#rotating-better_auth_secret) explains.
+- **Discovery.** `/mcp` answers an unauthenticated request with `401` and a `WWW-Authenticate` challenge. That points to `/.well-known/oauth-protected-resource/mcp`, which names the authorization server `https:///api/auth`, whose metadata is at `/.well-known/oauth-authorization-server/api/auth`. A proxy must pass `/.well-known/*` and `/api/auth/*` through unchanged.
+
+## Troubleshooting
+
+| What you see | Why, and what to do |
+|---|---|
+| The client says the server does not support OAuth, or fails at "authorization server metadata" | OAuth is off on this deployment (no `--base-url` on `cloudhal studio`, or `CLOUDHAL_MCP_OAUTH=off`). Use an agent key. |
+| Cursor or the MCP Inspector cannot register | Dynamic client registration is off (`CLOUDHAL_MCP_DCR=off`). Studio's Connect-by-URL tab says so per client. |
+| "invalid redirect uri" | The client's return address is not https, not this computer, not a known app scheme, or not on the operator's host list. |
+| "rate_limited" (429) when registering | Dynamic registration is limited to 10 registrations per address per hour and 300 per server per day. Wait, or use a client with a metadata document. |
+| "temporarily_unavailable" (429) | The server is limiting metadata-document fetches from your address (20 a minute). Wait a minute and try again. |
+| "temporarily_unavailable" (503) | The server holds as many clients as it allows (5,000 metadata-document clients, 5,000 self-registered ones). Ask its operator; the limits are in [hardening](cloudflare-hardening.md#what-a-client-can-and-cannot-do). |
+| A connected client stops working | It was disconnected, its member left, it reached 90 days, or the operator rotated the secret. Connect it again. |
diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts
index d5ee5e81..812f5699 100644
--- a/packages/api/src/app.ts
+++ b/packages/api/src/app.ts
@@ -204,7 +204,7 @@ export function createApi(deps: ApiDeps) {
features: {
agentKeys: Boolean(deps.agentKeys),
mcp: Boolean(deps.mcp),
- mcpOauth: Boolean(deps.oauthServer && deps.mcp?.oauth),
+ ...oauthFeatures(deps),
activity: Boolean(deps.audit ?? deps.mcp?.audit),
previewLinks: Boolean(deps.preview),
deployTransfer: Boolean(deps.admin) && problems.length === 0,
@@ -265,6 +265,21 @@ export function createApi(deps: ApiDeps) {
return app;
}
+/**
+ * RFC-0044: whether remote MCP clients connect by URL (OAuth), and how: the `/mcp` resource they
+ * are given and which registrations work here (Studio's "Connect by URL" notes, §11).
+ */
+function oauthFeatures(deps: ApiDeps) {
+ const server = deps.oauthServer;
+ const on = Boolean(server && deps.mcp?.oauth);
+ return {
+ mcpOauth: on,
+ ...(on && server
+ ? { mcpOauthClients: { resource: server.resource, ...server.registration } }
+ : {}),
+ };
+}
+
/** Better Auth paths HTTP callers never reach (raw and decoded forms are both checked). */
function fencedAuthPath(path: string, deps: ApiDeps): boolean {
const open = { register: Boolean(deps.oauthServer?.register) };
diff --git a/packages/api/src/auth.ts b/packages/api/src/auth.ts
index 4ceca3e6..3bced5ce 100644
--- a/packages/api/src/auth.ts
+++ b/packages/api/src/auth.ts
@@ -87,6 +87,10 @@ export function createAuth(options: AuthOptions): CloudhalAuth {
oauth: {
issuer: d.endpoints.issuer,
resource: options.mcpOauth.resource,
+ registration: {
+ cimd: Boolean(options.mcpOauth.registration?.cimd),
+ dcr: options.mcpOauth.registration?.dcr === true,
+ },
grants: createAgentGrantService(d),
// RFC 8414: the server-only `getOAuthServerConfig` endpoint; `createApi` serves it.
// No DPoP in v1 (ADR-0010): its algorithms are not advertised (the token endpoint
diff --git a/packages/api/src/auth/agent-grants.ts b/packages/api/src/auth/agent-grants.ts
index 295468ed..a5a06bbb 100644
--- a/packages/api/src/auth/agent-grants.ts
+++ b/packages/api/src/auth/agent-grants.ts
@@ -14,6 +14,13 @@ import {
type Scope,
} from "@cloudhal/agent-runtime";
import { consentInfo, giveConsent } from "./agent-consent.ts";
+import {
+ type ClientKind,
+ clientKind,
+ displayName,
+ documentHost,
+ lookalikeOf,
+} from "./client-identity.ts";
import type { AuthDeps } from "./deps.ts";
import { type AgentGrant, agentIdForGrant, providerRevocation } from "./grants.ts";
import { actorIdForUser, roleScopesHold } from "./key-rows.ts";
@@ -26,7 +33,17 @@ export interface AgentGrantSummary {
readonly id: string;
readonly agentId: string;
readonly clientId: string;
+ /** Self-asserted, made safe to show (`displayName`: plain, at most 100). */
readonly clientName: string;
+ /**
+ * How the server knows the client, as the consent screen showed it (`client-identity.ts`): a
+ * document client's host, a self-registered one's look-alike warning.
+ */
+ readonly client: {
+ readonly kind: ClientKind;
+ readonly host?: string | undefined;
+ readonly lookalike?: string | undefined;
+ };
readonly scopes: readonly Scope[];
/** What a call can use now: the grant narrowed to the owner's current role. */
readonly effectiveScopes: readonly Scope[];
@@ -35,28 +52,93 @@ export interface AgentGrantSummary {
readonly createdAt: string;
readonly lastUsedAt: string | null;
/** With `all`: whose connection it is. */
- readonly owner?: { readonly userId: string; readonly name: string | null } | undefined;
+ readonly owner?:
+ | { readonly userId: string; readonly name: string | null; readonly role: Role | null }
+ | undefined;
}
-export function createAgentGrantService(d: AuthDeps) {
- const summary = (grant: AgentGrant, role: Role | undefined): AgentGrantSummary => ({
- id: grant.id,
- agentId: agentIdForGrant(grant.id),
+/** The provider's client rows of the listed grants, by `clientId` (chunks within D1's limits). */
+async function clientRows(d: AuthDeps, ids: readonly string[]): Promise