Skip to content

MCP ​

Two related surfaces share this page: the mcps catalogue declares the external Model Context Protocol servers an agent can call (plus a per-server tool policy), and the mcp block configures hyprpilot's three in-tree servers. At launch, hyprpilot merges the catalogue and projects it onto the vendor's native MCP surface — you keep one catalogue, every vendor reads it.

The mcps catalogue ​

Each mcps entry carries either a file path or an inline mcp_servers map (exactly one — declaring both, or neither, is a load error). File paths follow the standard { "mcpServers": { … } } shape that Claude Code, Codex, and Cursor all read, so you can drop your existing ~/.claude.json straight in:

yaml
profiles:
  - id: engineer
    agent: claude-code
    mcps:
      - file: ~/.claude.json
      - file: ~/.config/hyprpilot/mcps/team.json
        ignore:
          - scratch-*
          - '*-internal'

Inside each file:

json
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${env:GITHUB_TOKEN}" }
    }
  }
}

Files iterate in order; a later file's server overrides an earlier one of the same name. A single malformed file warns and is skipped rather than aborting the launch. Transport is inferred by field presence (command → stdio, url → http/sse). Everything except the typed hyprpilot policy block stays opaque, so vendor-specific server fields pass through untouched.

Fields ​

FieldTypeDefaultWhat it does
filepath—An { "mcpServers": { … } } JSON file. Exactly one of file / mcp_servers.
mcp_serversmap—Inline server map, same shape as the file's mcpServers value.
includestring[] (globs)unsetWhen set, only server names matching a pattern are kept.
ignorestring[] (globs)[]Server names matching any pattern are dropped. Beats include on overlap.

Inline servers ​

If you want a one-off server without a file, declare mcp_servers on the entry directly:

yaml
mcps:
  - mcp_servers:
      hyprpilot-nvim:
        command: uvx
        args:
          - hyprpilot-nvim-mcp

Filtering servers ​

include and ignore are optional glob arrays per entry. ignore drops server names matching any pattern before they reach the agent. include is an allow-list: when set, only matching names survive, so a server added to the file later stays out until the list names it. A name matching both is dropped, and an empty or absent include applies no allow-list. Globs anchor against the full server name — work-* matches work-foo but not pre-work-foo.

yaml
mcps:
  - file: ~/.config/hyprpilot/mcps/personal.json
    include:
      - gitlab

Per-profile override ​

mcps on a profile wholesale-replaces the shared catalogue for that profile. mcps: [] means "no MCPs at all" — handy for a sandboxed read-only profile. To share one catalogue across every profile, put it in a patches entry instead of repeating it.

Reserved names

Each in-tree server's resolved name is reserved — by default hyprpilot, hyprpilot-skills, and hyprpilot-harness (see the mcp block). A configured server of that name is replaced by the injected entry, and renaming a server via mcp.<server>.name moves which name is reserved.

Tool policy ​

Each server entry takes an optional hyprpilot block for tool visibility and approval policy:

json
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
      "hyprpilot": {
        "includeTools": ["read_*", "list_*"],
        "excludeTools": ["delete_*"],
        "autoAcceptTools": ["read_*"],
        "autoRejectTools": ["delete_*"]
      }
    }
  }
}
FieldTypeDefaultWhat it does
includeToolsstring[] (globs)unsetVisibility allow-list. Unset = no allow-list; [] = deny all.
excludeToolsstring[] (globs)[]Visibility deny-list. Exclude beats include.
autoAcceptToolsstring[] (globs)inheritedApproval accept list. Falls back to mcp.autoAcceptTools.
autoRejectToolsstring[] (globs)inheritedApproval reject list. Reject beats accept.
  • Globs are server-relative — write read_*, not mcp__filesystem__read_*; the mcp__<server>__ prefix is implicit.
  • includeTools / excludeTools control visibility; autoAcceptTools / autoRejectTools control approval.
  • Servers with no per-server override inherit the mcp block's autoAcceptTools (default ['*']) / autoRejectTools.
  • Every other key on a server definition passes through to the vendor untouched.

The mcp block ​

hyprpilot ships four in-tree MCP servers. Each is its own subcommand, its own process, and its own catalogue entry, so each can be enabled, renamed, and given a tool policy independently:

ServerSubcommandDefault nameServesDefault
General toolshyprpilot mcp servehyprpilotopenenabled
Skillshyprpilot mcp skillshyprpilot-skillsskills (tools, resources, SEP-2640 skills/*) and promptsenabled
Agent harnesshyprpilot mcp harnesshyprpilot-harnesslist_profiles / spawn / session_*disabled
Passthroughhyprpilot mcp passthroughhyprpilot-passthroughthe tools mcp.passthrough.tools declaresdisabled

The mcp block gates and configures all four:

yaml
mcp:
  enabled: true # master gate over every in-tree server
  autoAcceptTools:
    - '*'
  autoRejectTools: []

  serve:
    enabled: true

  skills:
    dirs:
      - dir: ~/.config/hyprpilot/skills
      - dir: ~/.team/shared-skills
        ignore:
          - work-*
          - '*-experimental'
      - dir: /mnt/nfs/team-skills
        watch: false
    prompts:
      - dir: ~/.config/hyprpilot/prompts
    system_prompts: true

  harness:
    enabled: true
    max_sessions: 64
    max_live_sessions: 0
FieldTypeDefaultWhat it does
enabledbooltrueMaster gate. false auto-injects nothing, whatever the per-server blocks say.
serveobject—The general-tools server. See below.
skillsobject—The skills server. See below.
harnessobject—The agent-harness server. See below.
passthroughobject—The HTTP passthrough server. See below.
autoAcceptToolsstring[] (globs)['*']Default tool-approval accept list, copied onto servers with no per-server policy.
autoRejectToolsstring[] (globs)[]Default tool-approval reject list. Reject beats accept.

A profile's mcp field wholesale-replaces this block. autoAcceptTools / autoRejectTools are glob-validated at config load (like the ignore lists) — a malformed glob errors at startup with a field-path message instead of silently failing at match time.

Every per-server block accepts enabled, name, autoAcceptTools, and autoRejectTools. The default names in the table above are not compiled in — they are seeded as mcp.serve.name / mcp.skills.name / mcp.harness.name / mcp.passthrough.name in the shipped [[patches]], and the injector reads that field and nothing else, so a rename is a config edit. name is what the vendor prefixes tool calls with, so renaming the skills server to docs turns mcp__hyprpilot-skills__read_skill into mcp__docs__read_skill — anything that addresses a tool by name (a skill file, a system prompt) has to follow. The skill:// and hyprpilot:// resource URIs are fixed schemes and never change. A per-server autoAcceptTools overrides the block-level default rather than merging with it.

mcp.serve ​

The general-tools server — the surface for things that are neither a skills read nor an agent launch. open today. Stateless, so nothing to reload or reap.

mcp.skills ​

FieldTypeDefaultWhat it does
dirs{ dir, include?, ignore?, watch? }[]XDG rootSkill roots — trees of SKILL.md bundles, at any depth. Watched by default.
prompts{ dir, include?, ignore?, watch? }[][]Prompt directories — every *.md directly inside is served as an MCP prompt. Globs match the prompt name.
system_promptsbooltrueAlso serve the launched profile's own system_prompt files as prompts, so an edited one can be re-invoked live.

This server is also gated on having something to serve: with no skill and no loadable prompt, nothing is injected — and a profile with a system_prompt gets it even with no skills, because that prompt is served. See Skills & the hyprpilot MCP Server for what it exposes.

system_prompts is seeded snake_case; override it in the same spelling (see the warning under mcp.harness). The root defaults to ~/.config/hyprpilot/skills, seeded through an unscoped patches entry rather than a compiled default, so a user layer's patches extends the seed instead of replacing it.

dirs entries ​

FieldTypeDefaultWhat it does
dirpath—Skill root to scan. Missing roots warn and are skipped.
includestring[] (globs)unsetWhen set, only skill paths matching a pattern load from this root (* crosses /). An empty or absent list applies no allow-list.
ignorestring[] (globs)[]Skill paths matching any pattern are skipped. Beats include on overlap. First root wins on path collision.
watchbooltrueWatch this root and announce changes. Seeded on for the default root. Turn it off for a root on a filesystem that cannot deliver events (NFS, SSHFS, most FUSE) — those accept the watch and never fire, so edits there need reload.

mcp.harness ​

FieldTypeDefaultWhat it does
max_depthint1Levels of delegation allowed. See below.
max_sessionsint64Finished sessions retained before the oldest are evicted. 0 retains every one. See below.
max_live_sessionsint0Sessions allowed to run at once before spawn is refused. 0 allows any number. See below.
notify_on_completebooltruePush a completion event into the lead's context when a turn finishes. See Agent Harness.
includeProfilesstring[] (globs)unsetProfile ids this launch may delegate to. Unset applies no filter; [] means none. See below.
excludeProfilesstring[] (globs)[]Profile ids this launch may not delegate to. Beats includeProfiles on overlap.
mcpmcp blockunsetThe [mcp] block every delegate this harness spawns receives. See below.

max_depth, max_sessions, max_live_sessions and notify_on_complete are seeded by the compiled defaults, so your own config overrides them per field without restating the rest.

Write a seeded key the way the seed writes it

Config keys accept both snake_case and camelCase, but patches merge by key string before anything is typed. These four are seeded snake_case, so overriding one as maxDepth / maxSessions / maxLiveSessions / notifyOnComplete arrives as a second key and fails config load with duplicate field. The same holds for the other seeded multi-word keys, mcp.skills.system_prompts and mcp.passthrough.timeout_seconds. Every unseeded key is unaffected — there is nothing to collide with.

includeProfiles / excludeProfiles are the launcher's scope, distinct from the target's own profiles.harness opt-in. The two AND — a glob here narrows what is already nominated and can never promote a profile that never opted in. * crosses / (same globset semantics as $match.profile), so personal/* reaches personal/kilic/glm-5.2. Full treatment in Agent Harness → Scoping delegation per launch.

mcp.harness.max_depth ​

How many levels deep delegation may go. It is read in exactly one place — the mcp.harness block a gate is deciding on — and answers two questions with the same comparison, depth < max_depth:

  • whether a session running at that depth gets a harness server injected at all, and
  • whether a running sidecar at that depth may spawn.
ValueEffect
1Default. You delegate; your delegates do not. They get no harness server at all.
2Your delegates get a harness and may delegate once more. Theirs may not.
0Nothing anywhere gets a harness injected, and nothing may spawn.

The injection half is absolute — no enabled: true re-opens it, because a harness at the cap could only ever refuse spawn, so injecting one buys a long-lived process and seven tools that exist to error.

Raising it is a resource decision, not a security one. A max_live_sessions ceiling, where you set one at all, covers only its own sidecar's table, so an extra level lets N delegates each run N sessions — a fan-out no single ceiling catches and the lead that started the tree cannot see. There is deliberately no upper bound; the trade is yours to make.

mcp.harness.max_live_sessions ​

How many sessions this sidecar may have running at once. Past it, spawn is refused with a message naming the knob; session_kill on a finished or runaway session frees a slot. It bounds breadth where max_depth bounds recursion.

It is 0 — off — by default. How many agents are worth running at once is a property of your machine and your work, and hyprpilot has no way to guess it, so it refuses nothing until you say otherwise. Set a number on a shared or resource-tight host, where a profile's command being an arbitrary binary makes an unbounded fan-out something the host pays for. Leave it at 0 where an agent fanning out wide is exactly the point.

mcp.harness.max_sessions ​

How many finished sessions are retained before the oldest are evicted with their transcripts. 0 retains all of them.

Running sessions are outside this count, not merely spared by it — they hold a live model connection rather than history. Counting one would spend the retention budget on work still in flight, so with the concurrency ceiling off, enough concurrent agents would evict every transcript the cap exists to keep. session_list follows the same priority: running sessions lead, then most recent turn first.

Only distinct spawns grow the table — a conversation reuses its session however many turns it runs — so this bounds a long-lived sidecar's memory and temp directories without a tool you have to remember to call.

mcp.harness.mcp ​

An mcp block, same shape as the top-level one, applied to every delegate this harness spawns. It is folded per key onto whatever the delegate profile itself resolved: a key you set here wins, a key you leave unset inherits.

yaml
mcp:
  harness:
    enabled: true
    # what the agents I delegate to get
    mcp:
      serve:
        enabled: false
      skills:
        dirs:
          - dir: ~/.config/hyprpilot/skills/delegate

Distinct from includeProfiles, which answers which profiles you may reach. This answers what those agents can reach once running.

Setting harness.enabled: true here does not give a delegate a harness — the depth gate is checked first and does not consult enabled. Setting harness.max_depth here does, because the gate reads max_depth from the block it is deciding on, and the overlay is folded into that block. That is the supported way to hand one specific delegation an extra level without raising the cap for everything.

Because the fold is per key and not wholesale, a block naming only skills.enabled keeps the delegate's skills.dirs. Arrays replace rather than merge, so autoAcceptTools set here is the delegate's whole accept list.

Off by default, and that is a security property rather than a preference. A profile's command is an arbitrary binary, so anything that can call spawn executes commands as you. Turn it on deliberately — see Runtime → Agent Harness.

mcp.passthrough ​

FieldTypeDefaultWhat it does
enabledboolfalseInject the server. Also needs at least one tool.
tools{ name, description?, input_schema, url, body? }[]unsetThe tools served. Keyed by name, so a later layer retargets a tool.
timeout_secondsint300Per-request timeout, connect through the last byte. Seeded, so write it snake_case.
Tool fieldTypeWhat it does
namestringMCP tool name, 1-128 characters of [A-Za-z0-9_.-]. Unique within the block.
descriptionstringListed as written. This is how the agent learns what the tool is for.
input_schemaJSON objectListed as the tool's inputSchema. Advertised, not enforced.
urlhttp(s)://Every call POSTs here. Validated at config load.
bodyJSON objectStatic fields sent with every call; the call's arguments are laid over it and win on collision.
toml
[mcp.passthrough]
enabled = true

[[mcp.passthrough.tools]]
name = "decide"
description = "Ask the decision model a yes/no question."
url = "https://decide.internal.example/v1/decide"
body = { model = "decision-small", stream = false }
input_schema = { type = "object", properties = { question = { type = "string" } }, required = ["question"] }

Off by default, and like skills it is also gated on content: enabled with no tools injects nothing. The call contract and the argv caveat are in Runtime → HTTP Passthrough.

Vendor projection ​

The merged catalogue and policy are projected into each vendor's native shape at launch:

VendorServers viaPolicy via
claude-code--mcp-config <path> (0600 temp file)--allowedTools / --disallowedTools (mcp__server__tool)
codex-c mcp_servers.<name>.* overridesexact-name enabled_tools / disabled_tools / approval_mode
opencodeOPENCODE_CONFIG_CONTENT mcp.serversordered permissions rules in the same config (server_tool)
amp--mcp-config <path> (0600 temp file)not projected — set amp.tools.disable in the settings file

Codex does not support wildcard tool patterns in those fields, so wildcard patterns are skipped for Codex with a warning. Provider-native arguments you pass after -- (or env you set on the agent) suppress the generated equivalents.

Secrets in the vendor handoff ​

MCP server entries commonly carry secrets — bearer tokens in HTTP headers, API keys in stdio env. hyprpilot keeps expanded secret material out of the vendor's argv, because a process's argv is world-readable through /proc/<pid>/cmdline on Linux:

  • claude-code — the resolved MCP config (with ${VAR} header/env references already expanded) is written to a per-launch 0600 temp file and passed as --mcp-config <path>. The file is created owner-only from the start, so the secret never lands in argv. A launch that waits on the vendor — a headless --prompt / --file run, or a turn the agent harness drives — deletes the file as soon as the vendor exits. An interactive launch exec()s into the vendor, which leaves nothing behind to delete it, so each launch sweeps hyprpilot-mcp-*.json files older than 24 hours from the temp dir before writing its own.
  • codex — bearer tokens are projected as mcp_servers.<name>.bearer_token_env_var / env_http_headers references (the env var name, not its value), so codex resolves the secret from its own environment.
  • amp — the same 0600 temp file as claude-code, holding a bare { "<name>": { … } } server map. An amp --no-tui runner launched this way reads the file when it starts, and Amp restarts runners into CLI updates with the same flags, so a runner that outlives the 24-hour sweep restarts without the file — relaunch it through hyprpilot, or set amp.runner.autoUpdate.enabled to false.
  • opencode — the generated config rides the OPENCODE_CONFIG_CONTENT env var. Env is not world-readable like argv, but does inherit into child processes; this is the residual, lower-risk surface.