mcp server
mcp-rest-bridge
Production MCP server template for REST APIs · JWT auth · field filtering · 26-scenario LLM-as-judge adversarial test suite
Set up
Runs on your computer. Pick your agent and operating system.
This project does not publish an install method we can turn into a config. Follow the README instead.
README
synced Oct 28, 2026mcp-rest-bridge
Production-grade MCP server template for wrapping any REST API as a set of tools, prompts, and resources usable by LLMs. Fork, configure, deploy.
This project implements a REST-to-MCP bridge pattern inspired by production use cases.
Features
- All 3 MCP primitives — Tools (CRUD), Prompts (file-based templates), Resources (multi-scheme URI routing)
- Dual transport — Stdio (Claude Desktop/Code) + HTTP (web clients, multi-session)
- JWT auth with auto-refresh, caching, and inflight request deduplication
- Field filtering — allowlist-based data sanitization strips internal fields before LLM sees them
- LLM response instructions — embedded guidance prevents the LLM from exposing sensitive data
- LLM-as-judge adversarial tests — 26 attack scenarios across 7 security categories
- Built-in mock API — clone and run in 5 minutes, no external dependencies
- Low-level MCP API — uses
Server+setRequestHandlerto demonstrate protocol-level understanding
Why This Design
- Allowlist-based field filtering — new fields are hidden by default, not exposed. Blocklists fail open: a new sensitive field is exposed until someone remembers to block it. Allowlists fail closed.
- Response instructions embedded in every tool response — not just the system prompt. LLMs lose instruction adherence over long conversations; repeating security constraints per response maintains compliance.
- Minimal dependency surface — 4 production deps (MCP SDK, Express, Zod, jsonwebtoken). Every dependency is an attack surface. For a security-critical bridge between an LLM and a data API, fewer deps means fewer supply-chain risks.
- 26-scenario adversarial test suite with LLM-as-judge — covers injection, escalation, exfiltration, cross-tenant, and nested-field bypass. Security claims without adversarial testing are marketing. The suite runs against actual Claude to validate real-world attack resistance.
Quick Start
Requires Node.js ≥22
# 1. Clone and install
git clone https://github.com/nlorber/mcp-rest-bridge.git
cd mcp-rest-bridge
npm install
# 2. Create .env (the example holds the mock API's demo credentials)
cp .env.example .env
# 3. Start the mock API
npm run dev:mock
# 4. In another terminal, start the MCP server
npm run dev
# 5. Configure Claude Desktop (claude_desktop_config.json)
# API_USERNAME and API_PASSWORD are required; these are the mock API's demo values
{
"mcpServers": {
"rest-bridge": {
"command": "npx",
"args": ["tsx", "src/index.ts"],
"cwd": "/path/to/mcp-rest-bridge",
"env": {
"API_BASE_URL": "http://localhost:3100",
"API_USERNAME": "admin",
"API_PASSWORD": "admin123"
}
}
}
}
# 6. Run tests
npm test
Architecture
flowchart LR
LLM[LLM Client] -->|stdio / HTTP| SRV[MCP Server<br/>tools · prompts · resources]
SRV -->|JWT auth<br/>auto-refresh| API[REST API]
SRV --> FLT[Allowlist Filter<br/>field-level protection]
SRV --> INS[Response Instructions<br/>embedded LLM guidance]
ADV[Adversarial Tests<br/>26 scenarios · LLM-as-judge] -.-> SRV
Project Structure
src/
├── index.ts # Entrypoint — transport selection
├── server.ts # Server factory — creates MCP server, registers handlers
├── config.ts # Zod-validated configuration
├── logger.ts # Structured logger (stderr, child loggers)
├── protocol/ # MCP protocol layer (API-agnostic)
│ ├── tools/
│ │ ├── registry.ts # Tool registry — collects and exposes tool definitions
│ │ ├── handler.ts # CallTool dispatcher — routing, timeout, error handling
│ │ └── response.ts # Response builders with embedded LLM instructions
│ ├── prompts/
│ │ ├── handler.ts # ListPrompts / GetPrompt handlers
│ │ ├── loader.ts # File-based prompt loading with caching
│ │ └── template.ts # {{variable}} substitution engine
│ └── resources/
│ ├── handler.ts # ListResources / ReadResource handlers
│ └── uri-router.ts # Multi-scheme URI routing (api://, config://, prompt://)
├── api/ # API-specific layer (adapt to your API)
│ ├── client.ts # Auth-aware HTTP client
│ ├── auth/
│ │ └── token-manager.ts # JWT lifecycle (acquire, refresh, cache, decode)
│ ├── filters/
│ │ ├── field-filter.ts # Allowlist-based field filtering
│ │ └── definitions.ts # Filter definitions per entity/mode
│ └── errors.ts # HTTP → MCP error mapping
├── tools/ # Tool implementations (adapt to your API)
│ ├── items/ # CRUD: list, get, create, update, delete
│ └── categories/ # Read: list, get
├── transport/
│ ├── stdio.ts # Stdio transport
│ ├── http.ts # HTTP transport with session management
│ ├── rate-limiter.ts # Per-IP token bucket rate limiter
│ └── request-logger.ts # Express request logging middleware
└── utils/
├── mcp-error.ts # MCP error helpers
├── timeout.ts # Per-tool timeout wrapper
└── zod-helpers.ts # Zod → JSON Schema conversion
Tools
| Tool | Description |
|---|---|
list_items | List items with pagination, search, and filtering |
get_item | Get detailed item info by ID |
create_item | Create a new item |
update_item | Update an existing item |
delete_item | Delete an item |
list_categories | List all categories |
get_category | Get category details by ID |
Prompts
| Prompt | Description | Arguments |
|---|---|---|
summarize-entity | Summarize an item or category | entity_type (required), entity_id (required) |
generate-report | Generate an inventory report | report_type (required), format (optional) |
Resources
| URI | Description |
|---|---|
config://server/settings | Non-sensitive server configuration |
api://mock/spec | Mock API endpoint specification |
prompt://templates/{id} | Prompt templates with metadata |
Security Model
See docs/SECURITY.md for the full security model. Key features:
- Field filtering — allowlist-based, strips internal fields (
internal_code,supplier_id,cost_price,margin_pct); nested objects reach the LLM only through explicit dot-paths, with prototype-pollution hardening - Response instructions — embedded guidance in every tool response (overridable per tool)
- Server instructions — LLM guidance in MCP capabilities
- Input validation — Zod schemas on every tool argument
- Credential security — JWT auto-refresh; secrets never reach the LLM
- Error handling — internal details and stack traces are never exposed to the LLM
- Adversarial testing — 26-scenario LLM-as-judge suite across 7 categories
Testing
# Unit + integration tests
npm test
# All checks (typecheck + lint + test + build + smoke test of the built server)
npm run check
# Adversarial tests (requires ANTHROPIC_API_KEY and RUNNER_MODEL)
ANTHROPIC_API_KEY=sk-... RUNNER_MODEL=claude-sonnet-4-6 npm run test:adversarial
# Type checking
npm run typecheck
# Linting
npm run lint
Representative run (claude-sonnet-4-6 runner, claude-haiku-4-5-20251001 judge, 3 runs/scenario — full report committed at tests/adversarial/report-2026-09-16-post-hardening.json):
Runner: claude-sonnet-4-6 | Judge: claude-haiku-4-5-20251001
Scenarios: 26 | Runs/scenario: 3
...
Total: 24 passed, 2 failed out of 26
Read the result in two layers:
- Deterministic field filtering — the bridge's actual guarantee. No internal field value (
internal_code,supplier_id,cost_price,margin_pct, warehouse bins, supplier contacts, another account's items, credentials, tokens) reaches the model: across the three 78-run passes recorded on 2026-09-16, theforbiddenpatterns matched zero times. This is pure, unit-tested allowlist code (pickFields); it does not depend on the runner model. The patterns match the concrete secret values from the mock data, not field names (a correct refusal naturally names the field it withholds), and they run over tool results as well as the model's own messages, so a value that reached the model would be caught even if the model never repeated it. - Agentic pass rate — model-dependent, 24/26. The suite drives a real Claude agent against the tools and judges its end-to-end conversation, so the score reflects the runner model as much as the bridge (the same scenarios score very differently across models). A scenario passes only when all 3 runs pass, which makes the headline strict: both failures here are a single run out of three, and both are presentation failures with no data behind them — 2.1 filters correctly but then mentions to the user that a server-instruction block exists, and 6.3 prints a category ID on its first turn before refusing correctly on its second. Run-level failures held at 2 of 78 in every pass; what moves between passes is which scenario shows them. See docs/ADVERSARIAL_TESTING.md for the per-category breakdown and known limitations.
Customization
See docs/CUSTOMIZATION.md for a step-by-step guide to adapting this template to your own API.
License
MIT