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}"
# 4. Self-defined remote with harness-specific extra keys - name: slack registry: false transport: http url: https://mcp.slack.com/mcp oauth: clientId: "<pre-registered-client-id>" callbackPort: 3118Unknown keys like oauth above are passthrough fields: they are
preserved and written into the generated config for every harness you
install (so a Claude Code oauth block reaches all targets; harnesses
that do not recognise it ignore it). Keys that collide with a modeled
field (command, url, headers, env, …) are rejected with a
warning so they cannot redirect a server. See
Manifest Schema for the full rules.
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.
Registry-declared environment variables honor the registry’s
required flag. Servers with optional auth install without token
prompts until you choose to configure one. See the
manifest schema reference
for the full required-vs-optional runtime config rule.
| 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 |
| Antigravity CLI | .agents/mcp_config.json (project, only if .agents/ exists) or ~/.gemini/config/mcp_config.json (-g) | both | JSON mcpServers |
| OpenCode | opencode.json | project (only if .opencode/ exists) | JSON mcp |
| Windsurf | ~/.codeium/windsurf/mcp_config.json | global | JSON mcpServers |
| Kiro IDE | .kiro/settings/mcp.json (project, only if .kiro/ exists) or ~/.kiro/settings/mcp.json (-g) | both | 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 (for example, Copilot CLI to
~/.copilot/mcp-config.json, Claude Code to ~/.claude.json, Codex CLI to
~/.codex/config.toml, Gemini CLI to ~/.gemini/settings.json, Antigravity CLI to ~/.gemini/config/mcp_config.json, Windsurf to
~/.codeium/windsurf/mcp_config.json, Kiro to ~/.kiro/settings/mcp.json,
and JetBrains Copilot to its OS-specific user config). It 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.