diff --git a/app/(app)/settings/tokens/connection-guides.ts b/app/(app)/settings/tokens/connection-guides.ts
new file mode 100644
index 0000000..9a9f2a6
--- /dev/null
+++ b/app/(app)/settings/tokens/connection-guides.ts
@@ -0,0 +1,75 @@
+export const SERVICE_TOKEN_ENV_VAR = "ARIN_MCP_TOKEN";
+export const SERVICE_TOKEN_PLACEHOLDER = "arin_your_token_here";
+
+function bearerValue(token?: string): string {
+ return `Bearer ${token?.trim() || SERVICE_TOKEN_PLACEHOLDER}`;
+}
+
+export function mcpServerUrl(appUrl: string): string {
+ return `${appUrl}/api/mcp`;
+}
+
+export function claudeWebFields(appUrl: string) {
+ return {
+ name: "Arin",
+ remoteMcpServerUrl: mcpServerUrl(appUrl),
+ oauthClientId: "Leave blank",
+ oauthClientSecret: "Leave blank",
+ };
+}
+
+export function claudeWebBlock(appUrl: string): string {
+ const fields = claudeWebFields(appUrl);
+ return [
+ `Name: ${fields.name}`,
+ `Remote MCP server URL: ${fields.remoteMcpServerUrl}`,
+ `OAuth Client ID: ${fields.oauthClientId}`,
+ `OAuth Client Secret: ${fields.oauthClientSecret}`,
+ ].join("\n");
+}
+
+export function claudeCodeMacCommand(appUrl: string, token?: string): string {
+ return [
+ "claude mcp add --transport http arin \\",
+ ` ${mcpServerUrl(appUrl)} \\`,
+ ` --header "Authorization: ${bearerValue(token)}"`,
+ ].join("\n");
+}
+
+export function claudeCodeWindowsCommand(appUrl: string, token?: string): string {
+ return [
+ "claude mcp add --transport http arin `",
+ ` ${mcpServerUrl(appUrl)} \``,
+ ` --header 'Authorization: ${bearerValue(token)}'`,
+ ].join("\n");
+}
+
+export function codexMacCommand(appUrl: string, token?: string): string {
+ return [
+ `export ${SERVICE_TOKEN_ENV_VAR}="${token?.trim() || SERVICE_TOKEN_PLACEHOLDER}"`,
+ `codex mcp add arin --url ${mcpServerUrl(appUrl)} --bearer-token-env-var ${SERVICE_TOKEN_ENV_VAR}`,
+ "codex mcp list",
+ ].join("\n");
+}
+
+export function codexWindowsCommand(appUrl: string, token?: string): string {
+ return [
+ `$env:${SERVICE_TOKEN_ENV_VAR}="${token?.trim() || SERVICE_TOKEN_PLACEHOLDER}"`,
+ `codex mcp add arin --url ${mcpServerUrl(appUrl)} --bearer-token-env-var ${SERVICE_TOKEN_ENV_VAR}`,
+ "codex mcp list",
+ ].join("\n");
+}
+
+export function genericHttpConfig(appUrl: string, token?: string): string {
+ return `{
+ "mcpServers": {
+ "arin": {
+ "type": "http",
+ "url": "${mcpServerUrl(appUrl)}",
+ "headers": {
+ "Authorization": "${bearerValue(token)}"
+ }
+ }
+ }
+}`;
+}
diff --git a/app/(app)/settings/tokens/page.tsx b/app/(app)/settings/tokens/page.tsx
index a81d3b3..a60fabe 100644
--- a/app/(app)/settings/tokens/page.tsx
+++ b/app/(app)/settings/tokens/page.tsx
@@ -1,6 +1,7 @@
import { desc, eq } from "drizzle-orm";
import { db } from "@/db/client";
import { serviceTokens } from "@/db/schema/settings";
+import { env } from "@/lib/env";
import { relativeTime } from "@/lib/format";
import { requireOrgSession } from "@/lib/session";
import { TokensClient } from "./tokens-client";
@@ -20,7 +21,7 @@ export default async function ServiceTokensSettingsPage() {
.orderBy(desc(serviceTokens.createdAt));
return (
-
+
- Service tokens authenticate Claude (over MCP) to your workspace. Each token is
- shown once at creation — store it in your MCP client config.
+ Connect Claude, Codex, and other MCP clients to your workspace. Claude Web
+ uses OAuth; service-token clients show the token only once at creation.
({
id: r.id,
name: r.name,
diff --git a/app/(app)/settings/tokens/tokens-client.tsx b/app/(app)/settings/tokens/tokens-client.tsx
index bad09f1..abfedcf 100644
--- a/app/(app)/settings/tokens/tokens-client.tsx
+++ b/app/(app)/settings/tokens/tokens-client.tsx
@@ -5,6 +5,18 @@ import { Check, Copy } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import {
+ SERVICE_TOKEN_PLACEHOLDER,
+ claudeCodeMacCommand,
+ claudeCodeWindowsCommand,
+ claudeWebBlock,
+ claudeWebFields,
+ codexMacCommand,
+ codexWindowsCommand,
+ genericHttpConfig,
+ mcpServerUrl,
+} from "./connection-guides";
import { issueServiceTokenAction, revokeServiceTokenAction } from "./actions";
type TokenRow = {
@@ -15,10 +27,262 @@ type TokenRow = {
revoked: boolean;
};
-export function TokensClient({ tokens }: { tokens: TokenRow[] }) {
+function CodeBlock({
+ copyKey,
+ label,
+ value,
+ copiedKey,
+ onCopy,
+}: {
+ copyKey: string;
+ label: string;
+ value: string;
+ copiedKey: string | null;
+ onCopy: (key: string, value: string) => Promise;
+}) {
+ return (
+
+
+
+ {label}
+
+
onCopy(copyKey, value)}
+ >
+ {copiedKey === copyKey ? (
+ <>
+
+ Copied
+ >
+ ) : (
+ <>
+
+ Copy
+ >
+ )}
+
+
+
+ {value}
+
+
+ );
+}
+
+function ConnectionGuide({
+ appUrl,
+ token,
+ copiedKey,
+ onCopy,
+}: {
+ appUrl: string;
+ token?: string;
+ copiedKey: string | null;
+ onCopy: (key: string, value: string) => Promise;
+}) {
+ const fields = claudeWebFields(appUrl);
+ const tokenReady = Boolean(token?.trim());
+ const mcpUrl = mcpServerUrl(appUrl);
+
+ return (
+
+
+
+
+
Connect a client
+
+ Claude Web signs in with OAuth. Claude Code, Codex, and most HTTP MCP
+ clients use a service token in the `Authorization` header.
+
+
+
onCopy("mcp-url", mcpUrl)}>
+ {copiedKey === "mcp-url" ? (
+ <>
+
+ Copied URL
+ >
+ ) : (
+ <>
+
+ Copy URL
+ >
+ )}
+
+
+
+
+ {mcpUrl}
+
+
+ {tokenReady
+ ? "The snippets below already use the token you just created."
+ : `Generate a token first for the service-token tabs below. Existing token values are not shown again.`}
+
+
+
+
+
+
+
+ Claude Web
+
+
+ Claude Code
+
+
+ Codex
+
+
+ Generic MCP
+
+
+
+
+
+
+ Add a custom connector in Claude, then paste the server URL below. Leave
+ both OAuth client fields blank so Claude uses your server's discovery flow.
+
+
+
+ {[
+ ["claude-web-name", "Name", fields.name],
+ ["claude-web-url", "Remote MCP server URL", fields.remoteMcpServerUrl],
+ ["claude-web-client-id", "OAuth Client ID", fields.oauthClientId],
+ ["claude-web-client-secret", "OAuth Client Secret", fields.oauthClientSecret],
+ ].map(([key, label, value]) => (
+
+
+
+ {label}
+
+
+ {value}
+
+
+
onCopy(key, value)}
+ >
+ {copiedKey === key ? (
+ <>
+
+ Copied
+ >
+ ) : (
+ <>
+
+ Copy
+ >
+ )}
+
+
+ ))}
+
+
+
+
+
+
+
+ Use Claude Code's HTTP MCP transport. These commands add Arin directly
+ with the bearer token header already attached.
+
+
+
+
+
+
+
+
+
+
+
+ Codex CLI and the IDE extension share the same MCP configuration. Set the
+ token in an environment variable, then register Arin once with `codex mcp add`.
+
+
+
+
+
+
+
+
+
+
+
+ Use this for any remote HTTP MCP client that accepts a URL plus custom
+ headers. Replace the placeholder token if you haven't just generated one.
+
+
+
+ {!tokenReady ? (
+
+ Placeholder shown: `{SERVICE_TOKEN_PLACEHOLDER}`. Generate a new token
+ above to get a real value you can copy once.
+
+ ) : null}
+
+
+
+ );
+}
+
+export function TokensClient({
+ appUrl,
+ tokens,
+}: {
+ appUrl: string;
+ tokens: TokenRow[];
+}) {
const [name, setName] = useState("");
const [pending, setPending] = useState(false);
const [issued, setIssued] = useState<{ token: string; copied: boolean } | null>(null);
+ const [copiedKey, setCopiedKey] = useState(null);
const [error, setError] = useState(null);
async function onSubmit(e: FormEvent) {
@@ -38,11 +302,25 @@ export function TokensClient({ tokens }: { tokens: TokenRow[] }) {
}
}
- async function copy() {
+ async function copyIssuedToken() {
if (!issued) return;
- await navigator.clipboard.writeText(issued.token);
- setIssued({ ...issued, copied: true });
- setTimeout(() => setIssued((cur) => (cur ? { ...cur, copied: false } : cur)), 1500);
+ try {
+ await navigator.clipboard.writeText(issued.token);
+ setIssued({ ...issued, copied: true });
+ setTimeout(() => setIssued((cur) => (cur ? { ...cur, copied: false } : cur)), 1500);
+ } catch {
+ setError("Couldn't copy to clipboard. Copy the token manually.");
+ }
+ }
+
+ async function copyValue(key: string, value: string) {
+ try {
+ await navigator.clipboard.writeText(value);
+ setCopiedKey(key);
+ setTimeout(() => setCopiedKey((current) => (current === key ? null : current)), 1500);
+ } catch {
+ setError("Couldn't copy to clipboard. Copy the value manually.");
+ }
}
return (
@@ -65,7 +343,7 @@ export function TokensClient({ tokens }: { tokens: TokenRow[] }) {
{issued.token}
-
+
{issued.copied ? (
<>
Copied
@@ -77,31 +355,21 @@ export function TokensClient({ tokens }: { tokens: TokenRow[] }) {
)}
-
-
- Wire into Claude Code (HTTP MCP)
-
-
-{`{
- "mcpServers": {
- "arin": {
- "type": "http",
- "url": "${typeof window !== "undefined" ? window.location.origin : ""}/api/mcp",
- "headers": {
- "Authorization": "Bearer ${issued.token}"
- }
- }
- }
-}`}
-
-
+
+ The setup snippets below now use this token automatically for Claude Code,
+ Codex, and generic HTTP MCP clients.
+
) : null}
-