Skip to content
Conduit

mcp server

mcp-rest-bridge

Production MCP server template for REST APIs · JWT auth · field filtering · 26-scenario LLM-as-judge adversarial test suite

nlorber/mcp-rest-bridgecommunitylistedunreviewed

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, 2026

mcp-rest-bridge

CI Coverage Node TypeScript License

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 + setRequestHandler to 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

ToolDescription
list_itemsList items with pagination, search, and filtering
get_itemGet detailed item info by ID
create_itemCreate a new item
update_itemUpdate an existing item
delete_itemDelete an item
list_categoriesList all categories
get_categoryGet category details by ID

Prompts

PromptDescriptionArguments
summarize-entitySummarize an item or categoryentity_type (required), entity_id (required)
generate-reportGenerate an inventory reportreport_type (required), format (optional)

Resources

URIDescription
config://server/settingsNon-sensitive server configuration
api://mock/specMock API endpoint specification
prompt://templates/{id}Prompt templates with metadata

Security Model

See docs/SECURITY.md for the full security model. Key features:

  1. 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
  2. Response instructions — embedded guidance in every tool response (overridable per tool)
  3. Server instructions — LLM guidance in MCP capabilities
  4. Input validation — Zod schemas on every tool argument
  5. Credential security — JWT auto-refresh; secrets never reach the LLM
  6. Error handling — internal details and stack traces are never exposed to the LLM
  7. 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, the forbidden patterns 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

↑↓ navigate openesc close