MCP Servers¶
See also: Skills and Hooks (the guidance mechanism
MCP rules reuses), Repositories (a repository can
declare MCP presets alongside skills), and Projects (MCP
installs use the same per-project targets as skills).
Overview¶
An MCP (Model Context Protocol) server is an external tool/context provider an agent can call into. SkillKeeper manages MCP server presets and installs them into a project's agents, alongside skills.
A preset comes from one of two origins:
- Manual - defined by the user in SkillKeeper's own configuration
(
config.yaml, sectionmcp). Editable at any time; editing a preset can later update every install made from it. - Repo - declared in a repository's
mcp.yml/mcp.yamlfile. Read-only: it can only change by editing the file in the repository and syncing. Updated implicitly whenever the repository is fetched.
Installing a preset renders it into the native MCP configuration format of
one or more agents (Claude, Cursor, Copilot, OpenCode, Codex), tracks the
install in a small ledger, and - if the preset carries rules - writes
guidance into the agent's guidance file the same way skill guidance is
written.
The mcp.yml / mcp.yaml format¶
A repository may declare MCP server presets in an mcp.yml (or mcp.yaml)
file:
- at the repository root - these presets have no group, and
- inside any skill-group directory - the directory name becomes the preset's group.
If a directory has both mcp.yml and mcp.yaml, mcp.yml is read and
mcp.yaml is ignored entirely, even if mcp.yml fails to parse (this is a
precedence rule, not a fallback). A file that fails to parse is skipped with
a warning; it never fails the rest of the sync.
Schema:
version: 1
servers:
- name: <string> # required
type: stdio | http | sse # required
url: <string> # required for http/sse
headers: # optional, http/sse
<header-name>: <value>
command: <string> # required for stdio
args: [<string>, ...] # optional, stdio
env: # optional, stdio
<VAR_NAME>: <value>
rules: <string> # optional guidance body
stdio requires command; http and sse require url. A server missing
the field its transport needs fails validation for the whole file.
Example: repository root¶
# mcp.yml at the repository root
version: 1
servers:
- name: docs-http
type: http
url: "https://{host}/mcp"
headers:
Authorization: "Bearer {token}"
rules: |
Prefer the docs-http MCP for {host} lookups.
Cite the source path for every answer.
- name: local-fs
type: stdio
command: npx
args: ["-y", "@acme/fs", "--root", "{root}"]
env:
FS_TOKEN: "{token}"
Example: inside a skill group¶
# tooling/mcp.yml -- group "tooling"
version: 1
servers:
- name: tooling-sse
type: sse
url: "https://{host}/tooling/sse"
headers:
X-Api-Key: "{api_key}"
Both files exist for real in the
skillkeeper-test-repo
fixture repository:
mcp.yml
declares five ungrouped presets covering all three transports, with and without
parameters, headers, and rules;
tooling/mcp.yml
declares one preset in the tooling group.
Parameters¶
A server definition may reference {name} placeholders in url, header
values, command, args, env values, and rules. Parameters are not
declared anywhere - they are discovered by scanning every one of those
fields for {[A-Za-z0-9_]+} and collecting the distinct names. The same
name used in more than one field is still a single parameter.
Placeholder syntax is validated separately: an unclosed {, an empty {},
or a name containing a character outside [A-Za-z0-9_] is rejected, with
the offending position reported.
At install time, every parameter is presented for a value (plain text; no
typed or secret fields exist yet). The rendering step substitutes each
{name} with its value across all of the fields listed above, including
rules. Rendering fails if any referenced parameter has no value.
Example: the docs-http server above has two parameters, host and
token (host appears in both url and rules; it still counts once).
Install, update, and remove¶
Install¶
Installing a preset for one agent:
- Renders the parameter values into the definition.
- Allocates an instance name: the server's
name, snake_cased, with a_<n>suffix. Installing the same preset again produces a new instance (github_1, thengithub_2, and so on). If a name is already taken by a server SkillKeeper does not own, the counter skips it - an existing, unmanaged entry in the native config is never overwritten. - Writes the rendered definition into the agent's native MCP config file.
- If the definition carries
rules, renders and writes them into the agent's guidance file as a marked block (see "Rules" below). - Records the instance in
.skmcp.ymland its raw parameter values in.skmcp.params.yml. - On the first MCP install into a project, ensures the project's
.gitignoreexcludes both parameter files.
Update¶
An installed instance can be updated when the current source definition's content hash differs from the hash recorded at install time (see "Update detection" below). Updating an instance:
- Computes the new definition's parameters and compares them against the values already on file for that instance. Any newly-required parameter that has no stored value is missing.
- If any parameter is missing, its value must be supplied before the update proceeds; closing out of that prompt without supplying every missing value aborts the update - nothing changes.
- Removes the old instance and reinstalls the new definition under the same instance name, using the merged parameter values (existing values kept, missing ones now filled in).
- Refreshes the recorded hash in
.skmcp.yml.
Updating a repository preset that fans out to multiple installed instances (different projects, agents, or both) updates every instance whose hash is stale.
Remove¶
Removing an instance reverses install: the native server entry is dropped,
its guidance block (if any) is removed by its marker key, and its entries
are dropped from both .skmcp.yml and .skmcp.params.yml. Removal is
safe to call even when one side is already gone (missing native server,
guidance block, or ledger entry).
Update detection (hashing)¶
Each definition is hashed for identity comparison: a deterministic JSON
serialization of every field except name (so renaming a server in
mcp.yml is not a content change) with all object keys sorted recursively
(so key order in headers/env never affects the hash). The hash is
sha256:<hex digest>. Parameter values are never part of the hash -
they live only in .skmcp.params.yml - so filling in or changing a
parameter value is not, by itself, an "update."
Tracking files¶
Two files, kept per agent and scope, record what SkillKeeper has installed:
.skmcp.yml- the install ledger. One entry per installed instance:
schema: 1
servers:
- remote: git@github.com:acme/mcps.git # omitted for manual presets
group: tooling # omitted when at the repo root
local: <presetId> # present only for manual presets
source: docs-http # server name in mcp.yml/preset
name: docs_http_1 # assigned instance name
hash: sha256:... # hash of the raw def at install
The ledger identifies a preset by reference, not by a generated
install ID: (remote, group, source) for a repo preset, (local,
source) for a manual one. This is what lets update matching survive a
reinstall and lets the same file double as the ownership record - it is
the only place that says which native config entries SkillKeeper owns, so
updates and removals act on exact instance names instead of touching
anything unrecognized.
.skmcp.params.yml- the sibling parameter-values file, keyed by instance name:
docs_http_1:
host: mcp.example.com
token: sk-...
This file holds raw secrets and is never meant to be committed. The first
MCP install into a project appends both .skmcp.params.yml and
.skmcp.params.yaml to the project's .gitignore (creating the file if
it does not exist, or appending only the lines that are missing under a
# SkillKeeper MCP parameter values comment).
Both files live at the root of the agent's skills destination for the relevant scope - the same root the skills engine already resolves for that agent:
| agent | project scope | global scope |
|---|---|---|
| claude | <project>/.claude/skills/ |
~/.claude/skills/ |
| cursor | <project>/.cursor/skills/ |
~/.cursor/skills/ |
| copilot | <project>/.github/copilot/skills/ |
~/.config/github-copilot/skills/ |
| opencode | <project>/.opencode/skills/ |
~/.config/opencode/skills/ |
| codex | (not used for MCP) | ~/.codex/skills/ |
Codex MCP installs always use the global location, regardless of which project the install was started from (see "Codex" below).
Per-agent native destinations¶
Each agent has its own native MCP config file and its own supported transports:
| agent | destination | scope | transports | container key |
|---|---|---|---|---|
| claude | <project>/.mcp.json |
project | stdio, http, sse | mcpServers |
| cursor | <project>/.cursor/mcp.json |
project | stdio, http, sse | mcpServers |
| copilot | <project>/.vscode/mcp.json |
project | stdio, http, sse | servers |
| opencode | <project>/opencode.json |
project | stdio, http, sse | mcp |
| codex | ~/.codex/config.toml |
global | stdio only | mcp_servers |
Writers only touch their own container key and the server entries they own; other keys and other servers already in the file are preserved. Output key order is sorted, so re-writing the same content is a no-op diff.
Codex differs from the other four agents in two ways:
- Global scope: an MCP install for Codex always writes to
~/.codex/config.toml,~/.codex/skills/.skmcp.yml, and~/.codex/skills/.skmcp.params.yml- never into the project - regardless of which project the install was started from. This is intentional and applies to the guidance target too (~/AGENTS.md): Codex has only a single global MCP config, so a Codex MCP server is shared across every project. Keeping its ledger, parameter values, instance-name allocation, and rules global is what keeps that one config coherent (for example, it lets instance names stay unique in the singleconfig.toml). Note this differs from Codex skill guidance, which is project-scoped (<project>/AGENTS.md) because skills are per-project files - MCP servers are not. - stdio only: Codex's native config cannot express
httporsseservers. Installing a non-stdio preset for Codex is skipped rather than attempted, and is reported back as a skipped install.
The Codex writer round-trips ~/.codex/config.toml through a TOML
parser/serializer. That preserves table structure and values but does
not preserve hand-written comments or formatting in the file - a
config.toml with comments loses them the first time SkillKeeper edits it.
opencode's native shape differs from the other JSON-based agents: a stdio
server becomes a local entry whose command array is the command
followed by its args, with env under the key environment; an http/sse
server becomes a remote entry (both transports use the same shape, since
opencode does not distinguish them at this level).
Rules (guidance)¶
A preset's rules field, if present, is installed into the target agent's
guidance file using the same mechanism as skill guidance: a marked block
<!-- SKILLKEEPER_START: <key> -->
... rendered rules body ...
<!-- SKILLKEEPER_END: <key> -->
where <key> is built from the preset's identity (its source repository
remote, or local:<presetId> for a manual preset) and the installed
instance name, so the block can be found and replaced or removed later even
if the source preset changes or disappears. The rules body is rendered
(parameters substituted) before being written, and any literal
SKILLKEEPER_START/SKILLKEEPER_END marker line inside the body is
stripped first, so it cannot be mistaken for a block boundary.
Guidance files are the same ones skills write to per agent: CLAUDE.md (or
.claude/CLAUDE.md) for Claude, AGENTS.md for Codex and OpenCode,
.github/copilot-instructions.md for Copilot, and .cursorrules (or
.cursor/rules/skillkeeper.mdc) for Cursor. For Codex specifically, since
its MCP installs are global, its guidance target is ~/AGENTS.md, not a
project file.
Removing an instance removes exactly its own marked block; other SkillKeeper-owned blocks in the same guidance file (from skills or other MCP instances) are left untouched.