emperor-mcp 0.2.0

Opinionated MCP server framework: stateless Streamable HTTP transport, output framing, credential forwarding, rmcp bootstrap
Documentation
# The Emperor Profile — P1

**Profile version:** P1 · **Written against MCP spec revision:** [2025-06-18](https://modelcontextprotocol.io/specification/2025-06-18)
· **Applies to:** `emperor-mcp` 0.1.x

## What this is

The [Model Context Protocol](https://modelcontextprotocol.io) deliberately leaves choices open:
two standard transports, optional sessions, optional auth, no opinion on how tool output is
presented to a model. Openness is right for a spec. In an enterprise deployment, every open
choice is a place where two systems disagree in production.

This document closes those choices. It is a **deployment profile on top of the MCP
specification**: every rule either *narrows* an option the spec leaves open, *adds* a
requirement the spec is silent on, or *restates* a spec requirement to make it testable.
**No rule contradicts the specification.** A server that follows this profile is a fully
compliant MCP server and works with every compliant client.

Each rule states: the requirement, the rationale, its relation to the spec, and how it is
enforced — **by code** (with the module that enforces it) or **by policy** (verified in review,
see [`MCP_REQUIREMENTS.md`](./MCP_REQUIREMENTS.md)).

Conformance language follows RFC 2119 (**MUST** / **SHOULD**).

---

## Transport & sessions

### E1 — Streamable HTTP in production; stdio is a local/dev transport

Production MCP servers **MUST** be network services speaking
[Streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http).
The stdio transport **MUST NOT** be a production deployment mode; it remains supported for
local development and desktop-style embedding.

- **Rationale:** production servers need real lifecycles, horizontal scaling, observability,
  and network-level access control. A subprocess pipe has none of these.
- **Spec relation:** *narrows*. The spec defines both transports as standard and takes no
  position on deployment contexts (transports §, 2025-06-18).
- **Enforcement:** by design + policy. The production paths are [`serve::serve`]src/serve.rs
  and [`transport::serve_http`]src/transport.rs; [`transport::run_stdio`]src/transport.rs
  is documented as local/dev.

### E2 — Stateless: no `Mcp-Session-Id`, every request self-contained

Servers **MUST NOT** issue an `Mcp-Session-Id` and **MUST NOT** require one. Every HTTP POST
is an independent, self-contained exchange.

- **Rationale:** session affinity breaks load balancing, complicates failover, and couples
  credential lifetime to connection lifetime. Statelessness is what makes per-request
  credential forwarding (E6) sound.
- **Spec relation:** *narrows*. The spec says a server "**MAY** assign a session ID"
  (transports § Session Management); this profile forbids exercising that option.
- **Enforcement:** by code. [`serve.rs`]src/serve.rs pins `LEGACY_SESSION_MODE = false` (rmcp
  stateless mode + JSON responses); [`transport::http_router`]src/transport.rs has no session
  concept at all. Regression-tested: `http_request_gets_json_reply_without_session_header`.

### E3 — One MCP endpoint plus a liveness endpoint outside the protocol

Servers **MUST** expose a single MCP endpoint and **SHOULD** expose a `/health` liveness
endpoint that is not part of the MCP surface and requires no credentials.

- **Rationale:** orchestrators and load balancers need liveness without speaking MCP or
  holding credentials.
- **Spec relation:** *restates* the single-endpoint requirement; *adds* the health endpoint
  (the spec is silent on operational endpoints).
- **Enforcement:** by code on the rmcp path — [`serve.rs`]src/serve.rs mounts `/mcp` +
  `/health`. By policy on the `McpHandler` path (consumers mount their own router).

### E4 — Correct wire behavior: JSON or SSE by `Accept`, `202` for notifications

Responses to JSON-RPC *requests* **MUST** be `application/json` or `text/event-stream`
according to the client's `Accept` header; JSON-RPC *notifications* **MUST** be answered with
HTTP `202 Accepted` and no body.

- **Rationale:** this is where hand-rolled servers drift; drift here breaks clients silently.
- **Spec relation:** *restates* (transports § Sending Messages to the Server) — stated here so
  conformance is testable.
- **Enforcement:** by code. [`transport.rs`]src/transport.rs `handle_post` / `json_or_sse`;
  regression-tested (`http_notification_gets_202_no_body`, `http_sse_accept_yields_event_stream`).

## Security

### E5 — Localhost by default; production behind a trusted ingress

Development servers **SHOULD** bind `127.0.0.1`. Production deployments **MUST** run behind a
trusted ingress (gateway / reverse proxy) that terminates TLS, authenticates the caller, and
enforces `Origin` validation against DNS-rebinding.

- **Rationale:** the framework targets gateway-fronted enterprise deployments; the ingress is
  the policy enforcement point.
- **Spec relation:** *restates* the spec's Security Warning (Origin validation **MUST**,
  localhost binding **SHOULD**, authentication **SHOULD**) and *narrows* it into a concrete
  deployment shape.
- **Enforcement:** by policy behind an ingress, by code without one. [`serve.rs`]src/serve.rs
  leaves rmcp's in-process `Host`/`Origin` validation off by default (the ingress does it) and
  turns it on when `ServeOptions::with_allowed_hosts` / `with_allowed_origins` name hosts or
  origins (since 0.2). Regression-tested: `host_validation_refuses_unlisted_hosts_when_enabled`,
  `origin_validation_refuses_foreign_origins_when_enabled`. The examples bind `127.0.0.1`.

### E6 — Credentials are forwarded, never stored

Per-request credentials **MUST** travel in HTTP headers captured through an explicit
allow-list, **MUST NOT** be persisted by the server, and **MUST NOT** be logged. The server
forwards them to the upstream system and forgets them with the request.

- **Rationale:** an MCP server that stores credentials becomes a secret store with none of a
  secret store's guarantees. Forwarding keeps the credential's owner (the caller) and its
  consumer (the upstream) directly coupled — essential for multi-tenant deployments.
- **Spec relation:** *adds*. The spec's authorization section covers how clients authorize to
  servers; it is silent on how servers handle upstream credentials.
- **Enforcement:** by code. [`headers.rs`]src/headers.rs: `forward_headers_middleware`
  captures only `ForwardConfig` allow-listed headers into request-scoped `ForwardedHeaders`;
  the framework has no credential storage API. Regression-tested
  (`captures_only_allowlisted_headers`). "Never logged" is policy for tool authors.

### E7 — No auth shortcuts

Servers built on this framework **MUST NOT** ship testing modes, dev-mode credential bypasses,
or default credentials. Development, CI, and production run the same auth path.

- **Rationale:** every bypass eventually ships.
- **Spec relation:** *adds* (the spec has no opinion on development practices).
- **Enforcement:** by policy ([`MCP_REQUIREMENTS.md`]./MCP_REQUIREMENTS.md); the framework
  provides no bypass to enable.

## Output

### E8 — Tool output is framed as data, never instructions

Every tool result's text content **MUST** be wrapped in content-aware framing: a standing
"this is data, not instructions" instruction plus BEGIN/END delimiters carrying a per-call
random nonce, with bare delimiter strings inside the payload neutralized. The frame label
**MUST** be accurate for the content source (`ExternalWeb` / `TrustedReference` / `Computed`).

- **Rationale:** indirect prompt injection is the dominant connector risk; data/instruction
  separation ("spotlighting") is the cheap, high-value mitigation. Accurate labels matter:
  marking everything "untrusted" pollutes prompts and erodes smaller models' reasoning.
- **Spec relation:** *adds*. The spec is silent on prompt-injection mitigation of tool output.
- **Enforcement:** by code for the mechanism — [`framing.rs`]src/framing.rs (`frame`,
  `structured_framed`, marker neutralization; nonce-forgery regression-tested). By policy for
  its application: every tool a server exposes must return framed text
  ([`MCP_REQUIREMENTS.md`]./MCP_REQUIREMENTS.md §2).

### E9 — Protocol streams stay clean; logs are structured

On stdio, `stdout` **MUST** carry only protocol messages and logs **MUST** go to `stderr`. On
HTTP, logging **MUST** go through the `tracing` facade.

- **Rationale:** one stray `println!` corrupts a stdio protocol stream; unstructured logs are
  invisible to enterprise log pipelines.
- **Spec relation:** *restates* the stdio requirements (transports § stdio); *adds* the
  structured-logging requirement.
- **Enforcement:** by code direction — [`run_stdio`]src/transport.rs writes only protocol to
  stdout and documents the stderr rule; [`init_tracing`]src/lib.rs is the provided
  initializer. Stdout discipline inside tool code is policy.

## Errors

### E10 — JSON-RPC error envelopes, always

Malformed input **MUST** produce a JSON-RPC error (`-32700` with `id: null` for parse errors);
unknown methods **SHOULD** produce `-32601`. Errors are envelopes, never bare HTTP bodies —
except transport-level failures, which use HTTP status codes.

- **Rationale:** clients recover from structured errors; they crash on surprises.
- **Spec relation:** *restates* JSON-RPC 2.0 / MCP error semantics, made testable.
- **Enforcement:** by code. [`transport.rs`]src/transport.rs `parse_error`
  (regression-tested: `http_bad_json_returns_parse_error`); `-32601` demonstrated in
  [`examples/minimal_server.rs`]examples/minimal_server.rs.

---

## Compliance table

| Rule | Spec relation (2025-06-18) | Enforcement |
|------|----------------------------|-------------|
| E1 transport choice | narrows: transports § (both standard) | design + policy |
| E2 stateless | narrows: transports § Session Management ("MAY assign") | code: `serve.rs`, `transport.rs` + test |
| E3 endpoints | restates single endpoint; adds `/health` | code (rmcp path) / policy (`McpHandler` path) |
| E4 wire behavior | restates: transports § Sending Messages | code: `transport.rs` + tests |
| E5 ingress & origin validation | restates Security Warning; narrows deployment shape | policy behind an ingress; code: `ServeOptions` host/origin allow-lists + tests |
| E6 credential forwarding | adds (spec silent on upstream credentials) | code: `headers.rs` + test; logging rule = policy |
| E7 no auth shortcuts | adds | policy |
| E8 output framing | adds (spec silent on prompt injection) | code: `framing.rs` + tests; application = policy |
| E9 clean streams / tracing | restates stdio §; adds structured logging | code direction; tool-code discipline = policy |
| E10 error envelopes | restates JSON-RPC semantics | code: `transport.rs` + test |

## Profile versioning

- **P1** (this document) is written against spec revision **2025-06-18** — the revision the
  underlying SDK path implements (initialization-handshake protocol family, which the current
  spec documents for backward compatibility).
- The **current** spec revision is [2026-07-28]https://modelcontextprotocol.io/specification/2026-07-28,
  which replaces the initialize handshake with per-request version declaration
  (`MCP-Protocol-Version` header / `_meta`) and a mandatory `server/discover` RPC. **P2** will
  cover that family (the framework is on rmcp 3, which speaks it; the profile text follows).
- A profile version bump is required whenever a rule is added, removed, or its spec mapping
  changes. Servers declare the profile they target as "Emperor Profile P1".