meshmcp
identity-native control plane · MCP over WireGuard

meshmcp

Every agent-to-tool call: identified, governed, and provable.

Expose any Model Context Protocol server as a dark service — reachable only over a private WireGuard mesh, zero open ports, a cryptographic identity for every caller. Then enforce what each agent may do, prove what it did, learn a policy from how it behaves, and hand it credentials it never holds.

0 open ports Ed25519-signed audit MCP 2025-06-18 + draft userspace WireGuard go test -race
agent / IDE
holds a reference,
not a secret
meshmcp gateway
one control plane
identitypolicyauditinject
MCP servers
fswebpay+N
many · unmodified
L1 — L6 · the stack
the architecture

Six layers, one library

Connectivity, identity, policy, proof, intelligence, and reach — each a Go package, each built on the layer below. Adopt as much of the stack as you need; every layer keys off the same cryptographic identity.

L1CONNECTDark services Dark services on WireGuard · cryptographic per-caller identity · resumable, migratable sessions that survive roaming and gateway crashes.
L2ENFORCEAgent firewall Allow/deny per tool by identity, plus rate limits, time windows, taint tracking, data-flow labels, and human co-sign.
L3PROVESigned ledger Non-repudiable audit — a hash chain sealed by Ed25519-signed Merkle checkpoints · a live dashboard · deterministic session replay.
L4UNDERSTANDPolicy from behavior Profile what agents do, recommend a least-privilege policy, simulate a change as a CI gate, detect drift.
L5BROKERCredential broker Credential injection by identity — the agent references a secret by name and never holds the value; every use is audited.
L6SCALEReach An aggregating router (LB · failover · discovery), a managed control plane, and identity-mapped cross-org federation.
the fabric
the fabric

One control plane, many MCP servers

meshmcp isn't a server — it's the layer in front of your servers. A filesystem, a web-fetcher, a payments API, a customer database, a deploy pipeline: each a different MCP server, wrapped unmodified. Every call to any of them gets the same identity, policy, audit, and secret injection — and the router unions them into one namespaced endpoint.

agent Aread-only
agent Bbilling
IDE / CLIyou
meshmcp · one plane identitypolicyauditsecretsrouter
fs stdio
filesystem
read_*
web stdio
fetch / HTTP
taint_source
payments stdio
Stripe API
🔑 secretco-sign
customer-db stdio
records
emit pii
deploy stdio
CI / CD
⏰ windowco-sign
github http
repos
🔑 secret
slack http
messaging
block pii
vectors stdio
embeddings
rate-limited

Each server keeps its own nature — different tools, transports, risk. The plane gives them a shared spine: one WireGuard identity per caller, one policy language, one tamper-evident ledger, one credential broker.

share · fs

Give your team a local tool — expose nothing

Run an MCP server behind zero open ports. Teammates reach it over the mesh; an internet scan finds nothing.

$ meshmcp serve --config backends.yaml
defend · web

Stop prompt injection at the network

Untrusted content taints the session; the privileged tool simply won't route. No jailbreak can talk past a rule the mesh enforces.

taint_source: true taint_guard: true
contain · customer-db

Never let PII leave the mesh

Reading customer data labels the session pii; any egress tool refuses to run while that label is present.

emit_labels: [pii] · block_labels: [pii]
broker · payments

Give an agent a key it never sees

The agent writes {{secret:stripe_key}}; the gateway injects the real value by identity. The agent, its logs, its context — none hold it.

$ meshmcp secrets check --config secrets.yaml
prove · any server

Show an auditor exactly what happened

A tamper-evident, signed ledger of every tool call by identity. Provable complete-and-unedited with the public key alone.

$ meshmcp audit verify log --checkpoints cps
gate · payments

Require a human to co-sign money movement

A privileged call is held until a human identity on the mesh approves it. Silence is not permission.

$ meshmcp approve --store d peer transfer_funds
L2 · enforce
L2 · enforce

The agent firewall

Policy is declarative and keyed off the WireGuard identity the transport proves — not a header the caller sends. A denied call gets an inline JSON-RPC error; the backend never sees it.

policy.yaml — the whole language
default_allow: false            # deny by default
rules:
  # rate-limited reads for everyone
  - peers: ["*"]
    tools: ["read_*", "search"]
    allow: true
    rate: { max: 30, per: "1m" }

  # deploy only in business hours
  - peers: ["pubkey:<agent>"]
    tools: ["deploy"]
    allow: true
    when: { days: [mon,tue,wed,thu,fri],
            hours: "09:00-17:00", tz: "UTC" }

  # money movement needs a human co-sign
  - peers: ["*"]
    tools: ["transfer_funds"]
    allow: true
    require_cosign: true

Taint tracking is prompt-injection defense at the network layer. Mark data-fetching tools as a taint_source and privileged tools as a taint_guard. The moment untrusted content enters a session, the guarded tool stops routing — decided from connection state the model can't influence.

fetch() ALLOW ● session: tainted
write_file() BLOCKED ·"session tainted by untrusted data"
The write never reaches the backend. No LLM guardrail or ordinary firewall can express this.

Data-flow labels generalize taint to a lattice: emit_labels: [pii] on a reader, block_labels: [pii] on an egress tool — and PII can never leave the mesh through an agent.

L3 · prove
L3 · prove

A ledger you can prove to a regulator

Every decision is a hash-chained record. Periodic Ed25519-signed Merkle checkpoints seal the chain, so the log is complete-and-unedited — provable with the public key alone, even against an insider who can write the file.

seq 1
read_file allow
hash 9f9d…
seq 2
delete_all deny
prev 9f9d· e8f0…
seq 3
write cosign
prev e8f0· 26b2…
🔑 signed checkpoint Merkle root over seq 1–3 · Ed25519 signature · links to the previous checkpoint
Edit any record — even re-linking the whole chain — and the Merkle root no longer matches the signed root. It can't be forged without the private key.
$ meshmcp audit verify
$ meshmcp audit verify audit.jsonl --checkpoints cps.jsonl --pubkey <key>
OK  1240 records, 10 signed checkpoint(s), 1240 records committed
    non-repudiable: the log is complete and unedited, provable with the public key alone

# after a single tampered record:
$ meshmcp audit verify audit.jsonl --checkpoints cps.jsonl --pubkey <key>
FAILED  checkpoint 1 Merkle root mismatch: the records it covers were edited
        (signed root 0b78259d2e2c… , recomputed d11992f3ae84…)   exit 1
L4 · understand
L4 · understand

Policy that writes itself

A deny-by-default firewall nobody can author is a Ferrari with no steering wheel. insight is the read side: it turns the audit stream into policy, then watches for drift. It's the function AUDIT-RECORD* → POLICY-DSL.

observe

profile

per-identity behavior: tools, rate p99, hours, label flows.

recommend

synthesize

a least-privilege policy — exactly what was used, nothing more.

simulate

CI gate

replay real traffic through the change; exit ≠ 0 on any regression.

detect

drift → co-sign

deviation from baseline opens a human gate — fail-to-human, not fail-closed.

↺ a detected deviation becomes new observed behavior → re-recommend. The loop closes.
$ meshmcp insight — the loop, live
$ meshmcp insight recommend hist.jsonl > policy.yaml
# → pubkey:K granted read_dir, read_file · rate max:2/1m · when wed 09:00-17:00

$ meshmcp insight simulate hist.jsonl --policy policy.yaml
simulated 25 decisions: 25 unchanged, coverage 96%          exit 0

$ meshmcp insight simulate hist.jsonl --policy too-tight.yaml
REGRESSIONS (were allowed, now blocked):
  agent.mesh read_dir   allow→deny  ×8                       exit 1

$ meshmcp insight detect today.jsonl --baseline last-week.jsonl
  [1.00] unknown-identity  stranger.mesh   → require enrollment
  [0.80] new-tool          exfiltrate      → open co-sign
  [0.50] off-hours         03:00 UTC       → open co-sign
L5 · broker
L5 · broker

The agent never holds the credential

Because the gateway already parses every call, it injects secrets by identity. The agent carries a reference; the real value is spliced in on the way to the backend — and reaches only the backend.

agent
charge {
 "auth":"Bearer {{secret:stripe_key}}"
}
holds a reference, never the key
meshmcp gateway
✓ authorized by WireGuard identity
✓ refused if session tainted
✓ audited: "stripe_key: injected"
trace & audit see only the reference, never the value
MCP server
charge {
 "auth":"Bearer sk_live_…"
}
the value exists only here
Grants gate injection by identity, tool, and session labelblock_labels: [tainted] means untrusted content can never trigger a credential use.
L6 · scale
L6 · scale

From one server to a federated network

The same identity and audit primitives compose outward — one endpoint over many servers, a control plane that onboards nodes, and boundaries that bridge organizations.

router

One endpoint over N servers

Namespaced union of many backends, with replica load-balancing, health-based failover, a discovery registry, and full bidirectional MCP.

$ meshmcp router --config router.yaml
control

Onboard nodes without hand-wiring

A managed control plane serves enrollment (real NetBird one-off key issuance), the service registry, and policy distribution — as one mesh peer.

$ meshmcp control --netbird-token <pat>
federate

Bridge two organizations

A boundary maps a remote org's identity, admits only its granted tools, stamps a local principal, and audits every crossing.

$ meshmcp federate --config boundary.yaml
protocol/ · Go
the wire, typed

MCP protocol models, in Go

The plane speaks fluent MCP because the whole wire format is modelled as Go — faithfully translated from the official TypeScript sources, one package per protocol domain, each with round-trip and conformance tests. Polymorphic unions decode through Decode* discriminators; extends becomes struct embedding.

base2025-06-18 — the stable schema: JSON-RPC frames, resources, prompts, tools, sampling, completion, roots, elicitation.
draftserver/discover, MRTR, subscriptions, a structured error catalog, sampling tool-use, form/url elicitation, streamable-HTTP + stdio transports, OAuth 2.1 authorization.
extensionsServer Card, Tasks (io.modelcontextprotocol/tasks) and Apps (host↔UI bridge) — from the ext-* repos.
cachingA client-side response cache honouring the draft CacheableResult hints — use / refresh / bypass, plus notification-driven revalidation.
basejsonrpcresourcecontentprompttoolsamplingcompletionrootselicitationdiscovermcperrormrtrsubscriptionssamplingtoolselicitationdrafttransportauthorizationcachingservercardtasksapps
go test ./protocol/...
$ go test ./protocol/...
ok  meshmcp/protocol/tool        structuredContent: object · array · error
ok  meshmcp/protocol/discover    server/discover · capabilities
ok  meshmcp/protocol/samplingtools tool_use · tool_result
ok  meshmcp/protocol/authorization RFC 9728/8414/7591 · token-exchange
# 23 packages · 92 tests · every union round-trips
reference
reference

19 commands, one static binary

Everything is one meshmcp binary. Mesh credentials come from $NB_SETUP_KEY or flags.

serve & scale
serveexpose backends on the mesh
routeraggregate upstreams into one endpoint
orchestratea tool that calls other servers' tools
controlenrollment · registry · policy dist.
federatecross-org boundary
connect & drive
connectstdio ⇄ remote stdio bridge
forwardlocal TCP → mesh peer (HTTP)
ls · calllist / call tools from the CLI
read · promptresources & prompts
probein-process handshake diagnostic
govern & prove
approvehuman co-sign a held call
secretscheck the credential broker config
auditverify the chain · keygen
dashlive control dashboard
insightprofile · recommend · simulate · detect
replayre-issue a session & diff responses
quick start — 60 seconds
$ go build -o meshmcp .
$ export NB_SETUP_KEY=<key from app.netbird.io>

$ meshmcp serve --config examples/demo-backends.yaml   # prints mesh IP 100.x.y.z

# from any other machine on the mesh — nothing is exposed to the internet:
$ meshmcp ls   100.x.y.z:9101
$ meshmcp call 100.x.y.z:9101 add --arg a=2 --arg b=40
  → 42