A minimal MCP server built with Rust and
the rmcp SDK — a good starting point for a new
server or a demo. It exposes just two tools:
server_info — a health/status check.greet — a friendly greeting in one of a handful of languages, defaulting
to English. Ask it to "greet in French" and it replies Bonjour!.Built with Rust, rmcp, cargo, and
make. It is the Rust port of the sibling Python
mcp-hello-server, following the official MCP
Build a server (Rust)
reference. The Docker image is a fully-static binary on a distroless base — about
10 MB uncompressed (~2 MB compressed on the registry) and 0 known
vulnerabilities.
New to MCP? This is a tiny, safe server for seeing how an MCP client discovers and calls tools. Every tool is a harmless in-memory lookup, so it's a good sandbox. All you need is Docker and an MCP client — the steps below use Claude Code and the published Docker image (nothing to build or install).
Already running the Python
mcp-hello-server? It registers under the aliashelloand exposes the sameserver_info/greettools, so it's easy to mix up with this one — you might test the Python server while thinking you're testing the Rust one. Remove it first so your client only talks tohello-rust:claude mcp list # see what's registered (look for "hello") claude mcp remove hello # remove the Python server (its default alias)Use
--scope user/--scope projectif it was added at that scope, e.g.claude mcp remove hello --scope user. In Claude Desktop, delete thehelloentry frommcpServersinclaude_desktop_config.jsoninstead and restart.
1. Add the server. Claude Code launches the container per session and talks to it over stdio:
claude mcp add hello-rust -- docker run -i --rm -e MCP_TRANSPORT=stdio ghcr.io/mitchallen/mcp-hello-rust-server:latest
2. Confirm it connected:
claude mcp list # "hello-rust" should report ✔ Connected
3. Ask in plain language — Claude discovers the tools and picks one (the tool it calls is in parentheses):
server_info)greet → Bonjour!)greet → こんにちは (Konnichiwa), Alice!)server_info, reads languages)That round trip — the client listing tools, then calling one with arguments and getting structured JSON back — is MCP.
4. Remove it when you're done:
claude mcp remove hello-rust
Prefer HTTP? Run it as a long-lived server instead:
docker run --rm -p 8000:8000 ghcr.io/mitchallen/mcp-hello-rust-server:latest claude mcp add --transport http hello-rust http://localhost:8000/mcp
| Tool | Purpose |
|---|---|
server_info() | Health/status: app name, version, uptime, supported languages |
greet(language?, name?) | Greeting in language (default English); optional name |
greetgreet takes two optional arguments:
language — a language name, an alternate spelling, or an ISO code
(case-insensitive). Omit it to default to English. Supported: english,
spanish, french, german, italian, portuguese, japanese,
hawaiian (e.g. french, Français, or fr all work).name — optional; personalizes the message (Bonjour, Alice!).It returns { language, greeting, message }:
// greet(language="french")
{ "language": "french", "greeting": "Bonjour", "message": "Bonjour!" }
// greet(language="spanish", name="Alice")
{ "language": "spanish", "greeting": "Hola", "message": "Hola, Alice!" }
// greet() -> { "language": "english", "greeting": "Hello", "message": "Hello!" }
An unknown language returns an error listing the supported set.
Add a row to GREETINGS in src/greetings.rs (and, optionally, an alias / ISO
code in ALIASES). server_info reports the supported set automatically.
Requires a Rust toolchain (1.85+).
make build # cargo build --release
make test # run the test suite
make run # run the server over stdio
make help lists every target.
cargo run --release
# or
make run
make run-http # PORT defaults to 8000
PORT=9000 make run-http
The MCP endpoint is served at /mcp.
All configuration is via environment variables:
| Variable | Default | Purpose |
|---|---|---|
APP_NAME | mcp-hello-rust-server | Name reported by server_info |
MCP_TRANSPORT | stdio | stdio or http |
HOST | 127.0.0.1 | Bind address for http |
PORT | 8000 | Bind port for http |
Point a stdio-based client (e.g. Claude Desktop, Claude Code) at the release binary. With Claude Code, from the project directory:
make build
claude mcp add hello-rust -- "$PWD/target/release/mcp-hello-rust-server"
Confirm it's connected with claude mcp list (or /mcp inside a session).
Once the server is added, just ask in plain language — Claude picks the right tool. The tool it invokes is shown in parentheses.
server_info)greet, defaults to English → "Hello!")greet with language="french" → "Bonjour!")greet with language="japanese", name="Alice")server_info, then read languages)The image is published to two registries:
ghcr.io/mitchallen/mcp-hello-rust-servermitchallen/mcp-hello-rust-serverThe client starts a fresh container per session and talks to it over stdio. Use
-i (keep stdin open) and force the stdio transport, since the image defaults to
HTTP:
{
"mcpServers": {
"hello-rust": {
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT=stdio",
"ghcr.io/mitchallen/mcp-hello-rust-server:latest"]
}
}
}
Claude Code equivalent:
claude mcp add hello-rust -- docker run -i --rm -e MCP_TRANSPORT=stdio ghcr.io/mitchallen/mcp-hello-rust-server:latest
(Pin a version like :0.1.0 in place of :latest for a reproducible setup.)
Note: stdio uses stdin/stdout, not the network — no
-pport mapping is needed. Do not add-t(allocate a TTY): a TTY line-buffers and mangles the JSON-RPC stream, so use-ialone. Docker pulls the image on first run; the same command works with the Docker Hub image (mitchallen/mcp-hello-rust-server:latest).
The image serves HTTP by default. Start it once, then point an HTTP-capable client at it:
docker run -d --rm -p 8000:8000 --name mcp-hello-rust ghcr.io/mitchallen/mcp-hello-rust-server:latest
claude mcp add --transport http hello-rust http://localhost:8000/mcp
For clients that only speak stdio, bridge to the HTTP endpoint with
mcp-remote:
{
"mcpServers": {
"hello-rust": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://localhost:8000/mcp"]
}
}
}
Notes for remote use:
/mcp.Published multi-platform (linux/amd64, linux/arm64) images run the server
over streamable HTTP by default (MCP_TRANSPORT=http, HOST=0.0.0.0,
PORT=8000) so they're reachable on a published port.
The build compiles a fully-static musl binary on rust:1-alpine and copies
it onto a distroless Chainguard/Wolfi static
base — no shell, no package manager, runs as a non-root user, ~10 MB
uncompressed (~2 MB compressed), and scans
0 known vulnerabilities. Every build is gated by a Trivy scan (fails on
fixable CRITICAL/HIGH); the Rust dependency tree is separately scanned with
cargo-audit, and the published :latest is re-scanned daily — see
Security scanning.
docker pull ghcr.io/mitchallen/mcp-hello-rust-server:latest
docker run --rm -p 8000:8000 --name mcp-hello-rust ghcr.io/mitchallen/mcp-hello-rust-server:latest
Then connect an HTTP MCP client to http://localhost:8000/mcp.
Convenience targets pull and run the published image locally — handy for smoke-testing a release without a local build:
make docker-test # up + smoke + down in one shot (exits non-zero on failure)
make docker-up # pull + run ghcr.io/mitchallen latest, detached
make docker-smoke # MCP `initialize` handshake — passes if the server responds
make docker-down # stop it
make docker-up TAG=0.1.0 # pin a version
make docker-up REGISTRY=docker.io/mitchallen # pull from Docker Hub instead
make docker-up HTTP_PORT=9000 # publish on a different host port
make docker-build # docker build -t mcp-hello-rust-server .
make docker-run # serves http on localhost:8000
make scan # Trivy scan of the local image (fixable CRITICAL/HIGH fail)
Two complementary gates catch vulnerabilities, both reproducible locally:
image-scan (make scan) — Trivy scans the built container image and
fails the build on fixable CRITICAL/HIGH vulnerabilities. This is the OS /
base-image layer.cargo-audit — scans Cargo.lock against the
RustSec advisory DB for vulnerable crates compiled into
the binary (which an image scan can't see inside a static binary). Runs on
every push/PR and daily.scan-scheduled re-scans the published :latest image daily and uploads
results to the GitHub Security tab, catching CVEs disclosed after build time.Workflows live in .github/workflows/:
ci — on every push/PR to main: cargo fmt --check, cargo clippy -D warnings, and cargo test.image-scan / cargo-audit / scan-scheduled — vulnerability
scanning (see above).publish / publish-dockerhub — triggered by pushing a v* tag.
Build a multi-platform image, Trivy-scan it, push it to GHCR and Docker Hub,
then run make docker-test against the just-published image. The Docker Hub
job needs DOCKERHUB_USERNAME / DOCKERHUB_TOKEN repository secrets.To cut a release, use the release target — it bumps version in Cargo.toml
(and Cargo.lock), commits, tags, pushes, and creates the GitHub Release from
the CHANGELOG.md section, which triggers both publish workflows:
make release # patch bump (default)
make release BUMP=minor # or minor / major
The target refuses to run unless the working tree is clean, you're on main, and
CHANGELOG.md already has a ## [X.Y.Z] section for the new version.
Pushing to GHCR needs no setup — it uses the built-in GITHUB_TOKEN. The
publish-dockerhub job additionally needs two repository secrets and a
pre-created Docker Hub repo:
Create a Docker Hub access token (not your password): hub.docker.com → Account Settings → Personal access tokens → Generate new token, with Read & Write permissions (needed to push images and update the repo description). Copy it — Docker Hub shows it only once.
Create the Docker Hub repository mitchallen/mcp-hello-rust-server
(Public) so the push and the description-sync step have a target.
Add the two GitHub secrets — DOCKERHUB_USERNAME (your Docker Hub
username) and DOCKERHUB_TOKEN (the access token):
gh secret set DOCKERHUB_USERNAME --body "mitchallen"
gh secret set DOCKERHUB_TOKEN # prompts for the value — paste the token
Or via the web UI: repo Settings → Secrets and variables → Actions → New
repository secret. Verify with gh secret list (values stay hidden).
Without these, the GHCR publish job still succeeds; only publish-dockerhub
fails at the login step. Rotate the token by regenerating it on Docker Hub and
re-running gh secret set DOCKERHUB_TOKEN.
src/
greetings.rs — greeting data + language resolution (greet), with unit testsserver.rs — the rmcp tools (#[tool_router] / #[tool]) + ServerHandlermain.rs — the binary; transport wiring (stdio / HTTP)lib.rs — exposes the modules to the test cratetests/server.rs drives the tools through an in-memory rmcp client
over a tokio::io::duplex pipe (no network, no subprocess); src/greetings.rs
has #[cfg(test)] unit tests for the resolver/builder. Run everything with
make test, or the full CI gate with make check (fmt + clippy + test).Cargo.lock is committed and the Docker build compiles from
it. Run make lock (cargo generate-lockfile) after changing dependencies.MIT © Mitch Allen
Content type
Image
Digest
sha256:728c2ff8a…
Size
2 MB
Last updated
about 2 months ago
docker pull mitchallen/mcp-hello-rust-server