DEV Community

Cover image for Namespaces and Policy: Inside the Micro‑MCP Gateway
Malik Abualzait
Malik Abualzait

Posted on

Namespaces and Policy: Inside the Micro‑MCP Gateway

By Malik Abualzait

The story: from monolith to composition

When teams first try MCP, they often pack every capability into a single server: files, fetch, vector, SaaS. It works—until it doesn’t. A single permission error can expose too much. A small change can require a risky redeploy. Logs become a haystack.

Micro‑MCP asks a different question: what if each capability were a tiny server, and a thin gateway did the stitching—discovery, routing, policy, and audit? This post explains that gateway: how it names things, how it routes, and how it decides what’s allowed.

What the gateway is (and isn’t)

  • Is: an MCP‑aware aggregator in front of many single‑purpose MCP services.
  • Is not: a mega‑service that re‑implements your capabilities, nor a generic HTTP API gateway that ignores MCP semantics.

It speaks MCP‑style JSON‑RPC to the client, “fans in” capability catalogs, routes by namespace, and enforces cross‑cutting concerns.

Components

Namespaces: the language of composition

Namespaces prevent collisions and clarify ownership. In Micro‑MCP they’re part of the method name itself.

  • Examples: fs.listDir, http.fetchText, vector.search
  • Benefits:
    • Avoids name clashes across services
    • Makes routing trivial (everything before the dot is the target namespace)
    • Helps policy (“allow everything under fs.* for this principal”)

Good names are boring: use team.capability.action if you expect a large org. Start small and evolve.

Routing: one line, two ids, zero surprises

The gateway reads a JSON‑RPC message, authenticates, checks policy, then forwards to the namespaced service. To keep client ids intact, it uses an internal correlation id on the hop to the service and restores the client id on the way back.

Request (client → gateway):

{"jsonrpc":"2.0","id":42,"auth":"changeme","method":"fs.listDir","params":{"path":"."}}
Enter fullscreen mode Exit fullscreen mode

Forwarded (gateway → fs service):

{"jsonrpc":"2.0","id":1001,"method":"fs.listDir","params":{"path":"."}}
Enter fullscreen mode Exit fullscreen mode

Response (service → gateway):

{"jsonrpc":"2.0","id":1001,"result":[{"name":"docs","dir":true}]}
Enter fullscreen mode Exit fullscreen mode

Reply (gateway → client):

{"jsonrpc":"2.0","id":42,"result":[{"name":"docs","dir":true}]}
Enter fullscreen mode Exit fullscreen mode

That’s it: namespacing determines the target; correlation preserves client experience.

Policy: from allowlists to contextual decisions

Start simple with an allowlist. Patterns like fs.* or exact matches like http.fetchText cover many needs.

Add scopes when different principals need different slices (e.g., vector.search:read, vector.addDocument:write). Store the decision and the reason in your audit logs.

For high‑risk environments, graduate to contextual ABAC: evaluate conditions (caller role, time, resource sensitivity) via a policy engine (OPA/Rego or Cedar). For sensitive actions, add consent prompts and purpose binding so the user explicitly grants a specific use.

Security Flow

Authentication: dev vs prod

  • MVP: a static token is fine for local development.
  • Production: prefer OIDC (bearer tokens) or mTLS. Validate issuer/audience, expiration, and bind tokens to the connection where possible. Favor short‑lived credentials.

Observability: audit is your superpower

Audit logs are not “nice to have.” They’re how you answer: who did what, when, and why did the system allow it?

Log these fields at minimum:

  • timestamp, request_id, principal
  • method (namespaced), decision (allow/deny), reason
  • latency_ms, result_category (success, user_error, forbidden, transient)

Add distributed tracing so you can see client → gateway → service spans. Redact sensitive values—never log secrets.

Failure and resilience

  • Apply request size limits and backpressure to protect services
  • Use timeouts and circuit breakers for flaky dependencies
  • Prefer idempotency for side‑effecting tools, so safe retries are possible

Performance notes

  • Keep persistent connections to services (or long‑lived subprocesses)
  • Batch where possible; cache immutable resources by content hash
  • For large payloads, move bytes out of the gateway data plane (signed URLs)

Walkthrough (from this repo)

Run a namespaced call through the gateway:

printf '{"jsonrpc":"2.0","id":1,"auth":"changeme","method":"fs.listDir","params":{"path":"."}}\n' | \
  node gateway/src/index.js --config gateway/gateway.config.example.json --discovery gateway/discovery.example.json | cat
Enter fullscreen mode Exit fullscreen mode

Checklist for production readiness

Namespaces
- [ ] Namespace scheme documented; collisions linted

Policy
- [ ] Allowlist and scopes in place; tests for deny/allow paths
- [ ] High‑risk methods gated with consent/purpose

Auth
- [ ] OIDC or mTLS configured; tokens short‑lived and verified

Audit/Observability
- [ ] Structured audit with reason codes + tracing
- [ ] PII redaction; no secret logging

Resilience
- [ ] Request size limits; backpressure; timeouts; circuit breakers
Enter fullscreen mode Exit fullscreen mode

FAQ

Why not just a standard HTTP API gateway? MCP has a capability catalog and method semantics that benefit from protocol awareness (namespaces, schemas, correlation). A generic gateway can sit in front for TLS or coarse controls, but keep capability policy MCP‑aware.

Can I merge two services under one namespace? You can, but you lose isolation and clarity. Prefer separate namespaces unless you’re certain the lifecycle and risk profile are identical.

How do I evolve names? Use deprecation windows and aliases. Keep old names alive long enough for clients to migrate; document changes in the catalog.

Reference implementation

Top comments (0)