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.
One-line answer
Section titled “One-line answer”apm install --mcp io.github.github/github-mcp-serverThis adds one entry under dependencies.mcp: in apm.yml and writes
a runtime-specific MCP config file for every detected harness.
The mcp: section in apm.yml
Section titled “The mcp: section in apm.yml”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.
Adding a server from the CLI
Section titled “Adding a server from the CLI”apm install --mcp NAME writes the entry into apm.yml for you,
then runs install. Three shapes match the three manifest forms:
# Registryapm 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
# Remoteapm install --mcp linear --transport http --url https://mcp.linear.app/sseapm 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.
What apm install writes to disk
Section titled “What apm install writes to disk”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.
| Harness | File | Scope | Format |
|---|---|---|---|
| GitHub Copilot CLI | ~/.copilot/mcp-config.json | global | JSON mcpServers |
| VS Code (Copilot) | .vscode/mcp.json | project | JSON servers |
| Claude Code | .mcp.json (project) or ~/.claude.json (-g) | both | JSON mcpServers |
| Cursor | .cursor/mcp.json | project (only if .cursor/ exists) | JSON mcpServers |
| Codex CLI | .codex/config.toml (project, only if .codex/ exists) or ~/.codex/config.toml (-g) | both | TOML [mcp_servers.*] |
| Gemini CLI | .gemini/settings.json (project, only if .gemini/ exists) or ~/.gemini/settings.json (-g) | both | JSON mcpServers |
| OpenCode | opencode.json | project (only if .opencode/ exists) | JSON mcp |
| Windsurf | ~/.codeium/windsurf/mcp_config.json | global | JSON 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.
stdio vs HTTP servers
Section titled “stdio vs HTTP servers”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 optionalargs:. Use--env KEY=VALUE(repeatable) for environment variables. Servers do not go through a shell, so$VARand backticks inargsare passed literally. - http / sse / streamable-http — APM points your harness at a
remote endpoint. Requires
url:(http or https only — websockets andfile://are rejected). Use--header KEY=VALUE(repeatable) for HTTP headers such asAuthorization.
--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.
Token injection: GitHub MCP server
Section titled “Token injection: GitHub MCP server”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):
- The server name (case-insensitive) is one of:
github-mcp-server,github,github-mcp,github-copilot-mcp-server. - And the parsed URL hostname matches the GitHub host allowlist
(
github.com,*.github.com,githubcopilot.comhosts, 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):
GITHUB_COPILOT_PATGITHUB_TOKENGITHUB_APM_PATGITHUB_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}".
Updating and replacing a server
Section titled “Updating and replacing a server”Re-run apm install --mcp NAME ... against an existing entry:
| Situation | Behaviour |
|---|---|
New NAME | Appended to dependencies.mcp. |
Existing NAME, identical config | No-op. Logs unchanged. |
Existing NAME, different config, TTY | Prompts to replace. |
Existing NAME, different config, CI | Refuses with exit 2. Re-run with --force. |
Use --dry-run to preview the manifest change without writing.
Sibling commands
Section titled “Sibling commands”The apm mcp group is for discovery and standalone install:
apm mcp search <query> # search the configured registryapm mcp list # list available serversapm mcp show <name> # detailed server infoapm 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.