mcpvessel

module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 20, 2026 License: Apache-2.0

README

mcpvessel

Cage untrusted MCP servers, keep using them, compose them into agents, and share them.

CI Release License

An MCP server runs as a subprocess with your full user permissions. The protocol does not sandbox it, so an installed server can:

  • read your SSH keys, cloud credentials, and .env files
  • run arbitrary commands on your machine
  • send any of it anywhere

This is not theoretical. CVE-2025-6514 (rated critical) is host remote code execution from connecting to an untrusted server, and audits keep finding thousands of vulnerable public servers. Safe today does not mean safe after the next update.

mcpvessel runs each MCP server in an isolated container instead:

  • no access to your host or files
  • no outbound network unless you allow it
  • no provider keys inside the sandbox

It brings its own runtime, so there is no Docker or container engine to install. It can also compose several caged servers into a single LLM agent and distribute them over an OCI registry, both covered below.

Contents

Cage it

On macOS or Linux:

# Install the signed cask (this also wires up shell completions).
brew install --cask okedeji/tap/mcpvessel

# One-time runtime setup; on macOS this fetches a small Linux VM.
mcpvessel init

[!TIP] Try it in one command, no token, no config. This pulls mcpvessel's own docs as a signed MCP server and runs it caged:

mcpvessel serve io.github.okedeji/mcpvessel-docs --listen 127.0.0.1:7000
# point your MCP client at http://127.0.0.1:7000/mcp and ask it anything about mcpvessel

Caging a server of your own works the same way, whichever server it is. The example below uses GitHub's, because it carries a real token the cage must keep from leaking:

# Store the token. mcpvessel prompts for the value and hides your typing.
# (Or pipe it in for scripts: mcpvessel secrets set NAME < token.txt)
mcpvessel secrets set GITHUB_PERSONAL_ACCESS_TOKEN

# Cage GitHub's MCP server, named @me/github:0.1.
mcpvessel import io.github.github/github-mcp-server -t @me/github:0.1 --secret GITHUB_PERSONAL_ACCESS_TOKEN

# Serve it on one URL, with api.github.com the only host it can reach.
mcpvessel serve @me/github:0.1 --listen 127.0.0.1:7000 --secret GITHUB_PERSONAL_ACCESS_TOKEN --egress api.github.com

That prints one URL. Point Claude, Cursor, or any MCP client at it:

http://127.0.0.1:7000/mcp

All of GitHub's tools appear on that URL, and your client calls them exactly as before. The server runs in its own container with the internet switched off except for api.github.com, and its token stays inside the cage: it reaches GitHub and nowhere else, and it can never leave.

-t @me/github:0.1 is the caged server's handle: what you serve, push, and pull by. import also writes the editable source to ./github-mcp-server/, yours to tweak and rebuild anytime.

Not sure which hosts a server needs? You do not have to know up front. A run is deny-default: the first time a server reaches a new host, the connection is held and mcpvessel asks you to allow it. Approve it once and it is remembered:

# Serve with no egress set, the first call that reaches out is held, not failed.
mcpvessel serve @me/github:0.1 --listen 127.0.0.1:7000 --secret GITHUB_PERSONAL_ACCESS_TOKEN
# mcpvessel egress ls shows the held host; approve it and it is remembered for next time:
mcpvessel egress allow @me/github:0.1 api.github.com

Put several servers behind the same endpoint by importing more and serving them together, each in its own container with no route to the others:

# Cage a second server, a clock, named @me/time:0.1.
mcpvessel import pypi:mcp-server-time -t @me/time:0.1

# Serve both on one URL; --egress me-github:... grants hosts to only the GitHub server.
mcpvessel serve @me/github:0.1 @me/time:0.1 \
  --listen 127.0.0.1:7000 --secret GITHUB_PERSONAL_ACCESS_TOKEN \
  --egress me-github:api.github.com

Every server's tools appear together on that single URL, each still in its own cage. --egress me-github:api.github.com grants that host to the GitHub server alone; me-github is its address, which serve prints. The time server gets no network, since it needs none.

mcpvessel accepts any MCP server from npm, PyPI, or a container image, whether or not it is in a registry. If it runs as an MCP server, it can be caged.

Give it a brain

A caged server exposes tools while an MCP client like Claude does the thinking. Add --reasoning and the thinking moves inside the cage. The same servers become one agent that takes a goal and decides for itself which tools to call, in what order. One flag, no agent code.

Compose an on-call helper that reasons across Sentry and Brave Search:

# Store each server's key (prompts for the value; typing stays hidden).
mcpvessel secrets set SENTRY_ACCESS_TOKEN
mcpvessel secrets set BRAVE_API_KEY

# Compose both servers under one reasoning agent, named @me/oncall:0.1.
mcpvessel import io.github.getsentry/sentry-mcp io.github.brave/brave-search-mcp-server \
  --reasoning -t @me/oncall:0.1 --secret SENTRY_ACCESS_TOKEN --secret BRAVE_API_KEY

[!TIP] Shape how it reasons with --prompt "You are an on-call SRE; escalate P1s and cite the runbook." (or --prompt-file ./prompt.md for a multi-line one).

It needs a configured LLM provider (mcpvessel config provider set), plus the same keys and egress as before:

# Give it a task, it reasons over both servers' tools to answer.
mcpvessel run @me/oncall:0.1 "what is causing our top Sentry error this week, and how do I fix it?" \
  --secret SENTRY_ACCESS_TOKEN --secret BRAVE_API_KEY \
  --egress sentry.io --egress api.search.brave.com

This runs an LLM tool-use loop over both servers, caged alongside them, with a per-run spend cap. The result is an agent you invoke like any other, and the servers stay sandboxed as before. A secret only ever reaches a server that declares it, and like --egress, --secret can scope a key to just one server of several.

It does not have to live in your terminal. Serve it and it is an HTTP endpoint you can hit with nothing but curl:

# Serve the agent on one URL.
mcpvessel serve @me/oncall:0.1 \
  --listen 127.0.0.1:7000 --secret SENTRY_ACCESS_TOKEN --secret BRAVE_API_KEY \
  --egress sentry.io --egress api.search.brave.com

# Prompt it with curl; the result comes back as JSON.
curl -sX POST 127.0.0.1:7000/agents/oncall -d '{"prompt":"what is causing our top Sentry error, and how do I fix it?"}'
# {"result": "..."}

No MCP client or SDK needed, just JSON in and JSON out. The same agent can sit on a server, run in a CI job, or live behind your own API. It still speaks MCP on that port for clients that prefer it, and any single tool is directly callable at POST /agents/<name>/tools/<tool>.

[!TIP] For a response an app can render as it generates, add {"stream": true} to the body; the answer streams back as Server-Sent Events, chunk by chunk, instead of one JSON blob.

Ship it

A caged server or agent is a content-addressed bundle. Push it to any OCI registry you have logged in to (mcpvessel login):

mcpvessel push @me/oncall:0.1

A teammate pulls and runs it by the same reference, sandboxed the same way, without importing or building it themselves:

mcpvessel run @me/oncall:0.1 "what is causing our top Sentry error?"

It is signed on push and verified on pull, so they run exactly what you built, caged the same way. The publisher key fingerprint and how to verify a pull are in SECURITY.md.

How it works, briefly

A run is a small set of containers on private, internal-only networks. The server you cage sits alone on its own network with no route out. The only doors are small broker containers that mcpvessel runs for you: one filters every outbound network request against the allowlist you set, one brokers calls between servers, and when a server reasons with an LLM, one more holds your model key so the agent never sees it. On macOS all of this runs inside a lightweight Linux VM that mcpvessel sets up on first run, so nothing touches your host directly. On Linux it uses the host's own container runtime.

Install

Homebrew (recommended). Installs a signed cask and wires up shell completions:

brew install --cask okedeji/tap/mcpvessel

Direct download. Grab the archive for your OS and architecture from the releases page, verify it against checksums.txt, then put the binary on your PATH. This is the right path on Windows (run it inside WSL2).

From source. For contributors and anyone who wants to build it themselves:

git clone https://github.com/okedeji/mcpvessel
cd mcpvessel
make build

Note: on macOS the release archives bundle the Linux VM image the runtime needs, so prefer Homebrew or the direct download over go install.

Requirements

  • macOS (Apple Silicon or Intel) or Linux. On Windows, it runs inside WSL2.
  • Homebrew, for the recommended install above.
  • On first run, mcpvessel init sets up the runtime. On macOS that is a one-time step: it downloads a small Linux VM image and starts a rootless container daemon, which takes two to five minutes depending on your connection. Every run after that is a few seconds. On Linux this is a no-op and uses the host's container runtime directly.

Uninstall

Stop the runtime, remove the binary, then delete the state directory (this removes the macOS VM, cached images, your signing key, and config):

mcpvessel daemon stop
brew uninstall --cask mcpvessel   # or delete the binary you installed
rm -rf ~/.mcpvessel

Commands

mcpvessel --help lists every command, and mcpvessel <command> --help covers any one in full, with its flags and examples. You only need import and serve to get started; the rest is there as you grow into it.

Deeper guides for each command live in the docs directory.

Contributing and support

License

Apache 2.0. See LICENSE.

Directories

Path Synopsis
cmd
mcpvessel command
internal
bundle
Package bundle reads an agent's source directory plus its Vesselfile and packages them into a .agent file: a gzip-tarball of the source tree alongside a manifest.json that describes it.
Package bundle reads an agent's source directory plus its Vesselfile and packages them into a .agent file: a gzip-tarball of the source tree alongside a manifest.json that describes it.
config
Package config reads and writes the operator's ~/.mcpvessel/config.json: LLM provider endpoints and per-cage resource caps.
Package config reads and writes the operator's ~/.mcpvessel/config.json: LLM provider endpoints and per-cage resource caps.
daemon
Package daemon is mcpvessel's long-lived host process.
Package daemon is mcpvessel's long-lived host process.
egress
Package egress is the in-run HTTP CONNECT proxy: a cage reaches only the hosts its EGRESS allow: policy names, and the internal run network makes this the only way out.
Package egress is the in-run HTTP CONNECT proxy: a cage reaches only the hosts its EGRESS allow: policy names, and the internal run network makes this the only way out.
env
Package env is the single source of the VESSEL_* environment variable names.
Package env is the single source of the VESSEL_* environment variable names.
eval
Package eval runs an agent's eval suite: it loads the YAML the EVAL directive points at, runs each case in a real cage, checks the output against the case's expectations, and reports pass/fail counts plus an aggregate judge score.
Package eval runs an agent's eval suite: it loads the YAML the EVAL directive points at, runs each case in a real cage, checks the output against the case's expectations, and reports pass/fail counts plus an aggregate judge score.
githubauth
Package githubauth runs GitHub's OAuth device flow for MCP Registry publishing: a proven GitHub identity maps to the io.github.<user> namespace.
Package githubauth runs GitHub's OAuth device flow for MCP Registry publishing: a proven GitHub identity maps to the io.github.<user> namespace.
history
Package history is the daemon's durable run log: a bbolt store under ~/.mcpvessel, one record per run, keyed by the daemon-assigned run id.
Package history is the daemon's durable run log: a bbolt store under ~/.mcpvessel, one record per run, keyed by the daemon-assigned run id.
identity
Package identity is the single source of the product name and version.
Package identity is the single source of the product name and version.
llmgateway
Package llmgateway proxies an agent's OpenAI-compatible calls to the configured provider endpoint, holds provider keys so agents never see one, meters per-call cost, and enforces the run's shared budget.
Package llmgateway proxies an agent's OpenAI-compatible calls to the configured provider endpoint, holds provider keys so agents never see one, meters per-call cost, and enforces the run's shared budget.
locate
Package locate turns a bundle path or registry reference into a local .agent file, the step run, call, inspect, and tree share.
Package locate turns a bundle path or registry reference into a local .agent file, the step run, call, inspect, and tree share.
mcp
Package mcp wraps the official Go MCP SDK with the narrow surface mcpvessel needs: connect over stdio or HTTP, list tools, call one, read the text result.
Package mcp wraps the official Go MCP SDK with the narrow surface mcpvessel needs: connect over stdio or HTTP, list tools, call one, read the text result.
mcpgateway
Package mcpgateway is the in-run MCP gateway: a transparent MCP-over-HTTP reverse proxy that routes a parent agent's USES calls to each sub-agent and rejects denied tools with a JSON-RPC error.
Package mcpgateway is the in-run MCP gateway: a transparent MCP-over-HTTP reverse proxy that routes a parent agent's USES calls to each sub-agent and rejects denied tools with a JSON-RPC error.
mcpregistry
Package mcpregistry talks to the official MCP Registry: search, resolve a reverse-DNS name, publish a server.json.
Package mcpregistry talks to the official MCP Registry: search, resolve a reverse-DNS name, publish a server.json.
progress
Package progress renders build progress: Plain (line-by-line, safe everywhere) and TTY (BuildKit-style live dashboard), selected by the --progress flag.
Package progress renders build progress: Plain (line-by-line, safe everywhere) and TTY (BuildKit-style live dashboard), selected by the --progress flag.
reasoner
Package reasoner carries the Python reasoning harness that `import --reasoning` writes into a generated agent, and renders that agent's Vesselfile.
Package reasoner carries the Python reasoning harness that `import --reasoning` writes into a generated agent, and renders that agent's Vesselfile.
reference
Package reference parses agent references into OCI coordinates.
Package reference parses agent references into OCI coordinates.
registry
Package registry pushes and pulls .agent bundles as OCI artifacts: one gzip-tar layer under a manifest whose artifactType marks it mcpvessel's.
Package registry pushes and pulls .agent bundles as OCI artifacts: one gzip-tar layer under a manifest whose artifactType marks it mcpvessel's.
replay
Package replay owns the .replay artifact under ~/.mcpvessel/replays: the full-payload recording of a run's external interactions.
Package replay owns the .replay artifact under ~/.mcpvessel/replays: the full-payload recording of a run's external interactions.
resolve
Package resolve locks USES tags to the digests the manifest lockfile records and rejects cyclic graphs.
Package resolve locks USES tags to the digests the manifest lockfile records and rejects cyclic graphs.
runtime
Package runtime builds and runs agent cages (each cage is one OCI container).
Package runtime builds and runs agent cages (each cage is one OCI container).
secrets
Package secrets stores the operator's named secret values in a 0600 file under ~/.mcpvessel.
Package secrets stores the operator's named secret values in a 0600 file under ~/.mcpvessel.
serve
Package serve exposes a served run's public agents over MCP-over-HTTP and plain JSON-over-HTTP on one front door.
Package serve exposes a served run's public agents over MCP-over-HTTP and plain JSON-over-HTTP on one front door.
signing
Package signing signs pushed bundles and verifies pulled ones.
Package signing signs pushed bundles and verifies pulled ones.
store
Package store keeps built .agent bundles under ~/.mcpvessel/store.
Package store keeps built .agent bundles under ~/.mcpvessel/store.
telemetry
Package telemetry holds a run's trace model: the span tree the daemon writes into history and `mcpvessel trace` renders.
Package telemetry holds a run's trace model: the span tree the daemon writes into history and `mcpvessel trace` renders.
vesselfile
Package vesselfile parses and validates the Vesselfile, the declarative manifest at the root of every agent's source tree.
Package vesselfile parses and validates the Vesselfile, the declarative manifest at the root of every agent's source tree.
wrap
Package wrap generates the Vesselfile that turns an existing MCP server (npm, PyPI, or OCI) into an mcpvessel agent: a FROM/RUN/ENTRYPOINT that installs and launches it over stdio.
Package wrap generates the Vesselfile that turns an existing MCP server (npm, PyPI, or OCI) into an mcpvessel agent: a FROM/RUN/ENTRYPOINT that installs and launches it over stdio.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL