MCP server
Instead of running an agent loop, Fisk AI can serve a Fisk application’s commands over the Model Context Protocol so another client, such as Claude Desktop or Claude Code, calls them directly as tools.
The tool set, the input schemas, and the tag rules are the same ones the agent uses. Only the caller changes: where the agent drives the tools with an LLM against a prompt, an MCP server hands the same tools to whatever client connects and lets it decide when to call them.
Note
Serving over MCP is opt-in. The configuration must carry an expose.agent.mcp block, otherwise fisk mcp refuses
to start.
Starting a server
Serving over MCP needs less configuration than an agent: there is no agent loop, so system_prompt and llm.model are
not used. A minimal config needs only the application to introspect, a tool selection, and the expose.agent.mcp block:
Start the server with the mcp command:
The transport is HTTP, the streamable MCP transport. The port is taken from --port (or FISK_AI_MCP_PORT); if unset,
from expose.agent.mcp.port in the config; otherwise it defaults to 8080. All progress and logging go to stderr.
Use fisk info to preview which tools a configuration exposes before starting the server:
Connecting a client
Wire an MCP client to the server by pointing it at the running URL. For Claude Code:
A client that takes a JSON server map uses:
Configuration
MCP mode uses only the parts of the configuration that describe the application and the tool set. An
identity becomes the MCP server name, defaulting to fisk when unset; system_prompt, llm.model, and the
agent-only harness settings are ignored.
| Field | Description |
|---|---|
application_path | path to the Fisk application binary to introspect and serve; optional, omit it to serve only the enabled built-ins |
expose.agent.mcp | the opt-in block that enables MCP serving; must be present |
expose.agent.mcp.port | default listen port when --port and FISK_AI_MCP_PORT are unset, default 8080 |
expose.agent.mcp.address | host or IP to bind when --address and FISK_AI_MCP_ADDRESS are unset, default 127.0.0.1 (loopback); use 0.0.0.0 to listen on all interfaces |
expose.agent.mcp.instructions | free-text guidance sent to clients on connect |
expose.agent.mcp.confirm_over_mcp | how confirmation-gated commands behave when a client cannot be asked |
expose.agent.mcp.max_concurrent_tools | maximum tool calls run at once; 0 or unset uses the default 2, negative is rejected, capped at 1024 |
expose.agent.mcp.tool_timeout | how long a single served tool call may run, for example 60s; unset uses the default 30s |
include / exclude | select which commands and built-ins are served, matched on tool name (regex) or tag |
expose.agent.tools | narrow the served set further within the include/exclude selection, over commands and built-ins alike |
identity | the MCP server name; optional |
Instructions
expose.agent.mcp.instructions sets a block of free text sent to clients when they connect. A client may pass it to the
model as a hint about how to use the server, which suits orientation the individual tool descriptions are too terse to
carry:
How tools are exposed
Each command becomes an MCP tool named by its command path, for example stream_info, with its input schema and a
description built from the command’s help. Both the short help and any long help are surfaced to the client, so a
command that carries detailed long help gives the model richer guidance than a one-line summary alone.
Each tool carries a readable title annotation holding the space-separated command path, so stream rm rather than the
underscore tool name, and the behavioral hints its behavior tags declare:
| Tag | Annotation |
|---|---|
ai:read_only | readOnlyHint: true |
ai:destructive | destructiveHint: true |
ai:additive | destructiveHint: false |
ai:idempotent | idempotentHint: true |
Untagged commands send no hints at all, leaving the client on the MCP defaults, which treat a tool as destructive and
open-world. A confirmation gate does not change these hints: a command tagged both ai:read_only and ai:confirm is
still advertised as read-only.
Approval itself is not expressed as an annotation, because annotations are advisory hints a client may ignore rather than a control channel.
Every tag a command carries, reserved and free-form alike, also reaches the client as description text, as described under Command tags over MCP below.
The served tools are the agent’s include/exclude selection, narrowed further by expose.agent.tools when it is set.
With neither, every command is served, subject to the tag rules below, along with every enabled built-in that declares
MCP exposure: the knowledge tools when harness.knowledge is enabled, and read_file and
base64_encode when harness.tools lists them. Tool selection uses the same regular expressions over the tool name
as the agent, and a built-in is matched by its name, so exclude: {tools: [^knowledge_read$]}
serves the other knowledge tools without it. A built-in carries no tags, so an include by tag alone removes every
built-in unless a pattern names it back. A tool call runs the command and returns its result, limited by tool_timeout
per call and max_concurrent_tools in flight at once.
fisk mcp prints a note for each enabled built-in it does not serve, with the reason: the tool is reachable only in an
agent run, or a filter excluded it.
Command tags over MCP
The reserved command tags are honored over MCP, differing from the agent loop where noted:
| Tag | Behavior over MCP |
|---|---|
ai:deny | never exposed, the reliable way to keep a command off MCP entirely |
ai:no_defer | no effect, since MCP does not defer tools behind a tool-search tool |
ai:confirm | exposed and gated through elicitation rather than a local operator prompt |
| behavior tags | advertised as tool annotations, as described above |
All of a command’s tags, reserved and free-form alike, are included in the tool description as a trailing Tags: ...
line, the same as in the agent, so a client’s prompt can reference them.
Confirmation over MCP
Commands tagged ai:confirm, or a configured confirm_tags tag, require approval before they run. There is no local
operator on the MCP path, so Fisk AI requests approval from the calling client through MCP elicitation: before running a
gated command it asks the client to approve, showing the server name, the resolved command line, and the tag that gated
it, and runs the command only on an explicit approval. A refusal, a dismissal, or any elicitation error denies the call
and returns an authoritative result the model is told not to retry.
A built-in tool whose harness.tools entry sets confirm: true is gated the same
way. Its prompt shows the tool’s trace line in place of a command line, read_file <path> for example, and names
ai:confirm as the gate.
Not every client supports elicitation. expose.agent.mcp.confirm_over_mcp chooses what happens when the connected
client cannot be asked:
| Value | Behavior |
|---|---|
auto | default; ask clients that support elicitation, run the command ungated for clients that do not |
always | ask clients that support elicitation, refuse the command for clients that cannot be asked |
never | never ask, run gated commands ungated regardless of client support, delegating approval to the client’s own UI |
Warning
Elicitation is a request, not an enforcement boundary. A client is free to auto-approve, and under auto or never a
client that cannot elicit runs gated commands ungated. For a command that must never be reachable over MCP, use
ai:deny rather than relying on confirmation.
A client that already has its own approval UI may prompt twice under auto or always; set never when the client’s
own gating is trusted and the second prompt is unwanted.
What is not served
The built-in operator tools are agent-mode only and are never exposed over MCP, since there is no local operator on the MCP path:
- the human-in-the-loop
ask_human_*tools - the memory
memory_*tools
Tools imported from remote agents and MCP servers are not re-served either; they are reachable only in an agent run.
Safety
Every served command gets the same per-command protections as the agent: it runs as an argument
vector rather than through a shell, its arguments are checked against the command’s schema, its ANTHROPIC_API_KEY is stripped,
its output combines stdout and stderr and is capped at 64 KiB, and LLMFORMAT=1 is set.
The threat model is wider than an agent run:
- Any client that can reach the server’s port can invoke every exposed tool with any schema-valid arguments, the
knowledge base and the enabled
harness.toolsbuilt-ins included.ai:deny,include/excludeandexpose.agent.toolsare the gate on what is reachable, so scope the exposed set deliberately. - There is no agent loop, prompt, or token budget limiting total use.
tool_timeoutandmax_concurrent_toolslimit a single call and how many run at once. Neither limits how many calls a client makes, so do not expose the server on an untrusted network. - Command output is returned to the connected client rather than to Anthropic, so whoever connects sees whatever the commands print.
- Confirm-tagged commands are gated by elicitation, a request the client fulfills rather than an access control the
server enforces. Use
ai:deny, not confirmation, for anything that must never be reachable over MCP.