Guides / Tool servers (MCP)
Tool servers (MCP)
A tool server gives an AI assistant new abilities: read your tickets, open a pull request, query a database. kosa8 runs each one in a virtual machine of its own, puts one gateway between it and the assistant, and refuses to pass on a tool whose description has changed since you approved it.
What MCP is
The Model Context Protocol (MCP) is the common way an AI client asks another program what tools it offers and then calls them. A program that answers is an MCP server, or tool server: it publishes a list of tools, each with a name, a description and a schema for its arguments.
Why a tool server is a risk
There are two separate problems, and it helps to keep them apart.
npx or pip a moment before it starts. Launched
the usual way, it runs as you, with your files, your SSH keys and your network.kosa8 answers the first with a sandbox and the second with pins. The rest of this page is how.
The picture
- Your AI client is configured with one MCP server:
kosa8 mcp serve. - The gateway decides every call before it goes anywhere, and writes down what it decided.
- Each server you registered runs as a container in a sandbox named
mcp-NAME, with the network rule you gave it. - A server with no need for the network gets none. It cannot see the other servers, or your files.
- kosa8's own tools come through the same gateway, under the same rules if your policy names them.
Walkthrough
Register a server
Give it a name, the image to run in, what it may reach, and the command that starts it.
Everything after -- is the server's own command line.
$ kosa8 mcp add github --image node:22-alpine \ --egress-allow api.github.com --egress-allow registry.npmjs.org \ -e GITHUB_TOKEN=… \ --allow-tool list_issues --allow-tool create_issue \ -- npx -y @modelcontextprotocol/server-github registered github (image node:22-alpine) — served on next `kosa8 mcp serve`
A server started with npx has to download itself first, which is why the
registry is on the list. A server already in its image needs only the hosts it works with.
Check what is registered
$ kosa8 mcp ls
NAME IMAGE COMMAND EGRESS TOOLS
github node:22-alpine npx -y @modelcontextprotocol/server-github allow:api.github.com,registry.npmjs.org list_issues,create_issue
Nothing is running yet. Registration only writes the entry to
~/.kosa8/mcp-servers.json.
Point your AI client at the gateway
Most clients launch an MCP server as a command and talk to it over standard input and output. Give them this command:
$ kosa8 mcp serve
When it starts, the gateway boots each registered server in its sandbox, asks it for its
tools, and reports on standard error (never on standard output, which belongs to the
protocol). This is a real run with a small test server named echo that has one
tool:
kosa8 MCP gateway: AI session … kosa8 mcp gateway: echo: pinned 1 tool definitions on first use; a later change to any of them will be withheld until approved (`kosa8 mcp pins show echo`) kosa8 mcp gateway: echo up (1 tools)
A server that fails to start is reported and skipped. It never takes kosa8's own tools down with it.
Or serve over HTTP
$ kosa8 mcp serve --http 127.0.0.1:7433 kosa8 MCP token (send as `Authorization: Bearer TOKEN`; shown once): … kosa8 MCP server listening on 127.0.0.1:7433
Every request must carry that bearer token. Set KOSA8_MCP_TOKEN to choose the
token yourself; otherwise a random one is printed once. Only loopback addresses are accepted
unless you pass --insecure-listen.
How the tools appear
Each tool is listed to the assistant as NAME_tool: the server github
with a tool create_issue becomes github_create_issue. Its description is
prefixed with where it runs, [github via kosa8 sandbox mcp-github], and it is marked as
potentially destructive so that clients which ask before destructive calls will ask.
A third-party tool can never replace one that already exists. If a name collides with one of kosa8's own tools or an earlier server's, the newcomer is skipped and the gateway says so.
Policy uses the server name kosa8 for kosa8's own tools, so
kosa8 mcp add kosa8 … is refused: "kosa8" is how MCP policy names kosa8's own
tools; register this server under another name.
Flags for kosa8 mcp add
| Flag | What it does | Default |
|---|---|---|
--image | The container image the server runs in. | alpine |
--egress | The sandbox's network rule: any or deny. | any, or the organisation's default_egress |
--egress-allow | Permit outbound traffic only to these hosts, or *.domain wildcards. Repeatable. Replaces --egress. | none |
-e, --env | KEY=VALUE for the server process. Repeatable. | none |
--allow-tool | Expose only this tool. Repeatable. An exact name, not a pattern. | all tools |
A server registered without --egress or --egress-allow can reach
anything, unless your organisation sets default_egress. Name the hosts it needs. If you later tighten the rule in
mcp-servers.json, the gateway notices on its next start and recreates the sandbox
rather than reusing one made under the looser rule.
Flags for kosa8 mcp serve
| Flag | What it does |
|---|---|
--http ADDR | Serve streamable HTTP on this address instead of stdio. Bearer-token authenticated. |
--insecure-listen | Allow --http on an address that is not loopback. Anyone who holds the token and can reach it can run containers on your machine. |
--allow-mount DIR | A host directory the assistant may share into sandboxes it creates: read-only, or writable when given as DIR:rw. Repeatable. With none, the assistant cannot share any host directory. |
--session ID | The AI session the audit entries belong to. A new one is made when omitted. |
--sandbox NAME | The sandbox the assistant itself works in, so its audit entries name it. |
Two environment variables matter as well: KOSA8_MCP_TOKEN (the HTTP bearer token)
and KOSA8_AGENT_TOKEN, which tells the gateway which agent it is serving. See
Agent identity.
Pinned tool definitions
The first time the gateway sees a server, it records a fingerprint of every tool that server offers. From then on a tool is passed to the assistant only while its fingerprint is unchanged. This is called pinning.
What is in the fingerprint
The fingerprint is a SHA-256 digest over six parts of the definition, put into one canonical form first so that the same definition sent with different key order or spacing is the same tool.
| Part | Why it is included |
|---|---|
| Name | What the assistant calls. |
| Title | What a client shows the person approving a call. |
| Description | The words the model reads as instructions. |
| Input schema | What the model is asked to fill in. |
| Output schema | What the tool promises to return. |
| Annotations | Hints such as "read-only" that a client trusts when deciding whether to ask. |
When a definition changes
The attack this stops is sometimes called a rug-pull: you approve a harmless tool, and later the server serves a tool with the same name and different words.
- On first use the gateway pins each definition and tells you it has.
- Later the server describes the same tool differently.
- At the next start the gateway computes the digest again and compares.
- The tool is left out of the list, the reason is printed and recorded, and the change waits for you.
A tool that appears beside the approved ones is treated the same way: withheld until someone has looked. The gateway prints one line per withheld tool:
kosa8 mcp gateway: echo: tool "echo" withheld: its definition changed since it was pinned (sha256:…, now sha256:…) (review with `kosa8 mcp pins diff echo`, accept with `kosa8 mcp pins approve echo`) kosa8 mcp gateway: echo: tool "upload" withheld: it is new since this server's tools were pinned (review with `kosa8 mcp pins diff echo`, accept with `kosa8 mcp pins approve echo`)
Reviewing and accepting a change
$ kosa8 mcp pins show echo # what it is pinned to echo (image …) echo sha256:… 2 changed or new definitions withheld; see `kosa8 mcp pins diff echo` $ kosa8 mcp pins diff echo # what changed, in the words the model reads ~ echo CHANGED sha256:… -> sha256:… was: … now: … + upload NEW TOOL sha256:… description: … accept with `kosa8 mcp pins approve echo [TOOL...]` $ kosa8 mcp pins approve echo echo # accept one tool, or leave the names off for all pinned echo for echo; exposed on the next `kosa8 mcp serve`
If the description is the same and something else moved, the diff says so: the description
is unchanged; the schema, title or annotations differ.
| Command | What it does |
|---|---|
kosa8 mcp pins show [SERVER] | Prints each pinned tool and its digest, in the form an organisation policy pins them. |
kosa8 mcp pins diff SERVER | Shows the withheld definitions: changed ones with the old and new description, new ones with theirs. |
kosa8 mcp pins approve SERVER [TOOL…] | Accepts the pending definitions, all or only those named. Takes effect at the next gateway start. |
kosa8 mcp pins reset SERVER | Forgets a server's pins so it is pinned afresh on next start. For a server you replaced on purpose. |
approve and reset insist on a terminal. Run from a script or a pipe they
answer `kosa8 mcp pins approve` must be run by a person at a terminal, so an assistant
with a shell cannot quietly accept its own server's change. kosa8 mcp rm NAME removes a
server's pins with it.
Pins set by your organisation
First use is taken on trust. An organisation that wants more than that puts the digests in its
signed MCP policy, copied from kosa8 mcp pins show on a machine where someone reviewed
the server:
{ "server": "github",
"allow_tools": ["list_issues", "create_issue"],
"pins": [
{ "tool": "list_issues", "digest": "sha256:<64 hex digits>" },
{ "tool": "create_issue", "digest": "sha256:<64 hex digits>" }
] }
Once a server has any pins in the policy, those are the only pins that count for it. A tool the
policy does not list, or whose definition differs, is withheld, and the hint changes to the
org policy's pins decide this; ask your admin. Nothing can be approved locally. Each pin
names one exact tool, and a digest that is not sha256: followed by 64 hex digits is
refused when the policy is signed. How to sign and install a policy is in
Company rules.
Schema checks, in and out
A tool's schema says what arguments it accepts. The gateway holds both sides to it.
On the way in
The assistant's arguments are checked against the pinned input schema before the server sees them. Arguments the tool did not ask for are refused:
kosa8: the arguments do not match the schema echo/echo was approved with: …
On the way out
If the tool declared an output schema, its result is checked before the model reads it. A result outside the tool's own contract is replaced with:
kosa8: echo/echo returned a result that does not match the output schema it declared,
so it was withheld: …
A schema is also untrusted input. One that points at something off your machine, recurses
without making progress, or nests absurdly is refused when the gateway starts, and its tool is not
offered: tool "x" withheld: its input schema cannot be enforced: …. A single check
that takes longer than two seconds counts as failed.
kosa8's own tools
The same gateway serves thirty tools that drive kosa8 itself. List them with their descriptions at any time:
$ kosa8 mcp tools
| Group | Tools |
|---|---|
| Status | kosa8_status |
| Containers | container_run container_list container_logs container_exec container_stop container_stats |
| Images | image_list image_pull image_build |
| Networks and volumes | network_list network_create volume_list volume_create |
| Sandboxes | sandbox_create sandbox_list sandbox_pull sandbox_run sandbox_start sandbox_stop sandbox_delete |
| Save and undo | sandbox_checkpoint sandbox_checkpoint_list sandbox_rewind sandbox_snapshot snapshot_list snapshot_restore snapshot_rm |
| Copy and explore | sandbox_fork sandbox_explore |
A sandbox the assistant creates with sandbox_create has no network
unless the assistant asks for one. It can name the hosts it needs with egress_allow, or
ask for an open network with egress: "any". Either way the request is in the call's
arguments, so it is in the audit entry and can be the subject of a rule. Pulling images still
works with no network, because the service fetches images on your side. Where your organisation
sets default_egress, that default applies instead.
These tools can be governed like any third-party server by naming the server
kosa8 in your policy. See Rules for tool calls.
Which images may run as tool servers
An organisation can list the images allowed to run as MCP servers, in the signed organisation policy:
{ "name": "example-corp",
"allowed_mcp": ["node:22-alpine", "ghcr.io/example-corp/docs-mcp:1.4"] }
The list is checked when you register a server and again when the gateway starts it. An image that is not on it, compared exactly and without regard to case, is refused:
policy "example-corp" forbids MCP image "alpine" (allowed: node:22-alpine, ghcr.io/example-corp/docs-mcp:1.4)
An empty list means any image. The full field list is in Policy reference.
What the audit log records
Every call through the gateway to a third-party server is written to the audit log. An allowed call leaves two entries: the decision, and the result as a child of it. A refused call leaves the decision alone. Tools that were withheld or denied when the gateway started are recorded too, so "why did the assistant not have this tool" has an answer.
| Entry | When | What it carries |
|---|---|---|
mcp.decision | A tool is offered or withheld at start (phase: expose), or called (phase: call) | Server, tool, scope, allowed or not, the rule that decided, the policy's name, digest and signer, and the SHA-256 of the arguments. |
mcp.result | An allowed call returned | The same, plus the SHA-256 of the result, whether it was an error, and parent_seq pointing at the decision. |
"refs": {
"session": "…",
"mcp": {
"phase": "call",
"server": "echo", "tool": "echo", "scope": "eng",
"allowed": true,
"policy": "example-corp", "policy_digest": "sha256:…",
"arguments": "…",
"server_sandbox": "mcp-echo"
}
}
Arguments and results are named by digest and never stored. A tool call's arguments are whatever the assistant chose to send, which may be a query over customer records or a token it found, and the audit log is kept for years and forwarded to other systems. The digest lets someone who holds the arguments prove they are the ones that were sent. More in The audit log.
What this does not do
- First use is trusted. The definitions a server offers the first time are pinned as they are. If they were already hostile, the pin protects the hostile version. Organisation pins exist for this.
- Results are checked for shape, not for meaning. A result that matches its schema can still contain text written to steer the model. Nothing here detects instructions hidden in well-formed output.
- Nothing scans content. Descriptions, arguments and results are not searched for secrets, malware or prompt injection. A pin tells you a definition changed. Reading the difference is your job.
- A tool with no output schema returns anything. The outgoing check applies only to tools that declared one.
- The gateway itself runs as you. It is not in a sandbox. The servers are. Anything that can run commands as your user can also edit
mcp-servers.jsonandmcp-pins.json; the terminal check onpins approvestops a pipe, and nothing more determined. - Pins are checked when the gateway starts. A server that changes what it offers mid-session is not asked again until the next start. Calls are still checked against the schema that was pinned.
- Values given with
-eare in the server's hands. They are stored in~/.kosa8/mcp-servers.json, readable only by you, and passed to the server process, which can send them to any host its network rule allows. - Images are named by tag.
allowed_mcpcompares image names, not image contents.
Where to go next
Rules for tool calls
Decide a call by its arguments: allow, deny, or ask a person first.
Agent identity
Give each agent its own token, scope and name in the audit log.
Company rules
Sign a policy and install it so a laptop cannot edit around it.
Agent troubleshooting
Every refusal message, what it means and what to do.