Add a Connect an agent section with setup for Claude Code, Codex, Cursor, and VS Code

This commit is contained in:
Gregory Schier committed 2026-10-14 22:16:55 -07:00
1 parent 72b43f7110
commit 19908b1902
3 files changed
+254 -79

No files matched your search

@@ -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<AgentClient>({
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<AppMetaData>("cmd_metadata")).cliVersion,
@@ -48,7 +61,7 @@ export function SettingsAgents() {
<SettingsList className="space-y-8">
<SettingsSection
title="Yaak CLI"
description="The recommended way to connect an agent. It works with the app closed."
description="Lets agents use Yaak from the command line, even with the app closed."
>
<SettingRow
title="Status"
@@ -83,38 +96,23 @@ export function SettingsAgents() {
<SettingValue value={CLI_INSTALL_COMMAND} />
</SettingRow>
) : (
<>
<SettingRow
title="Agent skill"
description={
<>
Teach the agents you have installed to drive the <InlineCode>yaak</InlineCode>{" "}
command.
</>
}
>
<SettingValue value={SKILL_INSTALL_COMMAND} />
</SettingRow>
<SettingRow title="Claude Code" description="Add Yaak's MCP tools to Claude Code.">
<SettingValue value={CLAUDE_CODE_COMMAND} />
</SettingRow>
<SnippetRow
title="MCP client config"
description={
<>
For other clients that read an <InlineCode>mcpServers</InlineCode> config, like
Cursor.
</>
}
snippet={mcpServersConfig("yaak", ["mcp"])}
/>
</>
<SettingRow
title="Agent skill"
description={
<>
Teach Claude Code, Cursor, Codex, and opencode to drive the{" "}
<InlineCode>yaak</InlineCode> command, wherever they're installed.
</>
}
>
<SettingValue value={SKILL_INSTALL_COMMAND} />
</SettingRow>
)}
</SettingsSection>
<SettingsSection
title="MCP server in the app"
description="For clients that connect over HTTP, or to act on the workspaces open in Yaak."
description="For agents that connect over HTTP, or to act on the workspaces open in Yaak."
>
<ModelSettingRowBoolean
model={settings}
@@ -127,31 +125,52 @@ export function SettingsAgents() {
}
/>
{settings.mcpServerEnabled && token != null && (
<>
<SettingRow
title="Token"
description="Clients send this as a bearer token."
controlClassName="min-w-0 max-w-[min(24rem,40vw)] gap-2"
>
<SettingValue value={token} />
</SettingRow>
<SnippetRow
title="MCP client config"
description={
<>
For clients that read an <InlineCode>mcpServers</InlineCode> config, like Claude
Desktop and Cursor.
</>
}
snippet={mcpServersConfig("npx", [
"-y",
"mcp-remote",
MCP_URL,
"--header",
`Authorization: Bearer ${token}`,
])}
/>
</>
<SettingRow
title="Token"
description="Agents send this as a bearer token."
controlClassName="min-w-0 max-w-[min(24rem,40vw)] gap-2"
>
<SettingValue value={token} />
</SettingRow>
)}
</SettingsSection>
<SettingsSection
title="Connect an agent"
description="Pick your agent or editor to see how to add Yaak's MCP tools to it."
>
<div className="py-4 border-b border-border-subtle">
<SegmentedControl
name="agents_setup_client"
label="Agent"
hideLabel
value={client}
onChange={setClient}
options={AGENT_CLIENTS}
/>
</div>
<SetupRow
title="Through the Yaak CLI"
description={
cliVersion.data == null
? "Recommended. Install the Yaak CLI above first."
: `Recommended. ${cliSetup(client).where}`
}
setup={cliSetup(client)}
/>
{settings.mcpServerEnabled && token != null ? (
<SetupRow
title="Through the app"
description={appSetup(client, MCP_URL, token).where}
setup={appSetup(client, MCP_URL, token)}
/>
) : (
<SettingRow
title="Through the app"
description="Turn on the MCP server above to connect this way."
>
<span />
</SettingRow>
)}
</SettingsSection>
</SettingsList>
@@ -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 (
<div className="py-4 border-b border-border-subtle">
<div className="text text-text">{title}</div>
<div className="mt-1 text-sm text-text-subtle">{description}</div>
<div className="mt-3 relative rounded-md border border-border-subtle bg-surface-highlight">
<pre className="p-3 pr-10 font-mono text-editor text-text-subtle select-text whitespace-pre-wrap wrap-break-word">
{snippet}
</pre>
<div className="absolute top-1.5 right-1.5">
<CopyIconButton size="xs" text={snippet} title={`Copy ${title.toLowerCase()}`} />
</div>
</div>
<Editor
readOnly
hideGutter
wrapLines
heightMode="auto"
stateKey={null}
language={setup.language}
defaultValue={setup.snippet}
className="mt-3 border border-border-subtle rounded-md px-2"
actions={<CopyIconButton size="sm" variant="border" text={setup.snippet} title="Copy setup" />}
/>
</div>
);
}
function mcpServersConfig(command: string, args: string[]) {
return [
"{",
' "mcpServers": {',
' "yaak": {',
` "command": ${JSON.stringify(command)},`,
` "args": [${args.map((a) => JSON.stringify(a)).join(", ")}]`,
" }",
" }",
"}",
].join("\n");
}
+62
View File
@@ -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);
}
});
});
+104
View File
@@ -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(", ")}]`,
);
}