MCP Servers
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-ai 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-ai 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-ai 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 allowlisted built-ins such as knowledge_search |
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.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 |
include / exclude | select which commands become tools, matched on tool name (regex) or tag |
expose.agent.tools | narrow the exposed set further within the include/exclude selection |
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 also carries MCP annotations:
- a readable title, the space-separated command path, so
stream rmrather than the underscore tool name - a read-only hint, derived from the command’s tags, when a command declares through a tagging convention that it only reads rather than mutates its environment, so a client can tell a safe query from a command that changes state; a command that declares no impact is left without the hint
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. Tool selection uses the same regular expressions
over the tool name as the agent. A tool call runs the command and returns its result; a
per-call timeout and a concurrency limit bound execution.
Command tags over MCP
The reserved command tags are honored over MCP, with two differences from the agent loop:
| 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 |
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.
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
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 bound to 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 and worth understanding:
- Any client that can reach the server’s port can invoke every exposed tool with any schema-valid arguments.
ai:denyandinclude/excludeare the gate on what is reachable, so scope the exposed set deliberately. - There is no agent loop, prompt, or token budget bounding aggregate use. A per-call timeout and a concurrency limit bound execution, but not the total number of calls, 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.