Skip to content

Install MCP servers

apm install is the same driver for two artifact kinds: APM packages (see Install Packages) and MCP servers. This page covers MCP servers: how you declare them, what gets written to each runtime, and how tokens get injected.

Terminal window
apm install --mcp io.github.github/github-mcp-server

This adds one entry under dependencies.mcp: in apm.yml and writes a runtime-specific MCP config file for every detected harness.

MCP servers live under dependencies.mcp: (or devDependencies.mcp:). Three forms are valid — pick the one that matches the source you have:

dependencies:
mcp:
# 1. Registry reference (bare string)
- io.github.github/github-mcp-server
# 2. Self-defined stdio (local process)
- name: filesystem
registry: false
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
# 3. Self-defined remote (HTTP / SSE)
- name: linear
registry: false
transport: http
url: https://mcp.linear.app/sse
headers:
Authorization: "Bearer ${LINEAR_TOKEN}"

The full grammar (overlays, ${input:...} variables, tools: allowlists, package: selection) is in Package Anatomy.

apm install --mcp NAME writes the entry into apm.yml for you, then runs install. Three shapes match the three manifest forms:

Terminal window
# Registry
apm install --mcp io.github.github/github-mcp-server
# stdio (everything after `--` is the spawn command)
apm install --mcp filesystem -- npx -y @modelcontextprotocol/server-filesystem /workspace
# Remote
apm install --mcp linear --transport http --url https://mcp.linear.app/sse

apm mcp install NAME ... is an alias that forwards to the same code path. The apm mcp group also provides search, list, and show for discovery — see the CLI reference.

For every harness APM detects in your environment, apm install writes a runtime-specific MCP config file. The schemas differ; the apm.yml source of truth does not.

HarnessFileScopeFormat
GitHub Copilot CLI~/.copilot/mcp-config.jsonglobalJSON mcpServers
VS Code (Copilot).vscode/mcp.jsonprojectJSON servers
Claude Code.mcp.json (project) or ~/.claude.json (-g)bothJSON mcpServers
Cursor.cursor/mcp.jsonproject (only if .cursor/ exists)JSON mcpServers
Codex CLI.codex/config.toml (project, only if .codex/ exists) or ~/.codex/config.toml (-g)bothTOML [mcp_servers.*]
Gemini CLI.gemini/settings.json (project, only if .gemini/ exists) or ~/.gemini/settings.json (-g)bothJSON mcpServers
OpenCodeopencode.jsonproject (only if .opencode/ exists)JSON mcp
Windsurf~/.codeium/windsurf/mcp_config.jsonglobalJSON mcpServers

How targets: gates which configs get written

Section titled “How targets: gates which configs get written”

MCP install honors the same target resolution chain as apm install for any other dependency: see Where files land. In short: --target wins, then apm.yml’s targets:, then auto-detect from harness directories.

When a runtime is outside the active target set, APM does NOT write its MCP config — and announces the drop on stdout so you can confirm the gate took effect:

[i] Skipped MCP config for claude, codex (active targets: copilot)

This single rule replaces two older ones that used to coexist:

  • A “directory opt-in” carve-out for Cursor / Gemini / OpenCode — now redundant, because targets: (or auto-detection) drives the gate for those runtimes too.
  • The pre-#1335 silent skip path, which dropped non-listed runtimes without telling you.

A malformed targets: field (both target: and targets: set, targets: [], or an unknown target name) fails closed: no MCP files are written and an [x] error names the field to fix. A greenfield project with no targets:, no --target flag, AND no detected signals (.github/copilot-instructions.md, .cursor/, etc.) also fails closed with the same [x] voice — consistent with how apm install treats the same input. Pin a target with --target or declare one in apm.yml. (#1335)

apm install -g --mcp NAME is a deliberate carve-out: it routes the write to each runtime’s user-scope MCP config (Copilot CLI to ~/.copilot/mcp-config.json, Codex CLI to ~/.codex/config.toml, Gemini CLI to ~/.gemini/settings.json) and does not consult the project-scope targets: whitelist — user-scope writes are by definition not project-bound. Workspace-only runtimes (VS Code, Cursor, OpenCode) are skipped at user scope.

MCP defines two transport families. APM exposes both:

  • stdio — APM (and your harness) spawns a local process and speaks MCP over its stdio. Requires command: and optional args:. Use --env KEY=VALUE (repeatable) for environment variables. Servers do not go through a shell, so $VAR and backticks in args are passed literally.
  • http / sse / streamable-http — APM points your harness at a remote endpoint. Requires url: (http or https only — websockets and file:// are rejected). Use --header KEY=VALUE (repeatable) for HTTP headers such as Authorization.

--transport is inferred when omitted: a --url implies a remote transport, a post--- command implies stdio. The mutually-exclusive combinations (--url plus stdio command, --header without --url, etc.) are rejected with exit code 2.

APM does not template arbitrary environment variables into MCP config files (your harness does that at runtime). It does inject one specific credential automatically:

When the Copilot CLI adapter writes a remote MCP config and the server is identified as the GitHub MCP server, APM resolves a token and adds an Authorization: Bearer <token> header.

The server is identified as “GitHub” only when it satisfies both of these narrow checks (copilot.py:1004):

  1. The server name (case-insensitive) is one of: github-mcp-server, github, github-mcp, github-copilot-mcp-server.
  2. And the parsed URL hostname matches the GitHub host allowlist (github.com, *.github.com, githubcopilot.com hosts, and registered GHES hostnames).

This is a parsed-host allowlist on hostname, not a substring check. A URL like https://github.com.evil.example does not match because the parsed hostname is github.com.evil.example, not github.com.

The token is resolved from this chain (first non-empty wins):

  1. GITHUB_COPILOT_PAT
  2. GITHUB_TOKEN
  3. GITHUB_APM_PAT
  4. GITHUB_PERSONAL_ACCESS_TOKEN (Copilot CLI compat)

If none are set, no header is injected and the server is written without auth — you will get an unauthenticated request at runtime. For other authenticated remote servers, set headers explicitly with --header Authorization="Bearer ${MY_TOKEN}".

Re-run apm install --mcp NAME ... against an existing entry:

SituationBehaviour
New NAMEAppended to dependencies.mcp.
Existing NAME, identical configNo-op. Logs unchanged.
Existing NAME, different config, TTYPrompts to replace.
Existing NAME, different config, CIRefuses with exit 2. Re-run with --force.

Use --dry-run to preview the manifest change without writing.

The apm mcp group is for discovery and standalone install:

apm mcp search <query> # search the configured registry
apm mcp list # list available servers
apm mcp show <name> # detailed server info
apm mcp install <name> # alias for `apm install --mcp <name>`

Full flag tables and exit codes: CLI reference.

  • Authoring an MCP server as a primitive of your own package — see the producer ramp.
  • Lockfile and trust boundary for transitive MCP servers — Lifecycle.