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. APM then attempts runtime-specific MCP config writes for targets that pass selection, scope, and adapter prerequisites.

Registry metadata prefetches use up to four concurrent lookups while preserving the order of the requested server references.

The two sections have different propagation rules. In the root project, apm install activates both sections for the author’s environment. A direct or transitive dependency package contributes only dependencies.mcp; its devDependencies.mcp entries never enter the consumer’s target config, lockfile provenance, or audit baseline. This keeps package-author debug servers out of consumer tool lists.

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: 3118

Unknown 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.

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
# Global stdio install (writes user APM + runtime config only)
apm install -g --target claude --mcp fetch -- npx -y @modelcontextprotocol/server-fetch
# Global registry install (requires one unique registry match)
apm install -g --target claude --mcp io.github.github/github-mcp-server

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 selected target that passes its scope and adapter prerequisites, 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.

MCP Registry v0.1 container packages use registryType: oci. APM selects Docker automatically, keeps Docker run options before the image, and places package arguments after the image. Copilot, Codex, Gemini, and related adapters prefer packages in npm, OCI, then PyPI order. VS Code prefers npm, PyPI, then OCI. OCI packages require Docker to be available when the harness starts the server; no per-target launcher configuration is needed.

When a required registry runtime variable has a default, APM prompts once per variable and displays that default as the suggested answer. Press Enter to accept it or provide an override. Secret defaults remain accepted on Enter but are never displayed. For OCI/Docker launchers, non-secret selected values replace every {variable} reference across the package’s runtime and package arguments before the native config is written. VS Code renders secret variables as target-native secret-input references instead, so secret bytes never enter mcp.json. A required variable without a collected value or default declines that target configuration; VS Code treats workspaceFolder as its built-in ${workspaceFolder} token.

Claude Code is the one target whose entries are merged key by key rather than replaced, so keys APM does not manage (a hand-authored OAuth block, for example) survive a reinstall. The keys describing a transport are not among them: an entry is rewritten to carry only the transport its declaration names, so redeclaring a server from http to stdio drops the previous url and headers instead of leaving both transports on one entry. This cleanup also repairs older mixed entries, but only when APM writes that server. An unchanged self-defined declaration with matching lock state can skip the write; repeating that install does not automatically repair a mixed entry. Rewriting an unchanged, valid entry preserves its field order.

For VS Code and Copilot-family adapters, non-container npm, pypi, and generic packages preserve typed v0.1 runtimeArguments and packageArguments in authored order, with exactly one semantic package identity. Legacy value_hint arguments remain supported. Registry defaults resolve normally, secret variables use target-native references, unresolved optional groups are omitted atomically, and malformed or unresolved required entries fail closed.

Harness File Scope Format
GitHub Copilot CLI .github/mcp.json project JSON mcpServers
GitHub Copilot CLI $COPILOT_HOME/mcp-config.json (-g, unset/blank: ~/.copilot/mcp-config.json) global JSON mcpServers
VS Code (Copilot) .vscode/mcp.json project JSON servers
Claude Code .mcp.json (project) or $CLAUDE_CONFIG_DIR/.claude.json (-g; unset/blank: ~/.claude.json) both JSON mcpServers
Cursor .cursor/mcp.json (project, only if .cursor/ exists) or ~/.cursor/mcp.json (-g) both JSON mcpServers
Codex CLI .codex/config.toml (project, only if .codex/ exists) or $CODEX_HOME/config.toml (-g, when non-blank; otherwise ~/.codex/config.toml) 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
Hermes Agent $HERMES_HOME/config.yaml (unset/blank: ~/.hermes/config.yaml; explicit --target hermes only) home-scoped YAML mcp_servers
OpenCode opencode.json (project, only if .opencode/ exists) or ~/.config/opencode/opencode.json (-g) both 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
JetBrains Copilot %LOCALAPPDATA%\github-copilot\intellij\mcp.json (Windows) or $XDG_CONFIG_HOME/github-copilot/intellij/mcp.json (macOS/Linux; defaults to ~/.config/github-copilot/intellij/mcp.json) global JSON servers

For OpenCode, set the top-level enabled field on an MCP dependency to pass a value unchanged into that server’s opencode.json entry. If omitted, APM keeps the existing true default. APM does not validate or coerce explicit values; OpenCode interprets them. Other targets ignore this OpenCode-only field. Reinstall applies changes to enabled, including its JSON type; removing the field restores true.

Global Cursor and OpenCode installs preserve your personal servers, including ones that share a name with a package-declared server. Reinstalling and pruning change only the entries APM wrote for that target. If you move a direct --mcp server to a different --target, APM writes the new target and then removes the entry from the old one; if that removal fails, the server stays in both targets and retrying the same install removes the old entry.

If a user config is unreadable, contains JSONC, has a non-object server map, or uses a symlink below the home directory, APM warns and fails without rewriting it, even on a no-op reinstall. Fix the reported config and rerun the original command with --global and the same --target selection. A home-directory alias itself remains supported.

How targets: gates which configs get written

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

MCP install resolves targets in this order:

  1. Explicit CLI selection: --runtime (legacy, one runtime) or --target (one or more targets).
  2. Canonical targets: / target: values declared in apm.yml.
  3. The saved apm config set target ... default.
  4. Machine discovery, only when the manifest and saved config do not restrict targets.

--exclude narrows whichever set wins. Progress output names that post-exclusion selection; project, scope, and adapter gates can still skip a write.

The same effective decision drives package, MCP, and LSP phases; APM does not re-resolve each phase independently.

At project scope, the copilot target writes MCP state to .github/mcp.json, which Copilot CLI discovers from the repository. The vscode target writes only .vscode/mcp.json. At global scope, copilot writes $COPILOT_HOME/mcp-config.json, or ~/.copilot/mcp-config.json when COPILOT_HOME is unset.

Portability boundary: A committed target list makes lockfile MCP ownership deterministic across machines with different installed harnesses. If targets: is omitted (or a legacy all declaration is folded to omission), machine discovery is intentional and ownership can follow each machine’s installed harnesses. Declare targets when teammates or CI must produce the same MCP ownership.

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)

On reinstall, removing a previously configured target also removes the APM-managed server entries from that target’s native config. User-authored servers and unrelated JSON or TOML settings remain unchanged.

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 before machine discovery: no MCP files are written and an [x] error names the field to fix. A greenfield project with no targets:, no --target flag, no saved target, 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. The command exits non-zero before adding a direct --mcp entry to apm.yml or deploying package files. Pin a target with --target, declare one in apm.yml, or save one with apm config set target <value>. A native MCP config write failure also exits non-zero and names the target path or permissions to check. (#1335)

apm install -g --mcp NAME routes the write to each runtime’s user-scope MCP config (for example, Copilot CLI to $COPILOT_HOME/mcp-config.json when COPILOT_HOME is set, otherwise ~/.copilot/mcp-config.json; Claude Code to $CLAUDE_CONFIG_DIR/.claude.json when CLAUDE_CONFIG_DIR is set to a non-whitespace absolute path (both for installation and stale server cleanup). Unset or blank values use ~/.claude.json; relative values are rejected. Codex CLI writes to $CODEX_HOME/config.toml when CODEX_HOME is set to a non-whitespace value or ~/.codex/config.toml otherwise, Cursor to ~/.cursor/mcp.json, OpenCode to ~/.config/opencode/opencode.json, Gemini CLI to ~/.gemini/settings.json, Antigravity CLI to ~/.gemini/config/mcp_config.json, Hermes to $HERMES_HOME/config.yaml whenever selected explicitly (or ~/.hermes/config.yaml when unset or blank), Windsurf to ~/.codeium/windsurf/mcp_config.json, Kiro to ~/.kiro/settings/mcp.json, and JetBrains Copilot to its OS-specific user config). When the user-scope manifest declares a targets: field (or the CLI passes --target), only the matching runtimes receive the config write. When no CLI target, user-scope manifest target, or saved apm config target restricts targets, all detected user-scope-capable runtimes are configured. The workspace-only VS Code target is skipped with a warning when a mixed target set also contains a global-capable runtime. If none of the selected targets supports user scope, the command exits 2 before changing the user manifest, lockfile, or runtime configuration. The direct command creates or updates ~/.apm/apm.yml; it does not fall back to the current project’s manifest. With --dry-run, it previews the user-scope entry and runtime targets without creating that manifest, lockfile, or runtime configuration. APM leaves an existing Cursor or OpenCode config untouched and reports that target as failed when it cannot parse the file as JSON (for example, OpenCode JSONC comments) or when its user-scope path runs through a symlink.

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.

Codex requires HTTPS for non-loopback remote endpoints. Plain HTTP is accepted only for literal loopback addresses such as localhost, ip6-localhost, 127.0.0.0/8, and ::1, which keeps local development servers usable without sending cleartext traffic off the machine. For example:

Terminal window
apm install --target codex --mcp local-dev --url http://localhost:3000/mcp

This writes the endpoint to the Codex MCP configuration.

--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.

For Cursor, env-var references are written using Cursor’s native ${env:NAME} syntax, so referenced secret values are resolved by Cursor when it starts the MCP server and are not written into the Cursor config for the install scope (.cursor/mcp.json for a project, ~/.cursor/mcp.json with -g). Explicit static values in mcp.env remain static and are written as authored; keep secrets out of those values.

APM translates supported environment-variable references using each target’s native syntax. It also injects one specific GitHub credential automatically when the server is recognized as the GitHub MCP server:

When an adapter using the shared GitHub auth path writes a remote MCP config and the server is identified as the GitHub MCP server, APM selects a token source and applies the target’s auth behavior described below.

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 selected 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 the manifest declares a nonempty string Authorization value (case-insensitive header name), that explicit value takes precedence over automatic GitHub authentication. Registry-provided headers alone do not disable automatic authentication. For Copilot and Cursor, this also applies to dictionary-shaped headers accepted from a custom registry: manifest overrides retain their priority. This compatibility does not certify the custom response against an upstream registry schema. Environment references are translated according to the target’s interpolation rules. This MCP selection is environment-only: it does not use repository authentication’s per-org variables or credential helpers.

For a target that supports runtime environment substitution, automatic GitHub auth writes a target-native reference to the selected variable (for example, ${GITHUB_TOKEN} or ${env:GITHUB_TOKEN}); the resolved credential value is not written into the generated config. Literal-only targets keep their existing automatic-token behavior. If none of the listed variables is set, no automatic header is added.

Reinstalling the same declaration does not automatically repair credentials already written to runtime config. Follow Repairing existing credentials to preserve custom settings and rotate exposed credentials. For other authenticated remote servers, set headers explicitly with --header Authorization="Bearer ${MY_TOKEN}".

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.

First replace any authored static credential in apm.yml with an environment reference. Otherwise regenerating the entry writes that static value again. Follow the target’s interpolation rules when making variables available and editing its config.

For Cursor, open the config for the scope you installed into (.cursor/mcp.json for a project, ~/.cursor/mcp.json with -g; see What apm install writes to disk). Replace only the affected header or env values with ${env:NAME} references. Preserve the authorization scheme, for example Bearer ${env:GITHUB_TOKEN}, and make the variable available to Cursor. Keep all other fields and servers, and inspect the diff. An ordinary reinstall of the same declaration does not perform this repair.

If you instead regenerate an entry, save its custom fields first, remove only that server entry, reinstall, and restore those fields without restoring the old credential. Do not delete the whole config file. Rotate any credential exposed in a committed or shared config; editing the file does not revoke it.

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.