Skip to main content
Glama

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:dev

Open 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:start

Then open http://127.0.0.1:4500.

Core infrastructure

The product is built on four TypeScript components:

  1. A custom MCP server with strict validation and stdio isolation.

  2. An MCP security gateway with Bearer authentication and tool-level authorization.

  3. A streaming LLM gateway that redacts PII across partial stream chunks.

  4. 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 check

npm run check performs all submission checks in sequence:

  1. Prettier formatting verification

  2. Strict TypeScript type checking

  3. Production build

  4. 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

npm run check

Run formatting, type checking, build, and all tests

npm run build

Compile TypeScript into dist/

npm run typecheck

Run TypeScript without emitting files

npm run format:check

Verify Prettier formatting

npm run product:dev

Run the API and React interface in development

npm run product:build

Build the API and production frontend

npm run product:start

Serve the compiled product from one process

npm run test:task1

Run Task 1 tests

npm run test:task2

Run Task 2 tests

npm run test:task3

Run Task 3 tests

npm run test:task4

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
.prettierignore

Generated 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

get_customer_record

customer_id

Returns a record from the mock customer store

trigger_refund

customer_id, amount, reason

Creates a mock refund result for a known customer

Validation rules

  • customer_id must match ^CUST-[A-Z0-9]{5}$.

  • amount must be a finite number greater than zero.

  • reason is 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_record is read-only and idempotent.

  • trigger_refund is 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:start

For development without a separate build step:

npm run task1:dev

The process waits for MCP JSON-RPC messages on stdin. The protocol and stdio tests provide the easiest reproducible client interaction:

npm run test:task1

Rubric 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, tools/list, and tools/call, preserve request IDs, and assert -32602 errors.

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

tools/list

Forward

Forward

Regular tools/call

Forward

Forward

admin_* tools/call

Forward

Return -32001 locally

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_TOKEN

  • GATEWAY_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/call parameters return -32602 Invalid params.

  • Unauthorized admin_* calls return -32001 Unauthorized Tool Call without 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 -32603 response.

  • Allowed request bytes are forwarded unchanged after inspection.

  • The caller's gateway Authorization header 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:start

In a second terminal, start the gateway:

npm run task2:gateway:start

Default endpoints:

  • Downstream mock: http://127.0.0.1:4101/mcp

  • Security 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:task2

Rubric 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, admin_* tools, and invalid credentials.

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 output

Network 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-transform and X-Accel-Buffering: no discourage 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:start

In a second terminal, start the gateway:

npm run task3:gateway:start

Default endpoints:

  • Mock provider: http://127.0.0.1:4201/v1/chat/completions

  • Guardrail 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:task3

Rubric 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:

  1. Evict events outside the active window.

  2. Sum active reservations for the tenant.

  3. Reject the request if the proposed total exceeds the limit.

  4. 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: primary or secondary for routing visibility.

Run Task 4

Start the combined mock providers:

npm run task4:mock:start

In a second terminal, start the router:

npm run task4:router:start

Default endpoints and storage:

  • Router: http://127.0.0.1:4400

  • Mock providers: http://127.0.0.1:4401

  • SQLite 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_URL

  • PRIMARY_MODEL_API_KEY

  • SECONDARY_MODEL_URL

  • SECONDARY_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:task4

Rubric 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

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