# lib/integration-setup.ts · screen/col

[View on GitCafe](https://git.cafe/screen/col/blob/69fb62d64838418945518001e78e2a230d149fcc/lib/integration-setup.ts)

Repository: [screen/col](https://git.cafe/screen/col)

Visibility: public

Requested revision: 69fb62d64838418945518001e78e2a230d149fcc

Requested commit: 69fb62d64838418945518001e78e2a230d149fcc

Commit: 69fb62d64838418945518001e78e2a230d149fcc

Blob: e73c453c684780780d326cf7aadbf17bb387e903

Size: 9365 bytes

[Immutable source](https://git.cafe/screen/col/blob/69fb62d64838418945518001e78e2a230d149fcc/lib/integration-setup.ts?format=markdown)

```
import type { Integration, McpClient, McpServer, McpServerPath, SecretEnv } from "../data/integrations";
import { connectorPaths, mcpServerPaths } from "../data/integrations.ts";

/**
 * Turns an integration's setup paths into what a person or an agent needs: a
 * config snippet for each client, short labels, and a copyable agent prompt.
 *
 * All client-specific knowledge lives here, taken from each client's MCP docs:
 * the config file, its wrapper key, and how a secret is read from the
 * environment. Entries in `data/integrations.ts` stay client-neutral, so they
 * never hold a client snippet or a secret value.
 *
 * - Claude Code: https://code.claude.com/docs/en/mcp (`${VAR}` expansion in .mcp.json)
 * - Cursor: https://cursor.com/docs/context/mcp (`${env:VAR}` interpolation)
 * - VS Code: https://code.visualstudio.com/docs/agents/reference/mcp-configuration (`${input:id}` prompts)
 * - Codex: https://developers.openai.com/codex/mcp (`env_http_headers` reads values from the environment)
 */

/** Where each client reads MCP config: the project root, or the home directory for `~` paths. */
export const CONFIG_FILES = {
  "Claude Code": ".mcp.json",
  Cursor: ".cursor/mcp.json",
  "VS Code": ".vscode/mcp.json",
  Codex: "~/.codex/config.toml",
} as const satisfies Record<McpClient, string>;

export interface ClientSetup {
  client: McpClient;
  file: (typeof CONFIG_FILES)[McpClient];
  language: "json" | "toml";
  code: string;
  /** What to do after saving the file. */
  next: string;
}

type Snippet = Pick<ClientSetup, "language" | "code">;

const json = (value: unknown) => JSON.stringify(value, null, 2);
/** TOML basic strings use the same escapes as JSON strings. */
const tomlString = (value: string) => JSON.stringify(value);
const tomlKey = (key: string) => (/^[A-Za-z0-9_-]+$/.test(key) ? key : tomlString(key));
const capitalize = (text: string) => text.charAt(0).toUpperCase() + text.slice(1);
/** "A, B, or C" */
const either = (items: readonly string[]) => new Intl.ListFormat("en", { type: "disjunction" }).format(items);

const secretOf = (server: McpServer): SecretEnv | undefined =>
  server.transport === "http" && server.auth.kind === "api-key" ? server.auth.secret : undefined;

const usesOAuth = (server: McpServer) => server.transport === "http" && server.auth.kind === "oauth";

/** Request headers for a remote server, with the secret written the way `reference` spells it. */
const headers = (server: McpServer, reference: (secret: SecretEnv) => string) =>
  server.transport === "http" && server.auth.kind === "api-key"
    ? { headers: { [server.auth.header]: reference(server.auth.secret) } }
    : {};

const stdioEntry = (server: Extract<McpServer, { transport: "stdio" }>) => ({ type: "stdio", command: server.command, args: server.args });

/** VS Code input ids are referenced as `${input:id}`. */
const inputId = (secret: SecretEnv) => secret.name.toLowerCase().replaceAll("_", "-");

const snippets: Record<McpClient, (key: string, server: McpServer) => Snippet> = {
  "Claude Code": (key, server) => ({
    language: "json",
    code: json({
      mcpServers: {
        [key]: server.transport === "stdio"
          ? stdioEntry(server)
          : { type: "http", url: server.url, ...headers(server, (secret) => `\${${secret.name}}`) },
      },
    }),
  }),
  Cursor: (key, server) => ({
    language: "json",
    code: json({
      mcpServers: {
        [key]: server.transport === "stdio"
          ? stdioEntry(server)
          : { url: server.url, ...headers(server, (secret) => `\${env:${secret.name}}`) },
      },
    }),
  }),
  "VS Code": (key, server) => {
    const secret = secretOf(server);
    return {
      language: "json",
      code: json({
        ...(secret && { inputs: [{ type: "promptString", id: inputId(secret), description: secret.label, password: true }] }),
        servers: {
          [key]: server.transport === "stdio"
            ? stdioEntry(server)
            : { type: "http", url: server.url, ...headers(server, (value) => `\${input:${inputId(value)}}`) },
        },
      }),
    };
  },
  Codex: (key, server) => {
    const lines = [`[mcp_servers.${tomlKey(key)}]`];
    if (server.transport === "stdio") {
      lines.push(`command = ${tomlString(server.command)}`, `args = [${server.args.map(tomlString).join(", ")}]`);
    } else {
      lines.push(`url = ${tomlString(server.url)}`);
      if (server.auth.kind === "api-key") lines.push(`env_http_headers = { ${tomlString(server.auth.header)} = ${tomlString(server.auth.secret.name)} }`);
    }
    return { language: "toml", code: lines.join("\n") };
  },
};

function nextStep(client: McpClient, key: string, server: McpServer): string {
  const secret = secretOf(server);
  const oauth = usesOAuth(server);
  const step = {
    "Claude Code": oauth
      ? `start Claude Code, approve the ${key} server, then run /mcp, select ${key}, and choose Authenticate`
      : `start Claude Code in the project and approve the ${key} server when asked`,
    Cursor: oauth ? `select Connect next to ${key} in Cursor's MCP settings and sign in` : `enable ${key} in Cursor's MCP settings`,
    "VS Code": oauth
      ? `select Start above ${key} in the file, then allow access when the browser opens`
      : secret
        ? `select Start above ${key} in the file. VS Code asks for the ${secret.label} once and stores it securely`
        : `select Start above ${key} in the file`,
    Codex: oauth ? `run codex mcp login ${key}, then restart Codex` : "restart Codex to load the server",
  }[client];
  // VS Code prompts for the secret itself; the other clients read it from the environment.
  return secret && client !== "VS Code" ? `Set ${secret.name} in your environment, then ${step}.` : `${capitalize(step)}.`;
}

/** One config snippet per documented client, across every MCP server path. */
export function clientSetups(integration: Integration): ClientSetup[] {
  return mcpServerPaths(integration).flatMap(({ key, server, clients }) =>
    clients.map((client) => ({ client, file: CONFIG_FILES[client], ...snippets[client](key, server), next: nextStep(client, key, server) })),
  );
}

export type Access = "No sign-in" | "Account sign-in" | "API key";

const serverAccess = (server: McpServer): Access =>
  server.transport === "stdio" || server.auth.kind === "none" ? "No sign-in" : server.auth.kind === "oauth" ? "Account sign-in" : "API key";

/** How a user gets access through each setup path, without duplicates. A connector always signs in to the provider's account. */
export function accessLabels(integration: Integration): Access[] {
  return [...new Set(integration.setup.map((path): Access => (path.type === "Connector" ? "Account sign-in" : serverAccess(path.server))))];
}

export const transportLabel = (server: McpServer) => (server.transport === "stdio" ? "Local (stdio)" : "Remote (HTTP)");

function serverBrief({ key, server, clients }: McpServerPath, provider: string): string {
  const access = server.transport === "http" && server.auth.kind === "api-key"
    ? `- Needs a ${server.auth.secret.label}, sent in the ${server.auth.header} header. Ask me to create it at ${server.auth.secret.url} and set ${server.auth.secret.name} in my environment. Reference ${server.auth.secret.name} from the config instead of the value.`
    : usesOAuth(server)
      ? `- Signs in with OAuth. The client opens ${provider}'s sign-in page on first connection; tell me when to approve access.`
      : "- Needs no sign-in or API key.";
  return [
    `MCP server "${key}":`,
    server.transport === "stdio"
      ? `- Runs locally over stdio: ${[server.command, ...server.args].join(" ")}`
      : `- Remote Streamable HTTP endpoint: ${server.url}`,
    access,
    `- Config file per client: ${clients.map((client) => `${client}: ${CONFIG_FILES[client]}`).join("; ")}.`,
  ].join("\n");
}

/** Copyable prompt that sets the integration up in whichever client the user has. */
export function agentPrompt(integration: Integration): string {
  const servers = mcpServerPaths(integration);
  const connectors = connectorPaths(integration);
  const steps = [
    "Ask which client I use if you can't tell.",
    ...servers.map(({ key, clients }) => `For ${either(clients)}, add the "${key}" server to that client's config file, using its documented format. Keep every server and setting already in the file.`),
    ...(connectors.length ? [`For ${either(connectors.map(({ client }) => client))}, don't edit any files. Send me the connector link so I can add it there.`] : []),
    "Reload the client, finish any sign-in, and confirm the server's tools are listed.",
  ];

  return [
    `Set up ${integration.name} (${integration.url}) for the AI client I use with this project.`,
    ...servers.map((path) => serverBrief(path, integration.provider.name)),
    ...(connectors.length ? [["Connectors (no config file):", ...connectors.map(({ client, url }) => `- ${client}: ${url}`)].join("\n")] : []),
    ...(integration.notes?.length ? [["Before you start:", ...integration.notes.map((note) => `- ${note}`)].join("\n")] : []),
    ["Steps:", ...steps.map((step, index) => `${index + 1}. ${step}`)].join("\n"),
    "Never write a secret value into a config file or ask me to paste one into this chat. If anything here differs from the provider's guide, follow the guide.",
  ].join("\n\n");
}

```
