From bbbdc74fb68c95ca6d7a2f2c5c15bd97dac8ad9b Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 27 Aug 2026 16:05:43 -0600 Subject: [PATCH] docs: Added documentation for the new mcp allowlist feature --- Agents.md | 4 ++ Graph-Agents.md | 33 +++++++++++ MCP-Servers.md | 153 ++++++++++++++++++++++++++++++++++++++++++++++++ REPL.md | 4 +- Roles.md | 15 +++++ Sessions.md | 1 + Skills.md | 1 + 7 files changed, 210 insertions(+), 1 deletion(-) diff --git a/Agents.md b/Agents.md index 355b71e..495e171 100644 --- a/Agents.md +++ b/Agents.md @@ -68,6 +68,10 @@ version: 1 # Version of the agent # Function Calling Configuration mcp_servers: # Optional list of MCP servers that the agent utilizes - github # Corresponds to the name of an MCP server in the `/mcp.json` file +mcp_tools: # Optional per-server tool whitelists for the agent's MCP servers; a map of + github: # server name -> tool-name patterns (`*`/`?` globs, YAML list values only). + - get_* # See the MCP Servers wiki page, "Restricting MCP Tools (Tool Whitelists)" + - list_* global_tools: # Optional list of additional global tools to enable for the agent; i.e. not tools specific to the agent - web_search - fs diff --git a/Graph-Agents.md b/Graph-Agents.md index 4f9d41c..9e6b82a 100644 --- a/Graph-Agents.md +++ b/Graph-Agents.md @@ -71,6 +71,8 @@ global_tools: # global tools available to nodes - web_search_coyote.sh mcp_servers: # MCP servers available to nodes - pubmed-search +mcp_tools: # optional; per-server tool whitelists for those servers + pubmed-search: [search_*, get_*] # (YAML list values; see MCP-Servers "Restricting MCP Tools") skills_enabled: true # optional; master switch for skills in `llm` nodes enabled_skills: # optional; the *universe* of skills referenceable by any llm node - code-review @@ -117,6 +119,11 @@ nodes: set their own. `global_tools` and `mcp_servers` define the tool universe that an `llm` node's `tools:` whitelist selects from (a node with no `tools:` field gets none of them). +- **`mcp_tools`:** Optional per-server tool whitelists over the graph's + `mcp_servers` (a map of server name -> tool-name patterns). Applies to + every node; `llm` nodes can narrow it further with a node-level + `mcp_tools:` field (see + [The `mcp_tools` field](#the-mcp_tools-field-per-server-tool-patterns)). - **`variables`:** Same shape as a normal agent's `variables:` block. Each declared variable becomes available to script nodes as the env var `LLM_AGENT_VAR_`, exactly like bash tools called by normal @@ -461,6 +468,32 @@ whitelist is enforced against global tools, agent custom tools, and MCP alike. Each entry is validated at startup against the active agent's tool list; an unknown entry is a startup error. +### The `mcp_tools` field (per-server tool patterns) + +Where `tools:` selects *which servers* a node may use, an optional +`mcp_tools:` map narrows *which tools inside a server* the node may call: + +```yaml +review: + type: llm + prompt: "{{diff}}" + tools: [mcp:github] + mcp_tools: + github: [get_*, list_*] # read-only github for this node +``` + +- The node map is one more filter layer stacked on top of the graph-level + `mcp_tools:` (and every other active layer). Layers intersect, so a node + can only narrow what the graph already allows. See + [Restricting MCP Tools](MCP-Servers#restricting-mcp-tools-tool-whitelists) + for the full model. +- Values must be YAML lists (`graph.yaml` does not accept the + comma-separated string form). +- The validator rejects keys naming servers the graph doesn't enable: + `llm node 'mcp_tools' references MCP server '' not enabled by this + graph`. Keys may also be `mapping_mcp_servers` aliases. When the graph + uses `mcp_servers: [all]`, this check is skipped and any key is accepted. + ### Failure routing | Outcome | Behavior | diff --git a/MCP-Servers.md b/MCP-Servers.md index aaf2f14..8d65fbf 100644 --- a/MCP-Servers.md +++ b/MCP-Servers.md @@ -150,6 +150,7 @@ over stdin/stdout: | `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](#restricting-mcp-tools-tool-whitelists)) | ## HTTP (Streamable HTTP) Servers @@ -174,6 +175,7 @@ For remote MCP servers that support the Streamable HTTP transport: | `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](#restricting-mcp-tools-tool-whitelists)) | ## SSE Servers @@ -202,6 +204,7 @@ prefer `http` where the server supports it): | `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](#restricting-mcp-tools-tool-whitelists)) | **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 @@ -689,6 +692,147 @@ Coyote surfaces them in two places: 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: + +```json +{ + "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](Roles)'s metadata header | +| `agent` | `mcp_tools:` in the active [agent](Agents)'s configuration | +| `session` | `.set mcp_tools.` in the active [session](Sessions) | +| `skill` | `mcp_tools:` in a loaded [skill](Skills)'s frontmatter (one layer per loaded skill) | +| `node` | `mcp_tools:` on an `llm` node in a [graph agent](Graph-Agents) | + +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_` results, and both +`mcp_describe_` and `mcp_invoke_` 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 ` 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. +* ⚠ 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 ` 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.` 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](Graph-Agents)). +* Tab completion offers `mcp_tools.` for every configured server and alias. + # Coyote Configuration MCP servers, like tools, can be used in a handful of contexts: * Inside a session @@ -721,6 +865,11 @@ enabled_mcp_servers: null # Which MCP servers to enable by default. # 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 @@ -739,6 +888,8 @@ When you create a role, you have the following MCP-related configuration options ```yaml 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)`. @@ -752,6 +903,8 @@ When you create an agent, you have the following MCP-related configuration optio 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](#global-configuration). diff --git a/REPL.md b/REPL.md index 50f592a..5a278c6 100644 --- a/REPL.md +++ b/REPL.md @@ -372,6 +372,7 @@ The following settings can be adjusted at runtime: | `reasoning_effort` | string | Reasoning effort level for models that support it (e.g. `low`, `medium`, `high`); supported levels vary by model; `null` to reset to the model's default | | `enabled_tools` | string | Comma-separated list of enabled tools (e.g. `fs_ls,fs_cat` or `all`); the saved YAML config also accepts a list form | | `enabled_mcp_servers` | string | Comma-separated list of enabled MCP servers (e.g. `github,slack` or `all`); the saved YAML config also accepts a list form | +| `mcp_tools.` | string | Per-server MCP [tool whitelist](MCP-Servers#restricting-mcp-tools-tool-whitelists) patterns, comma-separated (e.g. `.set mcp_tools.github get_*,list_*`); `null` removes that server's entry, and bare `.set mcp_tools null` clears the current layer's whole map | | `enabled_skills` | string | Comma-separated list of enabled [skills](Skills) (e.g. `git-master,ai-slop-remover`); `null` clears the override | | `enabled_macros` | string | Comma-separated list of enabled [macros](Macros) at the global level (e.g. `standup,review-work`); `null` clears the list, re-enabling all macros | | `save_session` | boolean | Whether to auto-save sessions | @@ -457,7 +458,7 @@ The `.list` command lists the assets of a given kind, making them discoverable w | `.list skills` | List skills available in this context, with descriptions and a `(loaded)` marker for active skills | | `.list prompts` | List MCP prompts across all enabled servers (live listing), with each prompt's server, description, and arguments | | `.list tools` | List the tools that can be enabled/disabled via `.tool [enable\|disable] ` (excludes internal tools; in an agent context, lists the agent's tool pool) | -| `.list mcp-servers` | List the MCP servers that can be enabled/disabled via `.mcp [enable\|disable] ` (configured servers plus mapping aliases) | +| `.list mcp-servers` | List the MCP servers that can be enabled/disabled via `.mcp [enable\|disable] ` (configured servers plus mapping aliases); servers with an active [tool whitelist](MCP-Servers#restricting-mcp-tools-tool-whitelists) are tagged `[filtered]` | `.list skills` honors the same visibility and policy filters as the model-facing `skill__list` tool, so it shows exactly what the model can load in the current role/agent/session context. @@ -560,6 +561,7 @@ The following entities are supported: | `.info session` | Display information about the active session | | `.info agent` | Display information about the active agent | | `.info rag` | Display information about the active RAG | +| `.info mcp-server ` | Show a running MCP server's transport, capabilities, and [tool whitelist](MCP-Servers#restricting-mcp-tools-tool-whitelists) layers, with a per-tool allowed/hidden breakdown | | `.info tools` | List every tool that would be sent in the next chat completion request (composed across role, agent, skills, and MCP filters). Errors when `function_calling_support` is disabled. | | `.info todo` | Show the current todo list driving auto-continuation (goal, progress count, and per-task status). Only available when `auto_continue` is enabled. | diff --git a/Roles.md b/Roles.md index 2d24876..8de6d7a 100644 --- a/Roles.md +++ b/Roles.md @@ -62,6 +62,7 @@ The following table lists the available configuration settings and their default | `reasoning_effort` | `null` | Reasoning effort level for models that support configurable reasoning. Supported values vary by model (e.g. `low`, `medium`, `high`). See `models.yaml` for per-model levels | | `enabled_tools` | Global setting for `enabled_tools` | The tools that this role utilizes. Accepts either a YAML list or a comma-separated string | | `enabled_mcp_servers` | Global setting for `enabled_mcp_servers` | The MCP servers that this role utilizes. Accepts either a YAML list or a comma-separated string | +| `mcp_tools` | `null` (no role-level filter) | Per-server MCP tool whitelists. A YAML map of server name → tool-name patterns (`*`/`?` globs); each value accepts a YAML list or a comma-separated string, and a bare `null` value blocks all of that server's tools. See [Restricting MCP Tools](MCP-Servers#restricting-mcp-tools-tool-whitelists) | | `skills_enabled` | Global setting for `skills_enabled` | Master switch for [skills](Skills) under this role. Set to `false` to hide all skills | | `enabled_skills` | Global setting for `enabled_skills` | The [skills](Skills) this role activates. Accepts either a YAML list or a comma-separated string | | `auto_continue` | Global setting for `auto_continue` | Enable the [Todo System](TODO-System) auto-continuation for this role | @@ -70,6 +71,20 @@ The following table lists the available configuration settings and their default | `continuation_prompt` | Global setting for `continuation_prompt` | Custom prompt used when auto-continuing | | `prompt` | `null` | See [Prompt Types](#prompt-types) for detailed usage | +**Example: A role restricted to read-only GitHub access** +```markdown +--- +enabled_mcp_servers: github +mcp_tools: + github: get_*,list_*,search_* # a YAML list works too +--- +``` + +The `mcp_tools` map stacks with the other filter layers (`allowedTools` in `mcp.json`, session-level `.set` maps, +etc.) and can only narrow them. Note that sessions do **not** inherit the role's map: the role layer applies on its +own whenever the role is active, and session-level maps come only from `.set mcp_tools.`. See +[Restricting MCP Tools](MCP-Servers#restricting-mcp-tools-tool-whitelists) for the full model. + ## Instructions The instructions for a role is what you use to tell the model how you want it to behave. This typically consists of one or two sentences, but can be more. To see some examples, look at the [built-in roles](https://github.com/Dark-Alex-17/coyote/blob/main/assets/roles) to see how they are defined. diff --git a/Sessions.md b/Sessions.md index 863d643..42a5a9a 100644 --- a/Sessions.md +++ b/Sessions.md @@ -130,6 +130,7 @@ at runtime using the `.set` command or configured in the session's YAML file: | `reasoning_effort` | Global `reasoning_effort` value | Reasoning effort level for models that support it (e.g. `low`, `medium`, `high`). Only valid when the active model declares `reasoning_levels`. | | `enabled_tools` | Global `enabled_tools` value | Tools enabled for this session. Accepts a comma-separated string (e.g. `fs_ls,fs_cat`) or `all`. | | `enabled_mcp_servers` | Global `enabled_mcp_servers` value | MCP servers enabled for this session. Accepts a comma-separated string (e.g. `github,slack`) or `all`. | +| `mcp_tools` | None (not inherited from the role) | Per-server MCP tool whitelists for this session, set via `.set mcp_tools. ` and persisted in the session YAML. The role's `mcp_tools` map is **not** copied into the session; it applies as its own layer. See [Restricting MCP Tools](MCP-Servers#restricting-mcp-tools-tool-whitelists). | | `auto_continue` | Global `auto_continue` value | Enable the [Todo System](TODO-System) auto-continuation for this session. Overrides global and role settings. | | `max_auto_continues` | Global `max_auto_continues` value | Maximum number of automatic continuations before stopping. Overrides global and role settings. | | `inject_todo_instructions` | Global `inject_todo_instructions` value | Inject default todo tool usage instructions into the system prompt. Overrides global and role settings. | diff --git a/Skills.md b/Skills.md index 7363b1b..f5461ef 100644 --- a/Skills.md +++ b/Skills.md @@ -105,6 +105,7 @@ grants when loaded. All fields are optional. | `description` | empty | Short one-line description shown to the model when it lists available skills. Make it specific: this is what helps the model decide when to load. | | `enabled_tools` | none | Comma-separated tool names that become available while the skill is loaded. Union with the active role/agent/session's tools. Tools must exist in [visible tools](Tools). | | `enabled_mcp_servers` | none | MCP server names the skill needs. Accepts a YAML list (preferred) or a comma-separated string. Skills can reference servers from your `mcp.json`; those servers are auto-acquired on load and released on unload via reference counting. | +| `mcp_tools` | none | Per-server MCP tool whitelists applied while the skill is loaded, as one extra filter layer. A YAML map of server name -> tool-name patterns; each value accepts a YAML list or a comma-separated string, and a bare `null` value blocks all of that server's tools. Entries only apply to servers the skill enables. See [Restricting MCP Tools](MCP-Servers#restricting-mcp-tools-tool-whitelists). | | `auto_unload` | `false` | If `true`, the skill is automatically removed from the registry at the end of every turn where the model produced a final response (no more tool calls). | ## Body