honeymcp
An open-source honeypot for the Model Context Protocol - impersonates a legitimate MCP server to collect threat intelligence on attacks against the MCP ecosystem.
Status: Building toward v1.0 on a 28-day sprint. Currently speaks Streamable HTTP (MCP spec 2025-06-18) and legacy HTTP+SSE side by side.
Live: operator banner + operator-auth dashboard.
What this is
I built honeymcp because there's no public record of what attackers actually send to MCP servers. The protocol is a year old, products using it ship weekly, and the usual web threat feeds don't cover this layer yet. So I wrote the sensor that collects it.
It's one Rust binary. About 15 MB, SQLite on disk, fits in 256 MiB of RAM. You run it, point DNS, and it answers MCP handshakes the way a real server would. Personas are YAML. I ship four out of the box (postgres-admin, github-admin, vercel-admin, stripe-finance) covering the surfaces attackers actually go after: source code, deployments, environment variables, financial data. Anyone scanning the internet for MCP endpoints gets a full conversation with fake tools and canned responses that hold up under multi-turn interrogation.
What lands in SQLite: timestamp, method, IP, User-Agent, client name and version, the Mcp-Session-Id they used, the MCP-Protocol-Version they claimed, and a SHA-256 of the raw params so reruns correlate. Eleven detectors tag events at write time, so you can grep for "prompt-injection traffic that also did tool-enumeration" without scanning the whole DB.
It's not a proxy. It won't protect your production MCP server. It's a trap you put on the internet to learn from. GET / returns a plain-text banner saying exactly that with a GDPR erasure contact. GET /dashboard is a server-rendered analyst surface (attack-intelligence overview, source-country map, campaign grouping, live feed, Attack Story Timeline, per-session MCP Sequence Diagram SVG at /dashboard/sequence/<id>.svg, and markdown report at /dashboard/report.md); operator traffic is filtered out by default with a ?include_operator=true toggle. No admin panel, no write path exposed to the network.
honeymcp-probes is the second binary in this crate. It fires the same 13 payloads the detectors are tuned for, so you can audit your own MCP server without standing up a honeypot. Same codebase, same taxonomy.
Why
MCP is a young protocol with a rapidly growing attack surface: tool poisoning, prompt injection carried through tool descriptions and results, command execution bugs in servers (e.g. CVE-2025-59536), and data exfiltration through tool calls into LLM context. There is no good public corpus of what attackers are actually doing against real MCP servers. honeymcp aims to be a drop-in honeypot that produces that data.
What it does today
- Speaks JSON-RPC 2.0 over stdio (the baseline MCP transport).
- Speaks Streamable HTTP (MCP spec 2025-06-18):
POST /mcpwithAccept-based negotiation (JSON or single-message SSE),GET /mcpfor server-to-client SSE,DELETE /mcpfor explicit session teardown, session identified byMcp-Session-Idheader. - Speaks legacy HTTP+SSE (
POST /message,GET /sse) for older clients that have not moved to the 2025-06-18 transport yet. - Handles
initialize,tools/list,tools/call, and the commonnotifications/*frames. - Records a curated allowlist of high-signal request headers (
MCP-Protocol-Version,X-Forwarded-For,Authorization,Origin,Referer,CF-Connecting-IP,CF-IPCountry,GeoIP-Country,X-Real-IP,Accept,User-Agent, and more) intoclient_meta, and logs non-MCP scan paths (/.env,/.git/config, ...) asprobeevents that still run through the detectors. - Loads personas from YAML - server name, version, instructions, and a list of fake tools with canned responses, with optional
{{canary.*}}and{{arg.*}}response templating. - Records which canary markers were exposed by each response and can import later canary-alert JSON with
--import-canary-hits, so the dashboard can correlatemarker -> event_id -> source_ip -> later_token_use_ipwithout storing token values. - Serves several personas from one process, routed by URL path (
/<persona>/mcp); the default image runsaws-admin(cloud ops with live canary credentials),github-admin,vercel-admin, andstripe-financetogether, withpostgres-adminandfilesystem-adminalso bundled. - Ships as a Docker image for one-command deploy; release builds are cosign-keyless-signed with SPDX + CycloneDX SBOMs attached and SLSA Level 3 build provenance verifiable via
gh attestation verify(recipe indocs/SLSA.md). - Serves an operator banner (research-honeypot disclosure + GDPR contact) at
GET /and a server-rendered analyst dashboard at/dashboard(attack classes, campaigns, Geo-IP pulse map, detector co-occurrence heatmap, MCP risk score, live SSE feed, markdown report, Attack Story Timeline, and per-session MCP Sequence Diagram; all assets bundled in the binary, design indocs/dashboard-v2-design.md). The production Caddy deployment keeps operator endpoints behind a host-local source allowlist plus Basic Auth. - Runs eleven threat detectors (prompt injection, shell injection, CVE-2025-59536-class hook injection, secret exfil, unicode anomaly, recon, tool enumeration, IMDS-SSRF, path traversal, AWS tool-intent, scanner fingerprinting) on every request, tagging events at write time. Each detection carries its MITRE ATT&CK / ATLAS technique IDs in the
detections.mitre_techniquescolumn so SIEM consumers can pivot on technique without re-deriving the mapping. Full mapping indocs/MITRE-MAPPING.md. - Logs every request/response to SQLite (primary, queryable) and optionally mirrors to JSONL (grep/jq-friendly), including timestamp, method, SHA-256 of params, raw params, client name/version, session id, transport, remote address, and User-Agent.
- Tags operator traffic at write time (
is_operatorcolumn)./statsexcludes probes and validation curls by default so any number a third party reads is the external-only corpus; pass?include_operator=trueto fold them back in. - Exposes build provenance at
GET /version(crate version, 12-char git short sha with a-dirtysuffix when relevant, RFC3339 build time). Every deploy is verifiable in one curl. - Exports to STIX 2.1 via
honeymcp --db hive.db --export-stix bundle.json. Each detection becomes anindicatorlinked to MITRE ATT&CK / ATLASattack-patternobjects (deduped across the bundle) plus per-eventobserved-dataSCOs — TAXII / OpenCTI / Sentinel TI / Splunk Add-on for STIX ingest the file directly. Indicator UUIDs are v5-deterministic so re-exports stay stable.
Clustering, embeddings and a public weekly threat report come in later days of the sprint (see .local-plans/ if you are the maintainer).
Quickstart
cargo build --release
./target/release/honeymcp \
--persona personas/postgres-admin.yaml \
--db hive.db \
--jsonl hive.jsonl
Feed it a handshake manually to verify it's alive:
printf '%s\n' \
'{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"tools/list","id":2}' \
'{"jsonrpc":"2.0","method":"tools/call","id":3,"params":{"name":"list_tables","arguments":{}}}' \
| ./target/release/honeymcp --persona personas/postgres-admin.yaml --db hive.db
Inspect collected events:
sqlite3 hive.db 'SELECT method, client_name, response_summary FROM events ORDER BY id DESC LIMIT 20;'
Correlate imported canary alerts:
./target/release/honeymcp --db hive.db \
--persona aws=personas/aws-admin.yaml \
--backfill-canary-exposures \
--import-canary-hits canary-alert.json
For deploying the honeypot on a public VPS with HTTPS, see docs/DEPLOYMENT.md. For day-2 operations — health checks, alert response, triage SQL, backup, scaling — see docs/RUNBOOK.md. The service-level objectives the project commits to are documented in docs/SLOS.md.
$ printf '%s\n' \
'{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"tools/list","id":2}' \
'{"jsonrpc":"2.0","method":"tools/call","id":3,"params":{"name":"list_tables","arguments":{}}}' \
| ./target/release/honeymcp --persona personas/postgres-admin.yaml --db hive.db
--- stdout (JSON-RPC responses) ---
{"jsonrpc":"2.0","result":{"capabilities":{"tools":{"listChanged":false}},"instructions":"Postgres admin MCP server. Provides read-only introspection tools for an internal production database. All queries are audited.","protocolVersion":"2024-11-05","serverInfo":{"name":"postgres-admin","version":"15.4"}},"id":1}
{"jsonrpc":"2.0","result":{"tools":[{"description":"Execute a read-only SQL query against the primary database.","inputSchema":{"properties":{"sql":{"description":"SQL statement to execute.","type":"string"}},"required":["sql"],"type":"object"},"name":"query"}, ...]},"id":2}
{"jsonrpc":"2.0","result":{"content":[{"text":"public.users\npublic.orders\npublic.sessions\npublic.api_keys\npublic.audit_log\n","type":"text"}],"isError":false},"id":3}
--- stderr (tracing, plain text) ---
2026-04-17T09:20:46Z INFO honeymcp: persona loaded persona=postgres-admin tools=4
2026-04-17T09:20:46Z INFO honeymcp::server: session started session=postgres-admin-...
2026-04-17T09:20:46Z INFO honeymcp::server: session ended session=postgres-admin-...
$ sqlite3 hive.db 'SELECT COUNT(*), method FROM events GROUP BY method;'
1|initialize
1|tools/call
1|tools/list
Full unabridged capture: docs/demo-day1.txt.
Architecture
flowchart LR
Client["MCP client<br/>(attacker)"] -- "JSON-RPC 2.0<br/>newline-delimited" --> Transport[stdio transport]
Transport --> Session[Session dispatcher]
Persona[(persona<br/>YAML)] --> Session
Session -->|initialize| Init[InitializeResult]
Session -->|tools/list| List[ToolsListResult]
Session -->|tools/call| Call[ToolCallResult<br/>canned text]
Session --> Logger
Logger --> SQLite[(SQLite<br/>events table)]
Logger --> JSONL[(JSONL append log)]
Project layout
src/
protocol/ JSON-RPC 2.0 + MCP payload types
transport/ Transport trait, stdio + http (Streamable + legacy SSE)
persona/ YAML persona loader + validator
detect/ Eleven detectors (prompt_injection, shell_injection,
cve_59536, secret_exfil, unicode_anomaly, recon,
tool_enumeration, ssrf_imds, path_traversal,
aws_intent, scanner_fingerprint)
logger/ SQLite + JSONL structured logging
server.rs Session / request dispatcher
main.rs CLI entry (clap)
bin/probes.rs honeymcp-probes audit CLI
personas/ Example personas (postgres-admin, github-admin)
docs/
DEPLOYMENT.md VPS deploy guide (Caddy + systemd)
threat-model.md STRIDE pass + known gaps
legal/operator-banner.md Research-honeypot banner template
legal/privacy-gdpr-lia.md GDPR Art. 6(1)(f) LIA
Persona format
name: "postgres-admin"
version: "15.4"
instructions: "..."
tools:
- name: "query"
description: "..."
inputSchema: { type: object, properties: { sql: { type: string } } }
response: "... fake result text ..."
The persona is the only knob you need to turn to impersonate a new service.
Authoring personas
For the full schema guide and a worked example, see
docs/personas.md.
honeymcp-probes
Ships as a second binary in this crate. A CLI battery of 13 attack payloads you point at any MCP endpoint to see what gets through:
honeymcp-probes --target http://your-mcp-server/message
# JSON report for CI:
honeymcp-probes --target http://your-mcp-server/message --json > report.json
# Fail the build if any Critical-severity probe gets HTTP 2xx back:
honeymcp-probes --target http://your-mcp-server/message --fail-on-critical
The probe taxonomy mirrors the server's detector taxonomy exactly - anything honeymcp-probes sends is something honeymcp is tuned to spot. Defenders can audit their own MCP server without needing to run the sensor.
Development
Clone, then enable the versioned pre-commit hook (runs cargo fmt --check + cargo clippy -D warnings before every commit):
git config core.hooksPath .github/hooks
Toolchain: Rust 1.88+ (edition 2024 dependencies); the repo pins 1.89.0 via rust-toolchain.toml for local dev so cargo-audit / cargo-deny install cleanly.
make ci # fmt-check + clippy -D warnings + test + audit + deny
make test # just the tests
make coverage # lcov.info via cargo-llvm-cov
make docker # local image build
Performance
The benches/ directory carries three criterion suites covering the
detector pipeline, the SQLite + JSONL recorder, and the dispatcher
end-to-end. Numbers below are from a single M1 MacBook Pro
(aarch64-apple-darwin, release profile, single-core measurement).
A real Linux VPS gets close to the same shape with about 1.4× the
CPU cost; treat these as one operator's reproducible baseline rather
than vendor-style benchmark theatre.
| Stage | Payload | Median latency | Throughput |
|---|---|---|---|
| Detector pipeline (analyze_all, all 7 detectors) | 200 B recon | 4.5 µs | ~220 k events/s |
| Detector pipeline | 2 KB prompt-injection | 9.7 µs | ~103 k events/s |
| Detector pipeline | 64 KB worst case | 462 µs | ~2.1 k events/s |
| Logger.record (SQLite + JSONL) | 200 B | 260 µs | ~3.8 k events/s |
| Logger.record | 64 KB | 612 µs | ~1.6 k events/s |
| Dispatcher end-to-end (parse + persona + detect + record) | initialize |
292 µs | ~3.4 k req/s |
| Dispatcher end-to-end | tools/list |
542 µs | ~1.8 k req/s |
| Dispatcher end-to-end | tools/call read_file |
808 µs | ~1.2 k req/s |
cargo bench # all three suites, full criterion sample
cargo bench --bench detectors -- --quick # ~30 s, fewer samples
The bottleneck on a real deployment is the recorder, not the detector pipeline; the detectors themselves can analyse two orders of magnitude more events than SQLite can persist. That's the right ratio for a honeypot: every captured event runs the full classifier without the classifier ever pushing back on the request path.
Optional feature flags
Default build is SQLite + stderr logs, no external services. Two opt-in features:
--features postgres- Postgres backend via sqlx 0.8.6 (pgvector-ready). Pair withdocker compose up -d postgres && make db-migratefor a local dev DB. Concrete backend wiring is still in progress; the feature currently compiles the scaffolding only.--features otel- OpenTelemetry OTLP exporter. Spans are forwarded via gRPC/tonic toOTEL_EXPORTER_OTLP_ENDPOINTwhen set; the layer is not registered otherwise, so enabling the feature without setting the env var costs nothing.
Runtime env vars
| Var | Effect |
|---|---|
RUST_LOG |
Standard tracing filter, default info |
HONEYMCP_LOG_FORMAT |
pretty (default, human-readable stderr) or json (ndjson for Loki / Cloudwatch / Datadog) |
HONEYMCP_BANNER_CONTROLLER |
Substituted into the banner served at GET / |
HONEYMCP_BANNER_ABUSE_EMAIL |
Contact address on the banner (GDPR Art. 13/14 + Art. 17 channel) |
HONEYMCP_BANNER_CONTACT |
Optional human contact name on the banner |
OTEL_EXPORTER_OTLP_ENDPOINT |
gRPC OTLP collector URL (only with --features otel) |
OTEL_SERVICE_NAME |
Overrides service.name resource; defaults to honeymcp |
Contributions: see CONTRIBUTING.md (security disclosure -> SECURITY.md).
Prior art & why honeymcp
Adjacent work exists but targets different layers:
- MCP gateways (MintMCP, Aembit) - protective proxies for legitimate deployments, not deception.
- Prompt-injection classifiers (StackOne Defender, Augustus, CloneGuard) - detect payloads, don't generate attack telemetry.
- Agent red-team tools (DeepTeam, Garak) - offensive side, not passive collection.
honeymcp fills a gap: passive intel collection on what attackers actually send to MCP servers in the wild, with server-shape accurate enough to sustain multi-turn interaction. Maps to OWASP Top 10 for Agentic Applications 2026 - ASI04 (Agentic Supply Chain Vulnerabilities) and ASI05 (Unexpected Code Execution).
Roadmap
The original roadmap aimed at v1.0.0-rc.1 in 28 days with a 4-week
flagship plan (Terraform module set, multi-region EKS + k3s deploy,
ML classifier, public weekly report). After two weeks of running the
sensor solo on a $7 Lightsail box and watching real corpus growth, I
cut that scope. The reasons are documented in
docs/scope-decisions.md.
The active roadmap is below. Stable cuts are real cuts (signed release
- deploy + corrigendum); rows marked
cutare explicitly out of scope for this line and tracked separately if they ever come back.
| Track | Focus | Status |
|---|---|---|
| Foundation | stdio + Streamable HTTP + legacy HTTP+SSE, 7 detectors, CI (fmt + clippy + test matrix + audit + deny + coverage), signed release workflow + cosign + SBOMs, threat model + GDPR LIA | ✅ v0.6.0 shipped 2026-04-27 |
| Operator surface | Operator banner, /version build provenance, is_operator traffic filter, server-rendered analyst dashboard (attack-intel overview, campaigns, Geo-IP pulse map, heatmap, live feed, report, Attack Story Timeline + per-session MCP Sequence Diagram) |
✅ shipped on v0.6/v0.7 line |
| Persona library | postgres-admin, github-admin, vercel-admin, stripe-finance shipped; figma-dev / cloudflare-edge / linear-pm open as good first issue |
4 of 7 shipped |
| Storage backends | SQLite is the operator default. Postgres + pgvector lives behind --features postgres with the migration set ready; the recorder side is scaffold-only and lights up when corpus growth justifies it |
scaffold |
| Observability | --features otel wires the OTLP exporter (tracing-opentelemetry); HONEYMCP_LOG_FORMAT=json for ndjson stderr; Dispatcher::handle_request is #[instrument]-ed |
code ready, production wiring pending (see docs/scope-decisions.md) |
| Corpus + analysis | Honest external-only counts everywhere; first weekly writeup published; data drop gated on ≥200 events from ≥30 unique sources (see docs/blog/2026-04-24-first-week.md) |
gated on corpus growth |
| Multi-region (EKS central + k3s edges) | Out of scope on this line; needs >1 region of real traffic and a budget that justifies $300+/mo of orchestrator capacity. Tracked for re-evaluation after the first solo-region corpus drop | cut |
| Terraform module set | Out of scope on this line; single-region Lightsail deploy is documented in docs/DEPLOYMENT.md and reproducible without IaC |
cut |
| ML classifier (HDBSCAN + LLM) | Out of scope on this line; rule-based detectors carry today's corpus. The pgvector index is in migrations/ precisely so this can land later without a migration cliff |
cut, scaffolded |
Verify a release
Release images and tag artifacts are signed via cosign keyless (OIDC). To verify before deploying:
cosign verify ghcr.io/kosiorkosa47/honeymcp:vX.Y.Z \
--certificate-identity-regexp 'https://github.com/kosiorkosa47/honeymcp/.*' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com'
SBOMs (SPDX + CycloneDX) are attached to each GitHub Release and also attested to the container image digest.
License
Apache-2.0 - see LICENSE.
No comments yet
Be the first to share your take.