From 19908b190250defa235661692b0105458a0b22b3 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 14 Oct 2026 22:16:55 -0700 Subject: [PATCH] Add a Connect an agent section with setup for Claude Code, Codex, Cursor, and VS Code --- .../components/Settings/SettingsAgents.tsx | 167 +++++++++--------- apps/yaak-client/lib/agentSetup.test.ts | 62 +++++++ apps/yaak-client/lib/agentSetup.ts | 104 +++++++++++ 3 files changed, 254 insertions(+), 79 deletions(-) create mode 100644 apps/yaak-client/lib/agentSetup.test.ts create mode 100644 apps/yaak-client/lib/agentSetup.ts diff --git a/apps/yaak-client/components/Settings/SettingsAgents.tsx b/apps/yaak-client/components/Settings/SettingsAgents.tsx index 84099f138..102d53705 100644 --- a/apps/yaak-client/components/Settings/SettingsAgents.tsx +++ b/apps/yaak-client/components/Settings/SettingsAgents.tsx @@ -6,10 +6,19 @@ import type { AppMetaData } from "@yaakapp-internal/rpc-schema"; import { useAtomValue } from "jotai"; import { useKeyValue } from "../../hooks/useKeyValue"; import { appInfo } from "../../lib/appInfo"; +import { + AGENT_CLIENTS, + type AgentClient, + type AgentSetup, + appSetup, + cliSetup, +} from "../../lib/agentSetup"; import { rpc } from "../../lib/rpc"; import { CopyIconButton } from "../CopyIconButton"; import { Button } from "../core/Button"; +import { Editor } from "../core/Editor/LazyEditor"; import { IconButton } from "../core/IconButton"; +import { SegmentedControl } from "../core/SegmentedControl"; import { ModelSettingRowBoolean, SettingRow, @@ -21,7 +30,6 @@ import { const MCP_URL = "http://127.0.0.1:64343/mcp"; const CLI_INSTALL_COMMAND = "npm install -g @yaakapp/cli"; const SKILL_INSTALL_COMMAND = "yaak agent install"; -const CLAUDE_CODE_COMMAND = "claude mcp add yaak -- yaak mcp"; export function SettingsAgents() { const settings = useAtomValue(settingsAtom); @@ -30,6 +38,11 @@ export function SettingsAgents() { key: "mcp_server_token", fallback: null, }); + const { value: storedClient, set: setClient } = useKeyValue({ + key: "agents_setup_client", + fallback: "claude_code", + }); + const client = AGENT_CLIENTS.find((c) => c.value === storedClient)?.value ?? "claude_code"; const cliVersion = useQuery({ queryKey: ["cli_version"], queryFn: async () => (await rpc("cmd_metadata")).cliVersion, @@ -48,7 +61,7 @@ export function SettingsAgents() { ) : ( - <> - - Teach the agents you have installed to drive the yaak{" "} - command. - - } - > - - - - - - - For other clients that read an mcpServers config, like - Cursor. - - } - snippet={mcpServersConfig("yaak", ["mcp"])} - /> - + + Teach Claude Code, Cursor, Codex, and opencode to drive the{" "} + yaak command, wherever they're installed. + + } + > + + )} {settings.mcpServerEnabled && token != null && ( - <> - - - - - For clients that read an mcpServers config, like Claude - Desktop and Cursor. - - } - snippet={mcpServersConfig("npx", [ - "-y", - "mcp-remote", - MCP_URL, - "--header", - `Authorization: Bearer ${token}`, - ])} - /> - + + + + )} + + + +
+ +
+ + {settings.mcpServerEnabled && token != null ? ( + + ) : ( + + + )}
@@ -159,40 +178,30 @@ export function SettingsAgents() { ); } -function SnippetRow({ +function SetupRow({ title, description, - snippet, + setup, }: { title: string; - description: React.ReactNode; - snippet: string; + description: string; + setup: AgentSetup; }) { return (
{title}
{description}
-
-
-          {snippet}
-        
-
- -
-
+ } + />
); } - -function mcpServersConfig(command: string, args: string[]) { - return [ - "{", - ' "mcpServers": {', - ' "yaak": {', - ` "command": ${JSON.stringify(command)},`, - ` "args": [${args.map((a) => JSON.stringify(a)).join(", ")}]`, - " }", - " }", - "}", - ].join("\n"); -} diff --git a/apps/yaak-client/lib/agentSetup.test.ts b/apps/yaak-client/lib/agentSetup.test.ts new file mode 100644 index 000000000..847d88a2f --- /dev/null +++ b/apps/yaak-client/lib/agentSetup.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, test } from "vite-plus/test"; +import { AGENT_CLIENTS, appSetup, cliSetup } from "./agentSetup"; + +const URL = "http://127.0.0.1:64343/mcp"; +const TOKEN = "abc123"; + +describe("cliSetup", () => { + test("uses each client's own add command", () => { + expect(cliSetup("claude_code").snippet).toBe("claude mcp add yaak -- yaak mcp"); + expect(cliSetup("codex").snippet).toBe("codex mcp add yaak -- yaak mcp"); + }); + + test("writes the config shape each client reads", () => { + expect(JSON.parse(cliSetup("cursor").snippet)).toEqual({ + mcpServers: { yaak: { command: "yaak", args: ["mcp"] } }, + }); + expect(JSON.parse(cliSetup("vscode").snippet)).toEqual({ + servers: { yaak: { type: "stdio", command: "yaak", args: ["mcp"] } }, + }); + expect(JSON.parse(cliSetup("other").snippet)).toEqual({ + mcpServers: { yaak: { command: "yaak", args: ["mcp"] } }, + }); + }); +}); + +describe("appSetup", () => { + test("passes the token as a bearer header", () => { + expect(appSetup("claude_code", URL, TOKEN).snippet).toBe( + `claude mcp add --transport http yaak ${URL} --header "Authorization: Bearer ${TOKEN}"`, + ); + expect(appSetup("codex", URL, TOKEN).snippet).toBe( + `[mcp_servers.yaak]\nurl = "${URL}"\nhttp_headers = { "Authorization" = "Bearer ${TOKEN}" }`, + ); + expect(JSON.parse(appSetup("cursor", URL, TOKEN).snippet)).toEqual({ + mcpServers: { yaak: { url: URL, headers: { Authorization: `Bearer ${TOKEN}` } } }, + }); + expect(JSON.parse(appSetup("vscode", URL, TOKEN).snippet)).toEqual({ + servers: { yaak: { type: "http", url: URL, headers: { Authorization: `Bearer ${TOKEN}` } } }, + }); + expect(JSON.parse(appSetup("other", URL, TOKEN).snippet)).toEqual({ + mcpServers: { + yaak: { + command: "npx", + args: ["-y", "mcp-remote", URL, "--header", `Authorization: Bearer ${TOKEN}`], + }, + }, + }); + }); + + test("keeps argument lists on one line", () => { + expect(appSetup("other", URL, TOKEN).snippet).toContain( + `"args": ["-y", "mcp-remote", "${URL}", "--header", "Authorization: Bearer ${TOKEN}"]`, + ); + }); + + test("covers every listed client", () => { + for (const { value } of AGENT_CLIENTS) { + expect(cliSetup(value).snippet).not.toBe(""); + expect(appSetup(value, URL, TOKEN).snippet).toContain(TOKEN); + } + }); +}); diff --git a/apps/yaak-client/lib/agentSetup.ts b/apps/yaak-client/lib/agentSetup.ts new file mode 100644 index 000000000..fd5e489e2 --- /dev/null +++ b/apps/yaak-client/lib/agentSetup.ts @@ -0,0 +1,104 @@ +export type AgentClient = "claude_code" | "codex" | "cursor" | "vscode" | "other"; + +export const AGENT_CLIENTS: { label: string; value: AgentClient }[] = [ + { label: "Claude Code", value: "claude_code" }, + { label: "Codex", value: "codex" }, + { label: "Cursor", value: "cursor" }, + { label: "VS Code", value: "vscode" }, + { label: "Other", value: "other" }, +]; + +export interface AgentSetup { + /** Where the snippet goes, as a sentence */ + where: string; + snippet: string; + language: "shell" | "json" | "text"; +} + +const RUN_IN_TERMINAL = "Run in a terminal."; + +/** Setup for `yaak mcp`, which the client launches over stdio. */ +export function cliSetup(client: AgentClient): AgentSetup { + switch (client) { + case "claude_code": + return { where: RUN_IN_TERMINAL, snippet: "claude mcp add yaak -- yaak mcp", language: "shell" }; + case "codex": + return { where: RUN_IN_TERMINAL, snippet: "codex mcp add yaak -- yaak mcp", language: "shell" }; + case "cursor": + return { + where: "Add to ~/.cursor/mcp.json.", + snippet: json({ mcpServers: { yaak: { command: "yaak", args: ["mcp"] } } }), + language: "json", + }; + case "vscode": + return { + where: "Add to your user mcp.json (MCP: Open User Configuration).", + snippet: json({ servers: { yaak: { type: "stdio", command: "yaak", args: ["mcp"] } } }), + language: "json", + }; + case "other": + return { + where: "For clients that read an mcpServers config, like Claude Desktop and Windsurf.", + snippet: json({ mcpServers: { yaak: { command: "yaak", args: ["mcp"] } } }), + language: "json", + }; + } +} + +/** Setup for the app's MCP server at `url`, which needs `token` as a bearer token. */ +export function appSetup(client: AgentClient, url: string, token: string): AgentSetup { + const authorization = `Bearer ${token}`; + switch (client) { + case "claude_code": + return { + where: RUN_IN_TERMINAL, + snippet: `claude mcp add --transport http yaak ${url} --header "Authorization: ${authorization}"`, + language: "shell", + }; + case "codex": + return { + where: "Add to ~/.codex/config.toml.", + snippet: [ + "[mcp_servers.yaak]", + `url = ${JSON.stringify(url)}`, + `http_headers = { "Authorization" = ${JSON.stringify(authorization)} }`, + ].join("\n"), + language: "text", + }; + case "cursor": + return { + where: "Add to ~/.cursor/mcp.json.", + snippet: json({ mcpServers: { yaak: { url, headers: { Authorization: authorization } } } }), + language: "json", + }; + case "vscode": + return { + where: "Add to your user mcp.json (MCP: Open User Configuration).", + snippet: json({ + servers: { yaak: { type: "http", url, headers: { Authorization: authorization } } }, + }), + language: "json", + }; + case "other": + return { + where: "For clients that read an mcpServers config, through the mcp-remote bridge.", + snippet: json({ + mcpServers: { + yaak: { + command: "npx", + args: ["-y", "mcp-remote", url, "--header", `Authorization: ${authorization}`], + }, + }, + }), + language: "json", + }; + } +} + +/** Pretty JSON with arrays of plain values kept on one line. */ +function json(value: unknown): string { + return JSON.stringify(value, null, 2).replace( + /\[\n\s*([^[\]{}]*?)\n\s*\]/g, + (_, items: string) => `[${items.split(/,\n\s*/).join(", ")}]`, + ); +}