Security-Hardened MCP Server
by dhaanish597
README.md
# Security-Hardened MCP Server
A TypeScript Model Context Protocol server built to be defensible in an interview:
every security control is backed by an adversarial test or a committed artifact, or
it is listed as an open finding. Built on the official `@modelcontextprotocol/sdk`
(Streamable HTTP + stdio), Express 5, and Zod.
> Status: **M1 (schema validation)** — strict input rejection and output validation are
> proven (T05 CLOSED): every registered tool's `inputSchema` is a `.strict()` Zod object
> fed directly to the SDK, so unknown/extra keys, wrong types, and out-of-range values are
> rejected against the raw JSON-RPC arguments, and output is strict-validated before it
> leaves the server. Authentication, scopes, rate limiting, and the remaining eight tools
> are still open — all other threat-model rows remain OPEN.
## Architecture
```
requestId → originCheck → bodyLimit → auth → httpRateLimit → concurrencyCap
→ StreamableHTTPServerTransport → McpServer dispatch
→ withGuards: authz → rate limit → arg cap → timeout
→ output validate → sanitize → audit
```
(As of M1, `withGuards` steps 4/6/7/9 — arg byte cap, output validate, sanitize, and
structured error mapping — are live. The Express-layer middleware chain above
`McpServer dispatch` is still ordered stubs beyond `requestId` and body parsing.)
## Tools (12 planned)
| Tool | Scope | Demonstrates | Status |
|---|---|---|---|
| `echo` | `echo:call` | connectivity / baseline | Implemented |
| `get_time` | `time:read` | — | Implemented |
| `hash_text` | `hash:compute` | — | Implemented |
| `uuid_generate` | `uuid:generate` | — | Implemented |
| `json_validate` | `json:validate` | hostile-schema handling (schema-of-death) | Implemented |
| `text_summarize_stats` | `text:analyze` | — | Not yet implemented |
| `unit_convert` | `unit:convert` | — | Not yet implemented |
| `url_metadata` | `url:read` | SSRF defense (T06/T07) | Not yet implemented |
| `math_eval` | `math:eval` | AST sandbox (T09) | Not yet implemented |
| `password_strength` | `password:analyze` | — | Not yet implemented |
| `regex_test` | `regex:test` | ReDoS defense (T08) | Not yet implemented |
| `kv_store` | `kv:read` / `kv:write` | per-principal isolation (T10) | Not yet implemented |
## Threat model
See [docs/THREAT-MODEL.md](docs/THREAT-MODEL.md). T05 is CLOSED as of M1; all other rows
remain OPEN.
## How to run
Prerequisites: Node 22 (`nvm use`), Docker.
```bash
npm ci
# HTTP (Streamable HTTP on :3000)
MODE=http PORT=3000 npm run dev
curl -s localhost:3000/healthz # -> {"status":"ok"}
# stdio (JSON-RPC over stdin/stdout; logs go to stderr)
npm run build
MODE=stdio node dist/index.js
# Docker
docker build -t security-hardened-mcp-server:latest .
docker run --rm -e MODE=http -p 3000:3000 security-hardened-mcp-server:latest
# or: docker compose up --build
```
Inspect with MCP Inspector:
```bash
# HTTP: run the server, then open the Inspector and connect Streamable HTTP to
# http://localhost:3000/mcp
npx @modelcontextprotocol/inspector
# stdio: Inspector launches the server itself
MODE=stdio npx @modelcontextprotocol/inspector node dist/index.js
```
## Known limitations (M1)
- Still no authentication, no scopes, no rate limiting — `withGuards` steps 1–3 are
`TODO(M2)`/`TODO(M3)`; per-tool authorization (scope enforcement) lands in M2.
- The per-tool arg byte-cap control (`withGuards` step 4) exists and is unit-tested, but
its dedicated adversarial proof, T04, lands in M3.
- 5 of 12 tools implemented (`echo`, `get_time`, `hash_text`, `uuid_generate`,
`json_validate`); the remaining seven, and threat-model rows T06–T15, land in M2–M4.
- Not deployed; single-host only.
## M1 Claim Audit
| Resume phrase | Artifact | Verdict |
|---|---|---|
| "12 authenticated tools" | 5 of 12 tools exist (`echo`, `get_time`, `hash_text`, `uuid_generate`, `json_validate`); no auth yet — `withGuards` steps 1–2 are `TODO(M2)`, calls run with `principal: null` | **NOT YET** |
| "schema validation" | Strict input rejection is genuine and server-side: `register()` feeds the SDK `.strict()` objects, so unknown/extra keys, wrong types, missing required fields, out-of-range values, and deeply-nested payloads are rejected against the RAW arguments; output is strict-validated (step 6); `json_validate` caps a hostile schema's depth/size; errors are internal-detail-free (step 9). `T05_schema_rejection.test.ts` is green and **fails when strict registration or the complexity guard is removed** (delete-the-control drill recorded). | **PROVEN** |
| "rate limiting" | none yet (M3); `withGuards` step 3 + `rateLimit.ts` do not exist | **NOT YET** |
| "load-tested to 50+ concurrent" | none yet (M5) | **NOT YET** |
"Schema validation" is now the one PROVEN phrase; the other three remain NOT YET as scoped.
The per-tool arg **byte-cap** control (step 4) is implemented and unit-tested but its full
adversarial proof is T04 (M3), so it is not yet claimed as an independently-proven line.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues