25
MCP Servers
Alex Clarke edited this page 2026-08-28 12:20:10 -06:00

MCP servers are essentially APIs designed specifically for LLMs that work like a remote repository of tools for the model to access and extend its capabilities.

So think of it like this: Instead of having to write all your own custom tools to interact with different services, those services can expose their functionality through an MCP server.

Coyote has first-class support for MCP servers.

As mentioned in the Coyote Vault documentation, Coyote can inject sensitive configuration data into your MCP configuration file to ensure that secrets are not hard-coded.


Important Note

Be careful how many MCP servers you enable at one time, regardless of the context. When there is a significant number of configured MCP servers, enabling too many MCP servers may overwhelm the context length of a model, and quickly exceed token limits.

MCP Server Configuration

Coyote stores the user-scope MCP server configuration file, mcp.json, directly in the Coyote configuration directory (e.g. ~/.config/coyote/mcp.json on Linux). You can find the resolved location of the file Coyote is actually using with the following command:

coyote --info | grep mcp_config_file | awk '{print $2}'

Note

Historical location: older Coyote versions stored this file at functions/mcp.json inside the functions directory. That location is still fully supported: if <config-dir>/mcp.json does not exist but <config-dir>/functions/mcp.json does, Coyote keeps using the historical file. Existing installs continue to work unchanged, and there is no need to move the file. If both files exist, <config-dir>/mcp.json wins.

The syntax for the mcp.json file matches Claude Code's .mcp.json configuration format, with one small difference: the type field is always required in Coyote, even for stdio servers (Claude Code allows it to be omitted and infers stdio from the presence of a command). So any time you're looking to add a new server, look at its docs and find the Claude Code configuration example. You should be able to use the exact same configuration in your mcp.json file. Just make sure every entry has an explicit type.

Note: Coyote does not support Claude Code's "streamable-http" alias (use "http" instead) or envFile. For secrets, use Coyote Vault interpolation rather than Claude Code's ${VAR} shell-style expansion. OAuth-protected remote servers are supported natively (see OAuth Authentication below).

Every server entry must include a "type" field set to one of: "stdio", "http", or "sse".

Prefer the CLI over hand-editing? Skip to Managing MCP Servers from the CLI for --mcp-add, --mcp-list, --mcp-get, and --mcp-remove.

Running inside a Docker Sandbox? MCP servers often need extra network allowances beyond what the base kit provides. See Sandbox Compatibility at the bottom of this page for details and common gotchas.

Workspace-Local MCP Servers

In addition to the global mcp.json, Coyote automatically loads a workspace-local MCP config at startup. This lets you ship project-specific MCP servers alongside your code without touching your global configuration.

Coyote checks the following locations in order and loads the first file it finds:

  1. .coyote/mcp.json — the native location
  2. .coyote/.mcp.json — Claude-style file name (leading dot) inside the workspace config directory
  3. .mcp.json — project root; Claude Code's project-scope convention
<project-root>/
├── .coyote/
│   └── mcp.json       # same format as the global mcp.json (preferred)
└── .mcp.json          # Claude Code-compatible fallback

This means a repository that only ships a Claude Code .mcp.json works with Coyote out of the box. The .coyote/ directory name is also customizable via the COYOTE_WORKSPACE_CONFIG_DIR environment variable, so you can point Coyote at another tool's config directory (e.g. .cursor/, which holds an mcp.json) entirely.

The workspace file uses the exact same format as the global mcp.json, including Vault secret interpolation via {{SECRET_NAME}} syntax. Workspace server names shadow global ones on collision. This means that if both files define a server named my-db, the workspace version takes precedence.

When workspace MCP servers are loaded, Coyote prints a startup notice listing them:

Loading workspace MCP servers: my-db, project-search

Error handling: missing vault secrets and invalid server specs in the workspace file produce a warning and are skipped. They do not prevent Coyote from starting (unlike the global file, where missing secrets are a hard error).

Opting out

To disable workspace MCP loading for a session, pass --no-workspace-mcp:

coyote --no-workspace-mcp

To disable it permanently in your config:

no_workspace_mcp: true

Pairing workspace MCPs with workspace skills

Workspace MCP servers can be referenced in .coyote/skills/ skill frontmatter just like global servers:

---
description: Run project-specific database queries.
enabled_mcp_servers: my-db
---

See Workspace-Local Skills for details.

Transport Types

Coyote supports three MCP transport types:

Type Use Case
stdio Spawns a local subprocess and communicates over stdin/stdout
http Connects to a remote server via Streamable HTTP
sse Connects to a remote server via the legacy HTTP+SSE transport (deprecated in the MCP spec; prefer http where the server supports it)

Stdio Servers

Stdio is the standard transport for locally-installed MCP servers. Coyote spawns the process and communicates over stdin/stdout:

{
  "mcpServers": {
    "github": {
      "type": "stdio",
      "command": "docker",
      "args": ["run", "-i", "--rm", "ghcr.io/github/github-mcp-server"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_GITHUB_TOKEN"
      }
    }
  }
}
Field Required Description
type yes Must be "stdio"
command yes The executable to spawn
args no Arguments passed to the command
env no Environment variables for the subprocess
cwd no Working directory for the subprocess
allowedTools no Tool-name patterns the model may call (see Restricting MCP Tools)

HTTP (Streamable HTTP) Servers

For remote MCP servers that support the Streamable HTTP transport:

{
  "mcpServers": {
    "datadog": {
      "type": "http",
      "url": "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp"
    }
  }
}
Field Required Description
type yes Must be "http"
url yes The server endpoint URL
headers no Custom HTTP headers to include with every request
oauth.clientId no OAuth client ID. Omit to use Dynamic Client Registration (auto-registers on first .mcp auth)
oauth.callbackPort no Callback port for the OAuth redirect listener. Required when the server enforces a specific redirect URI port
oauth.redirectHost no Hostname used in the OAuth redirect URI. Defaults to 127.0.0.1. Set to localhost if the server's registered redirect URI uses that form instead (e.g. some Slack OAuth apps)
allowedTools no Tool-name patterns the model may call (see Restricting MCP Tools)

SSE Servers

For remote MCP servers that use the legacy HTTP+SSE transport (deprecated in the MCP spec; prefer http where the server supports it):

{
  "mcpServers": {
    "my-sse-server": {
      "type": "sse",
      "url": "http://127.0.0.1:64342/sse",
      "headers": {
        "Authorization": "Bearer my-token"
      }
    }
  }
}
Field Required Description
type yes Must be "sse"
url yes The server SSE endpoint URL
headers no Custom HTTP headers to include with every request
oauth.clientId no OAuth client ID. Omit to use Dynamic Client Registration (auto-registers on first .mcp auth)
oauth.callbackPort no Callback port for the OAuth redirect listener. Required when the server enforces a specific redirect URI port
oauth.redirectHost no Hostname used in the OAuth redirect URI. Defaults to 127.0.0.1. Set to localhost if the server's registered redirect URI uses that form instead (e.g. some Slack OAuth apps)
allowedTools no Tool-name patterns the model may call (see Restricting MCP Tools)

Note: Both http and sse types use the same underlying transport, which auto-negotiates the protocol with the server. The type field primarily serves as documentation of which protocol the server speaks. Neither type supports command, args, or cwd fields.

OAuth Authentication

Some remote MCP servers require OAuth 2.0 authentication (e.g. Notion, Jira). Coyote supports these natively, meaning no manual token management is required.

Example: Notion

{
  "mcpServers": {
    "notion": {
      "type": "http",
      "url": "https://mcp.notion.com/mcp"
    }
  }
}

That's it. Then in the REPL:

.mcp auth notion

Or from the command line:

coyote --auth-mcp notion

Your browser opens, you log into Notion, and the token is saved. On subsequent startups Coyote injects the token automatically. When the token expires it is refreshed silently.

How it works

  1. Discovery: Coyote fetches /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server to find the server's authorization and token endpoints (per RFC 9728 / RFC 8414). No manual endpoint configuration needed.
  2. Dynamic Client Registration (RFC 7591): If the server supports it (the Notion MCP server used in this example does), Coyote registers itself automatically and caches the client ID in ~/.cache/coyote/oauth/mcp_<name>_registration.json. The oauth_client_id field is only needed when DCR is unavailable.
  3. PKCE authorization code flow: A localhost callback server is bound on an ephemeral port (or the port specified by oauth.callbackPort if the server requires a fixed redirect URI). Your browser opens for login. The token is exchanged and stored in <cache_dir>/oauth/mcp_<name>_oauth_tokens.json.
  4. Token injection: On every connection to the server, Coyote loads the stored token (refreshing if expired) and injects it as an Authorization: Bearer header. No changes to mcp.json required.

If authentication hasn't been run yet

If Coyote tries to connect to an OAuth-protected server at startup and gets an auth challenge, it warns and skips the server rather than failing:

warn: MCP server 'notion' requires authentication. Run `.mcp auth notion` to authenticate.

Run .mcp auth notion (or coyote --auth-mcp notion) once to complete the flow, then restart Coyote.

Using a pre-existing client ID

If your organization pre-registers a client with the server (this is rare, as DCR handles this automatically for most servers):

{
  "mcpServers": {
    "my-server": {
      "type": "http",
      "url": "https://mcp.example.com/mcp",
      "oauth": {
        "clientId": "your-registered-client-id"
      }
    }
  }
}

When oauth.clientId is set, DCR is skipped and the provided ID is used directly.

Some servers (e.g. Slack) pre-register an OAuth app with a specific redirect URI and require the callback to land on a fixed port. Use oauth.callbackPort in that case:

{
  "mcpServers": {
    "slack": {
      "type": "http",
      "url": "https://mcp.slack.com/mcp",
      "oauth": {
        "clientId": "1601185624273.8899143856786",
        "callbackPort": 3118
      }
    }
  }
}

Coyote's redirect URI uses 127.0.0.1 by default (e.g. http://127.0.0.1:3118/callback). If the OAuth app you registered uses localhost instead, set oauth.redirectHost to match:

{
  "mcpServers": {
    "notion": {
      "type": "http",
      "url": "https://mcp.slack.com/mcp",
      "oauth": {
        "clientId": "your-slack-client-id",
        "redirectHost": "localhost"
      }
    }
  }
}

Note: This only applies when using a pre-registered clientId. When DCR is used (no clientId configured), Coyote registers its own redirect URI so the host format never mismatches.

Using a static token instead of OAuth

If the server issues long-lived tokens (e.g. Notion internal integrations), you can skip the OAuth flow entirely and use a static Authorization header:

{
  "mcpServers": {
    "notion": {
      "type": "http",
      "url": "https://mcp.notion.com/mcp",
      "headers": {
        "Authorization": "Bearer <your-notion-integration-token>"
      }
    }
  }
}

Use Coyote Vault to avoid storing the token in plaintext:

{
  "headers": {
    "Authorization": "Bearer {{notion_token}}"
  }
}

Secret Injection

As mentioned in the Coyote Vault documentation, you can use Coyote Vault to inject secrets into your MCP configuration file.

In fact, this is why you need to set up your vault before using Coyote at all: the built-in MCP configuration requires you set up some secrets to use it.

For more information about how to set up your vault and inject secrets, please refer to the Coyote Vault documentation.

Managing MCP Servers from the CLI

Coyote provides CLI flags to add, list, inspect, and remove MCP servers without hand-editing mcp.json. The flag surface mirrors Claude Code's claude mcp add so muscle memory transfers over directly.

Adding a server

Stdio (local subprocess)

Use trailing -- to separate Coyote's flags from the server command:

coyote --mcp-add filesystem -- npx -y @modelcontextprotocol/server-filesystem /path/to/dir

With environment variables:

coyote --mcp-add github --env GITHUB_TOKEN={{GITHUB_TOKEN}} \
  -- docker run -i --rm ghcr.io/github/github-mcp-server

--env is repeatable (--env KEY1=VAL1 --env KEY2=VAL2). Any value wrapped in {{NAME}} is treated as a Vault reference: if the secret is not already stored, Coyote will prompt you to add it interactively (see Automatic secret provisioning below).

HTTP (Streamable HTTP)

coyote --mcp-add notion --transport http --url https://mcp.notion.com/mcp

With headers for static-token authentication:

coyote --mcp-add datadog --transport http \
  --url https://mcp.datadoghq.com/api/unstable/mcp-server/mcp \
  --header "Authorization: Bearer {{DATADOG_TOKEN}}"

--header is repeatable. For OAuth-protected servers, use --client-id, --callback-port, and --redirect-host (see OAuth Authentication for background on when each is needed):

coyote --mcp-add slack --transport http --url https://mcp.slack.com/mcp \
  --client-id 1601185624273.8899143856786 --callback-port 3118

SSE (legacy HTTP+SSE)

coyote --mcp-add legacy --transport sse --url http://127.0.0.1:64342/sse

Transport inference

You can usually omit --transport:

  • If a trailing -- command is present, the transport defaults to stdio.
  • Otherwise it defaults to http.

Pass --transport explicitly when adding an sse server or when you want to be pedantic.

Overwriting an existing server

By default, --mcp-add prompts before overwriting an entry with the same name. Pass --mcp-force to skip the prompt:

coyote --mcp-add github --mcp-force -- docker run -i --rm ghcr.io/github/github-mcp-server

Listing servers

coyote --mcp-list

Prints every server from both user and workspace scopes, one line per server with its transport and target. Restrict with --scope:

coyote --mcp-list --scope workspace

Inspecting a server

coyote --mcp-get github

Prints the JSON block for the named server. Coyote searches user scope first, then workspace scope. Use --scope to restrict:

coyote --mcp-get my-db --scope workspace

Removing a server

coyote --mcp-remove github

Prompts before deleting. Skip the prompt with --mcp-force:

coyote --mcp-remove github --mcp-force

Coyote removes the server from the first scope it's found in (user first, then workspace). Use --scope to target a specific file.

Scope

The --scope flag controls which file --mcp-add / --mcp-remove / --mcp-list / --mcp-get read from or write to:

Value File
user ~/.config/coyote/mcp.json (global; the default)
workspace .coyote/mcp.json (project-local; created if missing)

For workspace scope, Coyote reads from the first existing file in the precedence order documented in Workspace-Local MCP Servers, and writes to .coyote/mcp.json when creating a new file.

Automatic secret provisioning

Any string value passed to --env, --header, --url, --client-id, --client-secret, --cwd, or --redirect-host is scanned for {{NAME}} Vault references. For each token whose secret isn't already in your vault, Coyote will:

  1. Print Value references vault secret {{ NAME }} which is not stored yet.
  2. Prompt: Add 'NAME' to the vault now? (default: yes)
  3. On confirmation, run the standard vault add-secret flow with masked input for the value

The literal {{NAME}} token is what gets written to mcp.json. Interpolation happens at load time. This means you can safely commit a workspace mcp.json without exposing secret values.

If you decline a prompt, the add is aborted and no config changes are written.

Flag reference

Flag Purpose
--mcp-add <NAME> Add a server
--mcp-remove <NAME> Remove a server
--mcp-list List all servers
--mcp-get <NAME> Show one server's config as JSON
--mcp-force Skip overwrite / removal confirmation
--transport <stdio|http|sse> Explicit transport (inferred if omitted)
--scope <user|workspace> Config file to read/write (default: user)
--url <URL> Endpoint for http / sse servers
--env KEY=VALUE Env var for stdio (repeatable)
--header "Name: Value" HTTP header for http / sse (repeatable)
--cwd <PATH> Working directory for stdio
--client-id <ID> OAuth client ID for http / sse
--client-secret <SECRET> OAuth client secret (use {{NAME}} to reference a vault secret)
--callback-port <PORT> OAuth callback port
--redirect-host <HOST> OAuth redirect host (127.0.0.1 by default)
-- <cmd> [args...] Stdio command + args (everything after -- is passed to the server verbatim)

Notes and caveats

  • --mcp-list / --mcp-get / --mcp-remove / --mcp-add are mutually exclusive. Combining them in one invocation is a parse error.
  • The trailing -- <cmd> is only meaningful for --mcp-add with stdio transport. Passing it with http or sse transport is an error.
  • Existing behavior change: because Coyote now supports -- as a trailing separator, prompt text that literally starts with - needs to be quoted or preceded by --. This affects only unquoted hyphen-prefixed prompts, which are rare in practice.
  • OAuth completion is separate from adding. --mcp-add writes the config; you still need coyote --auth-mcp <NAME> (or .mcp auth <NAME> in the REPL) to complete the OAuth authorization flow the first time.

Default MCP Servers

Coyote ships with an mcp.json file that includes some useful MCP servers:

  • atlassian - Interact with and manage Atlassian tools like Confluence and Jira.
  • github - Interact with GitHub repositories, issues, pull requests, and more.
  • docker - Manage your local Docker containers with natural language
  • ddg-search - Perform web searches with the DuckDuckGo search engine
  • iwe - Navigate and manage large markdown knowledgebases (plan repos, specs, notes) as a structured graph. The server is rooted at the directory Coyote is launched from (--project .), runs fully locally (no network access needed), and requires the iwec binary. Pairs with the built-in iwe-knowledge-base skill, which enables this server on load.

The mcp.json file is created from a bundled template on first run. It is your own configuration to edit freely. To pick up new default servers added in a Coyote update, run coyote --install mcp-config (or .install mcp-config in the REPL). This merges the bundled template into your existing configuration: only servers not already present in your file are added; your existing servers and any custom secret references are left untouched.

Interacting with MCP Servers

Coyote does not flood the model's context with a separate function declaration for every tool an MCP server exposes. Instead, each enabled server contributes a small set of meta-functions the model uses to discover and invoke the server's capabilities on demand:

Meta-function Purpose
mcp_search_<server> Keyword search over the server's catalog of tools, resources, resource templates, and prompts
mcp_describe_<server> Fetch the full schema or metadata for exactly one catalog item
mcp_invoke_<server> Call a tool on the server
mcp_read_<server> Read a resource (or expand a resource template) from the server
mcp_prompt_<server> Fetch a server-defined prompt, rendered with the given arguments

This keeps the per-server context cost small and constant no matter how large the server is: the model searches for what it needs, describes the matching item to get its exact schema, and then invokes, reads, or fetches it.

The Unified Catalog

mcp_search_<server> searches a single catalog spanning everything the server advertises: tools, resources, resource templates, and prompts. Every search result carries a kind field (tool, resource, resource_template, or prompt) so the model knows whether to follow up with mcp_invoke, mcp_read, or mcp_prompt. Listings follow server-side pagination, so large servers are cataloged completely.

mcp_describe_<server> accepts the same kind as an optional parameter (default: tool). The tool parameter carries the identifier for every kind: a tool name, a resource URI, a template's URI template, or a prompt name.

  • kind: "tool" returns the tool's full invocation schema
  • kind: "resource" returns the resource's catalog metadata (URI, MIME type, size)
  • kind: "resource_template" returns the template and its variables
  • kind: "prompt" returns the prompt's name, description, and arguments

Servers can annotate resources with an intended audience (user and/or assistant). Per the MCP spec this is advisory metadata, not access control, and Coyote surfaces it on both search results and read results so the model (and you) can see who the content was meant for.

Capability Gating

Servers advertise which capabilities they support (tools, resources, prompts) during the connection handshake, and Coyote only emits the meta-functions that make sense for each server:

Meta-function Emitted when...
mcp_search Always (the catalog degrades per kind, so search works with whatever the server supports)
mcp_describe Always (same reason)
mcp_invoke The server advertises tools, or the handshake info is unavailable (fail-open: a handshake hiccup never strips a working server's tools)
mcp_read The server advertises resources
mcp_prompt The server advertises prompts

So a resources-only server still gets mcp_search, mcp_describe, and mcp_read, and never wastes context on an mcp_invoke or mcp_prompt that would always error.

Note that declaring a capability and serving items for it are different things: some server SDKs advertise prompts/resources unconditionally even when zero prompts or resources are actually registered. Gating (and prompt completion) always reflects what the server actually serves, and .info mcp-server <server> live-probes each declared capability so its capabilities line only lists what the server actually offers.

Reading Resources

mcp_read_<server> reads a resource by URI, or expands a resource template with variable values:

Parameter Required Description
uri yes The resource URI, or a resource template with {var} placeholders
arguments no Template variable values. Only RFC 6570 Level 1 simple substitution is supported; values are percent-encoded
pattern no A regex applied to text content: only matching lines are returned, grep-style, with 2 lines of context and line-number prefixes
offset no Byte offset for paging text (default: 0). When pattern is set, offsets refer to the filtered stream, not the raw resource
max_bytes no Maximum text bytes to return (default: 51200, clamped to 204800)

Text content comes back as a structured result with paging metadata:

{
  "uri": "file:///var/log/app.log",
  "mime_type": "text/plain",
  "text": "...",
  "truncated": true,
  "total_bytes": 1048576,
  "next_offset": 51200,
  "note": "Content truncated; re-call with offset=51200 to continue (max_bytes is clamped to 204800)"
}

The pattern filter is applied before slicing, so paging walks the filtered stream: total_bytes and next_offset describe the filtered content, and for a multi-megabyte log resource, "lines matching ERROR" is one call instead of dozens of pages. When a read returns multiple content items, the paging parameters apply per text item and the whole response is additionally capped at 204800 bytes; items beyond the cap are replaced with a marker naming how many were omitted.

Binary Content

Binary content is never inlined into model context. Instead:

  1. Coyote first attempts a UTF-8 decode of the blob. If it decodes cleanly, it is treated as text and paged inline regardless of the server's claimed MIME type (servers mislabel text constantly).
  2. Genuinely binary content is decoded (up to a 50 MiB ceiling) and written to <cache_dir>/mcp-resources/<server>/<sha256>.<ext>, with 0600 permissions on POSIX systems. Writes land in a temp file and are renamed into place, so a visible spill file is always complete, even with concurrent readers. The read returns a self-describing metadata object instead of the bytes:
{
  "spilled": true,
  "path": "/home/you/.cache/coyote/mcp-resources/github/3b0c44...98fb.png",
  "uri": "resource://github/chart",
  "mime_type": "image/png",
  "sniffed": false,
  "size_bytes": 204812,
  "sha256": "3b0c44...98fb"
}

The path is actionable by you, by execute_command, by the fs_* tools, and by sub-agents, which is more useful than base64 the model cannot act on anyway. The spill directory is bounded: when the mcp-resources/ tree exceeds 512 MiB, the oldest files are evicted best-effort. Files are content-addressed by SHA-256, so re-reading the same content regenerates the identical path. Spilled files are untrusted input: Coyote never executes or auto-opens them.

Bounded Tool Results

The same content policy bounds mcp_invoke tool results before they reach the model:

  • Text content larger than 204800 bytes is sliced at a UTF-8 boundary with a truncation note asking the model to re-call with narrower arguments.
  • Image, audio, and embedded binary content is routed through the same spill pipeline as resource reads: written to disk, with the metadata object returned in its place.
  • Oversized structured content (beyond the same 204800-byte ceiling once serialized) is replaced with a truncation marker.
  • Server-supplied metadata strings (URIs, MIME types, and similar) are clamped to 4096 bytes.

In-bounds results pass through byte-identical, so well-behaved servers are unaffected.

Prompts

Servers that advertise the prompts capability expose named, parameterized message templates rendered server-side. Coyote surfaces them in two places:

  • For you: the .prompt REPL command invokes a prompt and submits the result as your chat input, and .list prompts lists every prompt across enabled servers.
  • For the model: the mcp_prompt_<server> meta-function takes a prompt name and an optional arguments object (string values only; prompt arguments have no schemas per the MCP spec) and returns the rendered prompt text as the tool result. Multi-message prompts are flattened into a single block with [user] / [assistant] labels, and missing required arguments produce an error listing them.

Restricting MCP Tools (Tool Whitelists)

Enabling an MCP server exposes everything it advertises. Sometimes that's more than you want: perhaps the model should read from GitHub but never mutate it, or a review role should only ever call get_* tools. Tool whitelists restrict which of a server's tools the model can call, per server, at every configuration level.

The allowedTools Field

Every server entry in mcp.json accepts an optional allowedTools field: a list of tool-name patterns. When present, the model can only call tools matching at least one pattern. When absent, all tools are allowed:

{
  "mcpServers": {
    "github": {
      "type": "http",
      "url": "https://api.githubcopilot.com/mcp/",
      "allowedTools": ["get_*", "list_*", "search_*", "create_pull_request"]
    }
  }
}

Warning

An empty list is not "no restriction": "allowedTools": [] blocks every tool on the server, and Coyote warns at startup (MCP server 'github' has an empty "allowedTools" list, so none of its tools will be callable). Omit the field entirely to allow everything.

Pattern Syntax

Patterns are simple globs matched against the entire tool name, case-sensitively:

Wildcard Matches
* Any run of characters (including none)
? Exactly one character

Everything else is literal. get_* matches get_issue but not paginated_get, and a bare get matches only a tool named exactly get. An invalid pattern logs a warning and matches nothing.

Filter Layers

allowedTools is only the outermost layer. Every configuration level can carry its own per-server whitelist map (the mcp_tools setting), and all active layers apply simultaneously:

Layer Where it's configured
global allowedTools on the server entry in mcp.json
config mcp_tools: in the global configuration file
role mcp_tools: in the active role's metadata header
agent mcp_tools: in the active agent's configuration
session .set mcp_tools.<server> in the active session
skill mcp_tools: in a loaded skill's frontmatter (one layer per loaded skill)
node mcp_tools: on an llm node in a graph agent

Layers intersect: a tool is callable only if it matches at least one pattern in every active layer. A layer can therefore only narrow what the other layers allow; e.g. a session can never grant a tool the role's map blocked. Two more rules complete the picture:

  • A level that doesn't mention a server contributes no layer for that server (no restriction from that level).
  • A level that maps a server to an empty pattern list blocks all of that server's tools.

Map keys are server names from mcp.json; mapping_mcp_servers aliases work anywhere a server name does and expand to a layer on each server they map to.

Blocked Tools Behave as Nonexistent

A blocked tool is invisible to the model: it never appears in mcp_search_<server> results, and both mcp_describe_<server> and mcp_invoke_<server> fail with the same error a genuinely missing tool produces:

delete_repo not found in github MCP server catalog

The model cannot distinguish a filtered tool from one that doesn't exist, so it won't waste turns trying to work around the policy. Only tools are filtered; resources, resource templates, and prompts are unaffected. The filter also constrains the model, not you: user-facing surfaces like .list mcp-servers and .prompt completion still show everything.

Inspecting the Effective Filter

The .info mcp-server <server> REPL command shows a running server's transport, capabilities, active filter layers, and the per-tool verdict:

> .info mcp-server github
server         github (http, connected)
capabilities   tools
filter layers  global (mcp.json):  get_* | list_* | search_* | create_pull_request
               role (reviewer):    get_* | search_* | bogus_*

tools (2 allowed / 4 total)
  ✗ create_pull_request  hidden by role layer
  ✓ get_issue            get_* (global) ∧ get_* (role)
  ✗ list_issues          hidden by role layer
  ✓ search_code          search_* (global) ∧ search_* (role)
⚠ role pattern 'bogus_*' matches no allowed tools
  • ✓ rows show which pattern matched in each layer, joined with .
  • ✗ rows name the first layer that hid the tool.
  • The capabilities line live-probes prompts and resources: capabilities with served items show a count (e.g. prompts (2), resources (3), resources count includes templates), while a capability the server declares but serves nothing for is omitted entirely. Some SDKs advertise prompts/resources unconditionally, and a capability with zero items is not worth listing. If a probe errors, the capability can't be proven empty and is shown as prompts (declared, list failed). tools is never annotated; the tool table below it tells the full story.
  • ⚠ lines flag dead patterns: patterns that match none of the server's allowed tools (usually typos). Dead patterns are also logged as warnings when the server's catalog is fetched.
  • With no active filter, the header shows filter layers (none — all tools allowed) and every tool is a bare ✓.

The command errors for servers that are not configured, and for configured servers that aren't running (start the server with .mcp enable <server> first).

Additionally, .list mcp-servers tags every server (or alias) that has at least one active filter layer:

MCP servers:
  ✓ github [filtered]
  ✗ slack

Adjusting Filters at Runtime

The .set mcp_tools.<server> REPL command reads and writes the innermost active layer's map: the session if one is active, else the agent, else the role, else the global in-memory configuration (the same cascade as the .tool/.mcp toggles):

Command Effect
.set mcp_tools.github get_*,list_* Set this layer's patterns for github (comma-separated, no spaces)
.set mcp_tools.github null Remove the github entry from this layer's map (not deny-all)
.set mcp_tools null Clear this layer's whole map

Notes:

  • The server must be configured in mcp.json (or be a mapping_mcp_servers alias) and enabled in the current context; otherwise the command is rejected.
  • After setting patterns, Coyote checks them against the server's live tool list and prints a note for any pattern that matches nothing (e.g. Note: pattern 'get*_' matches no allowed tools on 'github'.). It's a typo guard, not an error; the pattern is still stored.
  • A deny-all empty list cannot be expressed through .set; that state only comes from file configuration (a server: [] entry, or a bare server: null value in YAML frontmatter).
  • Graph agents reject the command entirely: Graph agents define MCP tool filters per-node via 'mcp_tools:' in graph.yaml. Use node-level mcp_tools: instead (see Graph Agents).
  • Tab completion offers mcp_tools.<server> for every configured server and alias.

Coyote Configuration

MCP servers, like tools, can be used in a handful of contexts:

  • Inside a session
  • Inside a role
  • Inside an agent
  • Globally (i.e. outside a session, role, or agent)

Each of these has a different configuration and interaction with the global configuration.

Enabling a server in any of these contexts exposes its capability-gated meta-functions to the model; see Interacting with MCP Servers above.

*Note: The names of each MCP server referenced in the below configuration properties directly corresponds to the names given in the mcp.json configuration file. So if you change the name of an MCP server from slack to lucem-slack, then you need to also update your Coyote configuration accordingly.

Global Configuration

The global configuration is essentially what settings you want to have on by default when you just invoke coyote. (Don't worry about agents, roles, or sessions yet. We'll get to them in a bit).

The following settings are available in the global configuration for MCP servers:

mcp_server_support: true         # Enables or disables MCP server support (globally).
mapping_mcp_servers:             # Alias for an MCP server or set of servers
  git: github,gitmcp
enabled_mcp_servers: null        # Which MCP servers to enable by default.
                                 # Accepts either a YAML list or a comma-separated string. Examples:
                                 #   enabled_mcp_servers: github,slack
                                 #   enabled_mcp_servers:
                                 #     - github
                                 #     - slack
mcp_tools: null                  # Per-server tool whitelists; a map of server name to tool-name
                                 # patterns (YAML list or comma-separated string per server). See
                                 # "Restricting MCP Tools (Tool Whitelists)" above. Example:
                                 #   mcp_tools:
                                 #     github: get_*,list_*

A special note about enabled_mcp_servers: a user can set this to all (or include all in the list) to enable all configured MCP servers in the mcp.json configuration.

(See the Configuration Example file for an example global configuration with all options.)

When running in REPL-mode, the mcp_server_support and enabled_mcp_servers settings can be overridden using the .set command:

REPL set MCP servers

Role Configuration

When you create a role, you have the following MCP-related configuration options available to you:

enabled_mcp_servers:           # Which MCP servers the role uses. Accepts either a YAML list (as shown)
  - github                     # or a comma-separated string (e.g. `enabled_mcp_servers: github,slack`).
mcp_tools:                     # Optional per-server tool whitelists for those servers (each value accepts
  github: get_*,list_*         # a YAML list or a comma-separated string). See "Restricting MCP Tools" above.

The values for mapping_mcp_servers are inherited from the [global configuration](#global-configuration).

For more information about roles, refer to the Roles documentation.

Agent Configuration

When you create an agent, you have the following MCP-related configuration options available to you:

mcp_servers:                 # Which MCP servers the agent uses
  - github
  - docker
mcp_tools:                   # Optional per-server tool whitelists for those servers (values must be
  github: [get_*, list_*]    # YAML lists here). See "Restricting MCP Tools (Tool Whitelists)" above.

The values for mapping_mcp_servers are inherited from the global configuration.

For more information about agents, refer to the Agents documentation.

For a full example configuration for an agent, see the Agent Configuration Example file.

Sandbox Compatibility

If you run Coyote inside a Sandbox, the sandbox's network proxy denies all unlisted domains. Two mechanisms cover most MCP needs automatically:

  • Remote servers are auto-allowed. At launch, Coyote generates a coyote-mcp mixin that adds every remote server url from your mcp.json to the sandbox's network allow list and declares your MCP secrets as sbx credentials, which are proxy-injected into request headers where possible, exposed as COYOTE_SECRET_<NAME> env vars otherwise. Any number of secrets per server is supported, and your mcp.json works unmodified inside the sandbox. See Sandboxes: The generated coyote-mcp mixin.
  • The bundled functions/sbx-mixin.yaml (installed via coyote --install functions) allowlists what the built-in mcp.json needs beyond its URLs: the npm registry and Docker registries that npx/uvx/docker-based MCP servers pull from, the ddg-search endpoints, plus the github (api.githubcopilot.com) and atlassian (mcp.atlassian.com) MCP hosts.

You only need your own mixin for domains Coyote can't derive from mcp.json: OAuth authorization hosts, registries for custom containerized servers, or URLs hidden inside args (e.g. mcp-remote <url>). Brief example:

# ~/.config/coyote/sbx-mixin.yaml
schemaVersion: "2"
kind: mixin
name: my-mcp-domains
permissions:
  network:
    allow:
      - "auth.example-mcp.com"
      - "sso.example-corp.com"

See Sandboxes: Extending the Sandbox for the full discovery path table.

Sandbox caveats

  • Servers whose URL lives in args are not auto-allowed (e.g. npx mcp-remote https://…). Coyote only derives egress from each server's url field; add such domains to a user mixin.
  • Containerized MCP servers (docker run -i ...) require their image registry in the network allow list. The sandbox's nested Docker daemon needs to pull the image. Declare ghcr.io, registry-1.docker.io, and auth.docker.io (or whichever registry you pull from) explicitly. The built-in functions/sbx-mixin.yaml already covers these for the defaults.
  • OAuth-based remote MCP servers (e.g. Atlassian via mcp-remote) need the OAuth host's domain too. Failures here often look like indefinite hangs at "Authorizing..." with no useful error.
  • Host file mounts do not survive into the sandbox. Only the project workspace is mounted. If your MCP server reads secrets from a file on the host (e.g. ~/.config/<service>/credentials), copy it in via sbx cp before starting Coyote, or inject the secret as an env var via the Vault instead.
  • The npx/uvx MCP server install pulls from public registries on first run. That can take 30+ seconds inside a fresh sandbox while it downloads and compiles. This is normal. Subsequent attaches reuse the cached install.