Table of Contents
- Important Note
- MCP Server Configuration
- Workspace-Local MCP Servers
- Transport Types
- Stdio Servers
- HTTP (Streamable HTTP) Servers
- SSE Servers
- OAuth Authentication
- Example: Notion
- How it works
- If authentication hasn't been run yet
- Using a pre-existing client ID
- Using a static token instead of OAuth
- Secret Injection
- Managing MCP Servers from the CLI
- Adding a server
- Stdio (local subprocess)
- HTTP (Streamable HTTP)
- SSE (legacy HTTP+SSE)
- Transport inference
- Overwriting an existing server
- Listing servers
- Inspecting a server
- Removing a server
- Scope
- Automatic secret provisioning
- Flag reference
- Notes and caveats
- Default MCP Servers
- Interacting with MCP Servers
- Restricting MCP Tools (Tool Whitelists)
- The allowedTools Field
- Pattern Syntax
- Filter Layers
- Blocked Tools Behave as Nonexistent
- Inspecting the Effective Filter
- Adjusting Filters at Runtime
- Coyote Configuration
- Sandbox Compatibility
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.jsoninside thefunctionsdirectory. That location is still fully supported: if<config-dir>/mcp.jsondoes not exist but<config-dir>/functions/mcp.jsondoes, 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.jsonwins.
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:
.coyote/mcp.json— the native location.coyote/.mcp.json— Claude-style file name (leading dot) inside the workspace config directory.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
- Discovery: Coyote fetches
/.well-known/oauth-protected-resourceand/.well-known/oauth-authorization-serverto find the server's authorization and token endpoints (per RFC 9728 / RFC 8414). No manual endpoint configuration needed. - 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. Theoauth_client_idfield is only needed when DCR is unavailable. - PKCE authorization code flow: A localhost callback server is bound on an ephemeral port (or the port specified by
oauth.callbackPortif 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. - Token injection: On every connection to the server, Coyote loads the stored token (refreshing if expired) and injects
it as an
Authorization: Bearerheader. No changes tomcp.jsonrequired.
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 (noclientIdconfigured), 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 tostdio. - 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:
- Print
Value references vault secret {{ NAME }} which is not stored yet. - Prompt:
Add 'NAME' to the vault now?(default: yes) - 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-addare mutually exclusive. Combining them in one invocation is a parse error.- The trailing
-- <cmd>is only meaningful for--mcp-addwithstdiotransport. Passing it withhttporssetransport 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-addwrites the config; you still needcoyote --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 theiwecbinary. Pairs with the built-iniwe-knowledge-baseskill, 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 schemakind: "resource"returns the resource's catalog metadata (URI, MIME type, size)kind: "resource_template"returns the template and its variableskind: "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:
- 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).
- Genuinely binary content is decoded (up to a 50 MiB ceiling) and written to
<cache_dir>/mcp-resources/<server>/<sha256>.<ext>, with0600permissions 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
.promptREPL command invokes a prompt and submits the result as your chat input, and.list promptslists every prompt across enabled servers. - For the model: the
mcp_prompt_<server>meta-function takes apromptname and an optionalargumentsobject (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
capabilitiesline 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 asprompts (declared, list failed).toolsis 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 amapping_mcp_serversalias) 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 (aserver: []entry, or a bareserver: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-levelmcp_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:
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-mcpmixin that adds every remote serverurlfrom yourmcp.jsonto the sandbox's network allow list and declares your MCP secrets as sbx credentials, which are proxy-injected into request headers where possible, exposed asCOYOTE_SECRET_<NAME>env vars otherwise. Any number of secrets per server is supported, and yourmcp.jsonworks unmodified inside the sandbox. See Sandboxes: The generated coyote-mcp mixin. - The bundled
functions/sbx-mixin.yaml(installed viacoyote --install functions) allowlists what the built-inmcp.jsonneeds 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
argsare not auto-allowed (e.g.npx mcp-remote https://…). Coyote only derives egress from each server'surlfield; 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. Declareghcr.io,registry-1.docker.io, andauth.docker.io(or whichever registry you pull from) explicitly. The built-infunctions/sbx-mixin.yamlalready 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 viasbx cpbefore starting Coyote, or inject the secret as an env var via the Vault instead. - The
npx/uvxMCP 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.
