MCP servers in plugins
After this page you will be able to declare an MCP server in a plugin, predict the names its tools get, approve it, and explain why a plugin server carries no credentials. For a complete, tested plugin read An MCP server in a plugin. For the exact manifest fields read the Manifest reference.
What a plugin MCP server adds
Section titled “What a plugin MCP server adds”An MCP (Model Context Protocol) server gives the model tools that run outside
Muse Code. Declaring one
in a plugin means the person who installs the plugin gets the server without
editing settings.json, and gets it in every session on that machine,
including sessions served to SDK clients.
Once approved, the server starts with each new session and its tools are
registered before the first model request. The model sees them as ordinary
tools and calls them under the usual approval rules. The standalone form, a
server declared under mcpServers in settings.json or in a project
.mcp.json, is documented in MCP servers;
this page covers what changes when the server ships inside a plugin.
Two transports, two entry shapes
Section titled “Two transports, two entry shapes”Each entry in capabilities.mcpServers declares one server. The transport
field picks the shape.
| Field | stdio | http |
|---|---|---|
id |
Required. Capability id, unique among the plugin’s servers. | Same. |
transport |
"stdio", or omit it (stdio is the default). |
"http", required. |
command |
Required. Non-empty array of strings, run as argv with no shell. | Ignored. |
url |
Ignored. | Required, non-blank. A streamable HTTP endpoint. |
A stdio server is a program Muse Code starts and talks to over its stdin and stdout:
{ "id": "forecast", "transport": "stdio", "command": ["python3", "mcp/server.py"]}An HTTP server is a URL Muse Code connects to:
{ "id": "docs", "transport": "http", "url": "https://example.com/mcp"}Any other transport value is an error (invalid-manifest-schema), a stdio entry
without a command array is an error (missing-capability-command), and an
http entry without a url is an error. Fields such as env, args,
headers and cwd are not read from a plugin entry; the
What plugin servers cannot do today
section explains the consequences.
A stdio command element that spells a relative path, such as
mcp/server.py, must exist as a regular file inside the plugin directory.
Validation fails otherwise (missing-capability-path), and a path that
escapes the plugin directory is refused (unsafe-path).
On the wire, a stdio server may frame messages either as newline-delimited
JSON or with LSP-style Content-Length headers. Muse Code probes for
newline-delimited JSON first and falls back to Content-Length, so a server
that writes one JSON object per line and flushes after each one works without
configuration.
How tools are named
Section titled “How tools are named”Muse Code registers each plugin server under the name
plugin:<plugin-id>:<server-id> and builds every model-facing tool name from
that:
mcp__plugin_<pluginId>_<serverId>__<tool>Every character in the plugin id, the server id or the tool name that is not
an ASCII letter, digit or underscore becomes _. The colons in the server
name and the hyphen in a plugin id are replaced the same way. For the plugin
forecast-tools with server forecast exposing describe_forecast, the
registered name is:
mcp__plugin_forecast_tools_forecast__describe_forecastTwo consequences follow. Match on the registered name when you write approval
rules or tool filters, not on a name you rebuild from the ids. And choose ids
that stay distinct after sanitisation: get.forecast and get-forecast on
the same server collapse to the same name.
An SDK client’s per-session mcpServers map may not reuse a plugin server’s
id or its plugin:<pluginId>:<serverId> key.
Plugins in SDK sessions has the details of that
check.
Review and approval
Section titled “Review and approval”MCP servers are runtime capabilities. Installing a plugin never starts one.
muse plugins inspect <plugin-id> lists the server with a review status:
review_needed after install, trusted_enabled once you approve the
current definition, and modified when the package changed after your
decision. Only a trusted_enabled server starts, and it starts with the
next new session. Trust, review and scopes
describes all six statuses and what moves a capability between them.
Approve one server or the whole plugin:
muse plugins approve forecast-tools:mcp_server:forecastmuse plugins approve forecast-toolsmuse plugins reject records the opposite decision. Both accept the stable
id plugin:<plugin-id>:mcp_server:<server-id> as well.
An approval binds to a definition hash that covers the entry’s fields and the
package digest. Editing the server script, the manifest or any other file in
the plugin and running muse plugins update changes that hash, and the server
drops to modified with the diagnostic modified_definition_hash. Nothing
about the command or the URL can change behind an approval you gave.
Trust, review and scopes explains the same
mechanism for hooks and the other reviewed kinds.
When the server starts and stops
Section titled “When the server starts and stops”- A trusted, enabled server starts during session startup, before the first
model request, so the model’s tool list is complete from the first turn.
This applies to the terminal UI,
muse execand sessions served over the SDK. - Approvals, enables, disables, updates and removals take effect in the next new session. A running session keeps the servers it started with.
- Plugin servers are optional. A server that fails to start, or whose
initializehandshake takes longer than 30 seconds, does not stop the session; the session runs without that server’s tools. - A stdio server is terminated when the session ends. Write the server so it also exits on its own when stdin closes, and do not fork long-lived children that would outlive it.
- Each tool call gets 300 seconds. A slower call is reported to the model as a failure.
The process environment
Section titled “The process environment”A plugin stdio server starts with a cleared environment. It receives only these variables:
| Variable | Value |
|---|---|
| The pass-through allowlist | The same short list hooks get, HOME, PATH and a handful of user, locale and temporary-directory variables, copied from the Muse Code process when set; Hook events and payloads names each one. |
MUSE_PLUGIN_ID |
The plugin id, for example forecast-tools. |
MUSE_PLUGIN_ROOT |
The installed copy of the plugin directory, inside the plugin cache. |
MUSE_PLUGIN_DATA_DIR |
A per-plugin directory for state, <data dir>/plugins/data/<plugin-id>. |
PLUGIN_ROOT, CLAUDE_PLUGIN_ROOT |
Aliases of MUSE_PLUGIN_ROOT for scripts written for other hosts. |
PLUGIN_DATA, CLAUDE_PLUGIN_DATA |
Aliases of MUSE_PLUGIN_DATA_DIR. |
MUSE_SESSION_ID |
The id of the session that started the server, applied last. |
Nothing else reaches the server: no proxy settings, no language toolchain
variables, and never a provider API key. Hooks get the same variables except
MUSE_SESSION_ID, which only MCP children receive.
The MUSE_PLUGIN_DATA_DIR directory may not exist yet, so create it before
writing to it. Native hook and MCP commands get no placeholder substitution:
Muse Code rewrites exactly one argv element, the one that spells a relative
path declared in the manifest, to the installed copy, and your script should
read MUSE_PLUGIN_ROOT to find any other file. The server’s working directory
is the session’s working directory, not the plugin directory.
The server’s stderr is discarded. If you need a trace, write to a file under
MUSE_PLUGIN_DATA_DIR.
What plugin servers cannot do today
Section titled “What plugin servers cannot do today”Plugin MCP server entries have no env, headers or credentials, so
authenticated servers belong in settings.json instead
(MCP servers shows that entry). In detail:
- There is no
envmap, and the process environment is cleared, so a stdio server cannot receive a token through the environment. - There is no
headersmap for HTTP entries, andmuse mcp loginonly applies to servers named insettings.json, so an HTTP plugin server cannot send anAuthorizationheader or use OAuth. - There is no
cwdfield. The server runs in the session’s working directory. - There is no per-server
framing,tool_timeout_secorrequiredsetting. Plugin servers use auto-detected framing, the 300 second tool budget and optional mode.
A server that needs a secret can still read one from a file the user places
under HOME or under MUSE_PLUGIN_DATA_DIR, but nothing in the plugin format
provisions it. If your server needs a credential to be useful, document a
settings.json entry instead of packaging it in a plugin.
Tool approval rules
Section titled “Tool approval rules”Tools from an approved plugin server follow the ordinary MCP approval path.
Under the default on-request approval mode a call prompts the user unless a
permission rule already allows it. Permission rules match on the registered
mcp__plugin_... name. SDK clients see these prompts as ordinary approval
requests; Approvals covers the decide
flow.
One rule is specific to admitted local servers, and approved plugin servers
count as such. If the server’s tools/list response annotates a tool with
readOnlyHint: true, a call to that tool under on-request mode is admitted
without a prompt; the event log records the decision as
allow:mcp_read_only_hint. An explicit deny or prompt rule for the tool still
wins, and the shortcut applies only to on-request mode. The server declares
the hint about itself, so treat it as part of what you review before
approving: only set it on tools that change nothing, and be wary of a
third-party server that marks everything read-only.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause | Fix |
|---|---|---|
muse plugins validate reports missing-capability-command |
A stdio entry has no command array, or the array is empty. |
Add "command": ["<program>", ...]. |
muse plugins validate reports missing-capability-path |
A relative path in command does not point at a file in the plugin. |
Fix the path or add the file. |
inspect shows review_needed after install |
Servers never start until approved. | muse plugins approve <plugin-id>. |
inspect shows modified |
You changed the package after approving. | Re-read the change, approve again, start a new session. |
| The tool is missing from a session you already had open | Servers load at session start. | Start a new session. |
| The tool is missing from a new session and there is no error | The server failed to start or did not answer initialize within 30 seconds; plugin servers are optional, so the session continued. |
Run the server by hand with a pipe (the example page shows how), check that the program in command is on PATH, and log to a file under MUSE_PLUGIN_DATA_DIR. In the terminal UI /mcp lists the session’s servers under plugin:<plugin-id>:<server-id>. |
| The server cannot find a data file next to the script | Only the one declared relative path is rewritten, and the working directory is the session’s. | Build paths from MUSE_PLUGIN_ROOT. |
Writing to MUSE_PLUGIN_DATA_DIR fails |
The directory is not created for you. | Create it first. |
| The server needs a token and never gets it | Plugin entries carry no env, headers or credentials. |
Declare the server in settings.json instead. |
| The model calls the tool without asking | The tool declares readOnlyHint and the mode is on-request. |
Add a prompt or deny rule for the registered tool name. |
Next steps
Section titled “Next steps”- An MCP server in a plugin: the complete plugin behind the examples above, with every command and its output.
- Manifest: every field of an
mcpServersentry. - Trust, review and scopes: what an approval binds to and how it differs from install scope.
- Plugins in SDK sessions: how plugin MCP tool calls and approvals appear to an SDK client.
- The muse plugins command:
inspect,approveandrejectin full.