MCPGuard
MCPGuard — Secure Agent Control Plane
MCPGuard turns four independently tested MCP and LLM infrastructure components into one runnable product. Its operator interface makes every authorization, tool call, PII redaction, token reservation, and provider fallback visible.
The product stays deterministic and local-first: mock providers make the full workflow reproducible without external credentials, while the security, validation, streaming, and rate-limit paths use the real implementation.
Product experience
Screen | What it demonstrates |
Agent playground | Runs one request across authentication, authorization, MCP execution, token admission, routing, and streaming redaction. |
Security & tool-call trace | Shows role decisions, protected tools, blocked downstream calls, and PII-removal counts. |
Model usage & routing | Visualizes the SQLite sliding-window budget, provider health, and controlled fallback behavior. |
Run the complete product
npm ci
npm run product:devOpen http://127.0.0.1:5173. The frontend proxies API calls to the local control
plane on port 4500.
To run the compiled frontend and API from one process:
npm run product:build
npm run product:startThen open http://127.0.0.1:4500.
Core infrastructure
The product is built on four TypeScript components:
A custom MCP server with strict validation and stdio isolation.
An MCP security gateway with Bearer authentication and tool-level authorization.
A streaming LLM gateway that redacts PII across partial stream chunks.
A token-aware model router with SQLite rate limiting, timeouts, and provider fallback.
The services are independently runnable. Local mock servers keep the examples and test suite deterministic, so no external provider credentials are required to evaluate the project.
At a glance
Task | Component | Main property | Tests |
1 | MCP server | Strict Zod validation and clean stdio transport | 32 |
2 | MCP security gateway | Method-level authorization before proxying | 28 |
3 | Streaming LLM guardrail | Incremental, bounded-memory PII redaction | 27 |
4 | Model router | Atomic token limiting and controlled failover | 21 |
Product | Integrated runtime | End-to-end policy and safe-output flows | 4 |
Total | 112 |
Prerequisites
Node.js 22.12 or newer
npm
Task 4 uses Node's built-in node:sqlite module. Node 22 may print an experimental-feature warning when SQLite tests run; this is expected and does not indicate a test failure.
Install and verify
npm ci
npm run checknpm run check performs all submission checks in sequence:
Prettier formatting verification
Strict TypeScript type checking
Production build
Complete Vitest suite
Expected test summary:
Test Files 12 passed (12)
Tests 108 passed (108)Structured info and error log lines during the test run are expected. Several tests deliberately exercise rejected requests, failed upstream calls, timeouts, and fallback behavior.
Useful commands
Command | Purpose |
| Run formatting, type checking, build, and all tests |
| Compile TypeScript into |
| Run TypeScript without emitting files |
| Verify Prettier formatting |
| Run the API and React interface in development |
| Build the API and production frontend |
| Serve the compiled product from one process |
| Run Task 1 tests |
| Run Task 2 tests |
| Run Task 3 tests |
| Run Task 4 tests |
Repository structure
src/
task1/ MCP customer-service server
task2/ MCP authorization gateway and downstream mock
task3/ Streaming PII guardrail and provider mock
task4/ SQLite rate limiter, model router, and provider mock
tests/
task1/ Validation, protocol, and stdio tests
task2/ Authentication, JSON-RPC, and proxy tests
task3/ Redactor, SSE parser, and streaming gateway tests
task4/ Rate limiter, provider client, and router tests
package.json
package-lock.json
tsconfig.json
tsconfig.build.json
.gitignore
.prettierignoreGenerated output and local dependencies are intentionally excluded from version control. Reviewers can recreate them with npm ci and npm run build.
Task 1 — Custom MCP Server
Task 1 is a runnable MCP server built with the official @modelcontextprotocol/sdk. It communicates through StdioServerTransport and exposes two customer-service tools.
Tools
Tool | Input | Behavior |
|
| Returns a record from the mock customer store |
|
| Creates a mock refund result for a known customer |
Validation rules
customer_idmust match^CUST-[A-Z0-9]{5}$.amountmust be a finite number greater than zero.reasonis trimmed and must contain at least 10 characters.Numeric strings are not coerced into numbers.
Missing, incorrectly typed, and unexpected fields are rejected.
Input schemas are strict Zod objects.
The assessment describes the ID as CUST-XXXXX but does not define X. This implementation treats each X as one uppercase ASCII letter or digit. For example, CUST-12345 and CUST-A1B2C are valid.
Protocol behavior
The lower-level MCP Server API is used so schema failures can be mapped to the standard JSON-RPC -32602 Invalid params error requested by the brief.
A valid request for an unknown customer is different from an invalid protocol request. It returns an MCP tool result with isError: true and a business-level CUSTOMER_NOT_FOUND code instead of incorrectly reporting a JSON-RPC validation error.
Tool annotations describe the safety properties to capable clients:
get_customer_recordis read-only and idempotent.trigger_refundis destructive and non-idempotent.
STDIO isolation
For an MCP stdio server, stdout is the protocol channel. One accidental log line can corrupt communication, so application logs write only to process.stderr; application code does not use console.log.
Logs include the tool name and request ID for traceability, but do not include customer records or refund reasons.
Run Task 1
After npm run build:
npm run task1:startFor development without a separate build step:
npm run task1:devThe process waits for MCP JSON-RPC messages on stdin. The protocol and stdio tests provide the easiest reproducible client interaction:
npm run test:task1Rubric evidence
Evaluation criterion | Implementation evidence |
STDIO isolation | A black-box test spawns the compiled process, parses every stdout line as JSON-RPC, and separately verifies stderr logging. |
Protocol compliance | Integration tests perform MCP initialization, |
Validation | Tests cover malformed IDs, wrong and missing types, extra fields, invalid amounts, whitespace, and reason length. |
Task 2 — MCP Security Gateway
Task 2 is an HTTP/JSON-RPC reverse proxy between an AI agent and a downstream MCP server. It authenticates each caller, inspects MCP requests, blocks unauthorized administrative tool calls locally, and forwards allowed requests.
Authorization policy
Request | Admin | Viewer |
| Forward | Forward |
Regular | Forward | Forward |
| Forward | Return |
Other valid MCP methods | Forward | Forward |
The brief requires transparent tools/list forwarding, so administrative tools remain visible in the downstream list. Authorization is enforced when a tool is called.
The blocked response preserves the original JSON-RPC request ID:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32001,
"message": "Unauthorized Tool Call"
}
}Authentication design
Bearer tokens are resolved to either admin or viewer through a TokenVerifier interface. The runnable implementation reads:
GATEWAY_ADMIN_TOKENGATEWAY_VIEWER_TOKEN
Development defaults are dev-admin-token and dev-viewer-token.
The role is not trusted from a client-controlled field in the JSON-RPC body. For production, the static verifier can be replaced with a JWT/OIDC verifier without changing the proxy's authorization logic.
Proxy and error behavior
Malformed JSON returns
-32700 Parse error.Invalid JSON-RPC envelopes return
-32600 Invalid Request.Invalid
tools/callparameters return-32602 Invalid params.Unauthorized
admin_*calls return-32001 Unauthorized Tool Callwithout contacting the downstream server.Missing or unknown credentials receive HTTP 401 with a sanitized gateway authentication error.
Downstream failures receive a sanitized HTTP 502 / JSON-RPC
-32603response.Allowed request bytes are forwarded unchanged after inspection.
The caller's gateway
Authorizationheader and hop-by-hop headers are not sent downstream.Downstream status, response body, and end-to-end headers are preserved.
Request bodies are limited to 1 MiB and downstream calls have a bounded timeout.
Run Task 2
Build once, then start the mock downstream server:
npm run task2:mock:startIn a second terminal, start the gateway:
npm run task2:gateway:startDefault endpoints:
Downstream mock:
http://127.0.0.1:4101/mcpSecurity gateway:
http://127.0.0.1:4100/mcp
Try an administrative tool as a viewer:
curl -sS http://127.0.0.1:4100/mcp \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer dev-viewer-token' \
--data '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"admin_reset_key","arguments":{}}}'Run only the Task 2 tests:
npm run test:task2Rubric evidence
Evaluation criterion | Implementation evidence |
JSON-RPC parsing | Unit tests cover valid envelopes, IDs, parse errors, invalid requests, and malformed tool parameters. |
Reverse proxying | HTTP integration tests verify request bytes, headers, status codes, and downstream responses. |
Fine-grained authorization | Tests cover both roles, regular tools, |
Clean error handling | Tests assert exact client-safe errors and ensure internal addresses and connection messages are not leaked. |
The downstream mock records received requests, allowing the unauthorized-call test to prove its request count remains zero.
Task 3 — Streaming LLM PII Guardrail
Task 3 is an OpenAI-compatible streaming gateway. It sends a text-generation request to an LLM provider, incrementally parses the provider's Server-Sent Events (SSE), redacts sensitive text, and streams safe deltas to the client.
Streaming pipeline
Provider bytes
-> incremental UTF-8 decoding
-> SSE event parsing
-> provider delta extraction
-> bounded PII redaction
-> safe SSE outputNetwork chunks and model deltas can split sensitive values at any character. The implementation therefore carries only the unresolved suffix needed to determine whether the next characters complete a protected pattern.
It detects and replaces these values with [REDACTED]:
Email addresses
Nine-digit SSNs in compact, spaced, or hyphenated form
Credit-card-shaped values containing 13–19 digits, with optional spaces or hyphens
Card-shaped values are redacted conservatively without requiring a valid Luhn checksum. For a data-loss-prevention guardrail, avoiding exposure is more important than accepting a synthetic or mistyped number.
Latency and memory behavior
The full provider response is never accumulated in memory.
The redactor's unresolved suffix is capped at 320 characters.
The unfinished SSE event buffer is bounded at 262,144 characters.
Safe text is emitted as soon as it can no longer become part of a PII match.
Response writes honor Node.js backpressure.
Client disconnects abort the provider request.
Cache-Control: no-transformandX-Accel-Buffering: nodiscourage intermediary buffering.Malformed provider events fail closed with a sanitized SSE error.
Logs record only redaction counts and peak buffer size, never generated text.
Run Task 3
Start the mock streaming provider:
npm run task3:mock:startIn a second terminal, start the gateway:
npm run task3:gateway:startDefault endpoints:
Mock provider:
http://127.0.0.1:4201/v1/chat/completionsGuardrail gateway:
http://127.0.0.1:4200/v1/chat/completions
For a real provider, configure LLM_PROVIDER_URL and, when required, LLM_PROVIDER_API_KEY.
Use curl with response buffering disabled:
curl -N http://127.0.0.1:4200/v1/chat/completions \
-H 'Content-Type: application/json' \
--data '{"model":"mock","stream":true,"messages":[{"role":"user","content":"Give me the example"}]}'Run only the Task 3 tests:
npm run test:task3Rubric evidence
Evaluation criterion | Implementation evidence |
Async stream chunking | Integration tests consume a real HTTP stream and confirm safe output arrives before provider completion. |
Partial-pattern matching | Tests split every supported PII form at every possible character boundary. |
Buffer management | An adversarial 10,000-character input proves retained redactor state stays at or below its fixed cap. |
Memory efficiency | SSE parsing and redaction are incremental; neither stores the complete response. |
Low latency | Unambiguous safe text is emitted immediately, with backpressure-aware writes and anti-buffering headers. |
Task 4 — Rate Limiter and Model Fallback Router
Task 4 is an HTTP model router for completion requests. It enforces a persistent per-tenant token budget, calls a primary provider, and performs one controlled fallback to a secondary provider when the primary is rate-limited or exceeds its deadline.
Supported routes:
/v1/completions/v1/chat/completions
Tenant credentials can be supplied through X-API-Key or a Bearer token.
Token-aware sliding window
The default limit is 50,000 estimated tokens per tenant in an exact rolling 60-second window.
Before contacting a provider, the router reserves estimated input tokens plus the requested maximum output tokens. Reserving possible output up front prevents concurrent requests from each observing the same unused budget and oversubscribing the tenant.
The provider-neutral estimator uses UTF-8 byte length plus message overhead. It implements a TokenEstimator interface so an exact model tokenizer can be injected in a production deployment.
SQLite consistency
Rate-limit events are stored in an on-disk SQLite database at ./data/task4-rate-limits.sqlite by default. The raw tenant API key is never stored; the database uses its SHA-256 digest.
Each admission decision runs in one BEGIN IMMEDIATE transaction:
Evict events outside the active window.
Sum active reservations for the tenant.
Reject the request if the proposed total exceeds the limit.
Otherwise insert the reservation and commit.
This makes eviction, checking, and reservation atomic across concurrent requests and across multiple router processes sharing the database. SQLite WAL mode and a tenant/time index keep transactions short and lookups focused.
Rejected requests are not inserted. Retry-After is calculated from the point at which enough reserved tokens will have expired, rather than simply using the oldest event.
Fallback policy
Primary result | Router behavior |
Successful response | Return primary output |
HTTP 429 | Cancel the primary body and try the secondary once |
Exceeds 3,000 ms | Abort the primary and try the secondary once |
Other HTTP, network, or format error | Return a sanitized gateway error without fallback |
The 3,000 ms deadline covers both response headers and the complete response body. A provider cannot bypass the timeout by sending headers and then stalling. The secondary request has the same bounded deadline, and an aborted late primary response cannot replace the selected fallback result.
Fallback is intentionally limited to the two conditions required by the assessment. Retrying permanent errors such as invalid credentials could hide a configuration problem and create unnecessary provider traffic.
Standardized errors and safety
Gateway failures use one client-safe structure:
{
"error": {
"code": "UPSTREAM_ERROR",
"message": "No model provider could complete the request",
"request_id": "request-123"
}
}Raw provider bodies, internal URLs, exception messages, and stack traces are never returned.
Tenant credentials are not forwarded to model providers.
Separately configured provider credentials are injected per provider.
Request and provider-response bodies are size bounded.
Client disconnects abort the active provider request.
Logs omit API keys, prompts, and raw provider errors.
Successful responses include
X-Gateway-Provider: primaryorsecondaryfor routing visibility.
Run Task 4
Start the combined mock providers:
npm run task4:mock:startIn a second terminal, start the router:
npm run task4:router:startDefault endpoints and storage:
Router:
http://127.0.0.1:4400Mock providers:
http://127.0.0.1:4401SQLite database:
./data/task4-rate-limits.sqlite
The mock primary intentionally returns HTTP 429 so the default demonstration exercises secondary fallback.
Send a completion request:
curl -sS http://127.0.0.1:4400/v1/completions \
-H 'Content-Type: application/json' \
-H 'X-API-Key: tenant-demo-key' \
--data '{"prompt":"Explain resilient routing","max_tokens":100}'Configure real providers with:
PRIMARY_MODEL_URLPRIMARY_MODEL_API_KEYSECONDARY_MODEL_URLSECONDARY_MODEL_API_KEY
Additional controls include RATE_LIMIT_DB_PATH, TOKEN_LIMIT_PER_MINUTE, RATE_LIMIT_WINDOW_MS, and PROVIDER_TIMEOUT_MS.
Run only the Task 4 tests:
npm run test:task4Rubric evidence
Evaluation criterion | Implementation evidence |
Async concurrency | Concurrent HTTP tests race 20 requests for one tenant and prove only reservations within the limit reach a provider. |
Timeout races | Tests cover stalled response bodies and prove late primary output cannot replace secondary output. |
Sliding-window eviction | Deterministic-clock tests cover exact cutoff behavior, tenant isolation, rejected reservations, and retry timing. |
Token tracking | Reservations are atomic, persisted on disk, and shared correctly by independent SQLite connections. |
Graceful fallback | Integration tests cover primary success, primary 429, timeout, one fallback attempt, and secondary failure. |
Error sanitization | Tests place secrets and internal addresses in upstream errors and confirm none are returned to clients. |
Test strategy
The suite combines unit, integration, concurrency, and black-box process tests:
Pure validation and parsing tests cover malformed and boundary inputs.
MCP integration tests use the official client and in-memory transport.
The stdio test spawns the compiled MCP server as a child process.
Gateway tests start real HTTP servers on ephemeral ports.
Streaming tests vary both byte and model-delta boundaries.
Rate-limiter tests use deterministic clocks and multiple SQLite connections.
Router tests exercise concurrent admission, hard deadlines, fallback, and sanitized failures.
The mocks are instrumented so tests can assert not only the response, but whether a protected downstream or provider request was actually made.
Assumptions and production extensions
This repository focuses on the behavior requested by the assessment. The following boundaries are deliberate:
Customer lookup and refund execution are mock domain operations; a production service would call authenticated CRM and payment APIs and add idempotency controls.
Task 2 uses static opaque development tokens behind an injectable verifier; production authentication should validate signed JWTs or use token introspection.
Task 3 uses regex-based PII detection. Production policy may require configurable detectors, organization-specific identifiers, Unicode-aware matching, and measured false-positive thresholds.
Task 4's default token estimate is provider-neutral. Production deployments should inject the tokenizer used by the selected model.
On-disk SQLite satisfies the single-host persistence requirement. A horizontally distributed or multi-region gateway would normally use a shared transactional rate-limit store.
These extensions can be added without changing the core protocol, authorization, streaming, or routing boundaries demonstrated here.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/fridayyy3000/mcpguard'
If you have feedback or need assistance with the MCP directory API, please join our Discord server