This repository provides a complete MCP server implementation that
- demonstrates all MCP protocol features (tools, resources, prompts, sampling)
- implements OAuth 2.0 authentication using the recommended separate auth server architectural pattern
- serves as a learning resource and starting template for building your own MCP servers
The Model Context Protocol enables seamless integration between AI applications and external data sources, tools, and services.
- Quick Start
- MCP Features
- Load-test tools
- Load-test deployment
- Development Setup
- Project Structure
- Documentation
- Contributing
- License
To start exploring ASAP:
# Clone and install
git clone https://github.com/modelcontextprotocol/example-remote-server.git
cd example-remote-server
npm install
# Start the server with in-process auth and in-memory session management
npm run dev:internal
# In another terminal, run MCP Inspector
npx -y @modelcontextprotocol/inspector
# Inspector will open a browser window.
# Connect to http://localhost:3232/mcp to authenticate and explore server featuresThe server is now running a lightweight config with everything bundled in a single process:
- authentication is handled by an in-process module, rather than a separate server
- sessions are stored in memory, rather than in Redis
Other configurations are available: see Development Setup, below.
This server implements the complete MCP specification:
- Tools: 7 example tools including echo, add, long-running operations, and LLM sampling
- Resources: 100+ example resources with pagination and subscription support
- Prompts: Simple and complex prompts with argument support
- Sampling: LLM interaction capabilities
- Elicitation: User input elicitation with various field types
- Transports: Both Streamable HTTP (recommended) and SSE (legacy)
This fork adds one module, src/modules/mcp/services/loadtest-tools.ts, registered from
createMcpServer(). Its tools exist to exercise a governing proxy in front of the server —
argument-level policy, awkward return shapes, deterministic failures, transport-level chaos
and server-side state.
Everything variable is deterministic. No Math.random and no wall-clock value decides
what a tool returns: variation is derived from fnv1a(sessionId + ":" + cursor), where the
cursor is that session's invocation count. Tools that vary accept an optional cursor
argument that pins the seed, so any observed behavior can be replayed exactly without
re-running a load test.
| Tool | Behavior |
|---|---|
ping |
Tiny, fast, always succeeds |
classify |
Echoes its arguments; schema covers a string enum, a number bounded 0..1, a boolean, a string array, and a nested object with optional enum and bounded-integer fields |
chaos_text |
Deterministic text of size (1kb/100kb/1mb/4mb) after a latency bucket (0ms/50ms/1s/10s/near_timeout = 55s) |
chaos_image |
Deterministic RGB-noise PNG of size (tiny 32px … huge 1024px, ~3MB), optionally with_text for a mixed text+image result |
chaos_result |
shape: empty (no content blocks), unicode (emoji, RTL overrides, control characters including NUL, quote/backslash bait, JSON/script bait, combining marks, escaped lone-surrogate text), or nested to depth |
chaos_fail |
mode: tool_error (isError result), client_error (JSON-RPC -32602, 400-shaped), server_error (-32603, 500-shaped), intermittent (fails when the cursor is a multiple of every_n, default 17) |
deep_nest |
Accepts a five-level nested argument and returns a result nested to depth (0..32) |
enormous_schema |
Declares 250 optional fields; echoes which were provided |
bulk_op_00 … bulk_op_39 |
Filler tools that make tools/list large (67 tools, ~63KB) |
vanishing |
Listed until its first call in a session, then absent from tools/list |
mutating_schema |
Its schema alternates between listings, and the handler validates the current version — a client that lists then calls sees a mismatch |
unauthorized |
Answered by middleware with HTTP 401 after authentication succeeded (mode: always or every_n) |
close_connection |
Answered by middleware, which writes bytes_first bytes and destroys the socket |
deterministic_sample |
Reports seed, cursor and derived samples, so reproducibility is directly observable |
counter |
{"op": "increment" | "read"}; state is scoped to one MCP session |
render_image, oversized_text, always_fails, slow_echo |
Named single-purpose tools: a small PNG, >1MB of text, an isError result, and a 12-second sleep |
State (counter, invocation cursor, vanished flag, schema version) is keyed by MCP session id and disposed when the session's server is cleaned up, so it persists across calls within a session and is shared with nothing else.
unauthorized and close_connection cannot be expressed as tool results, so the module also
exports loadTestTransportChaos, an Express middleware mounted on POST /mcp after the
bearer-auth middleware — the 401 and the dropped socket happen only once real authentication
has passed. Nothing in the server's auth was weakened to make them possible. On the legacy
SSE endpoint, where that middleware is not mounted, both tools return a text result saying so.
scripts/test-loadtest-tools.sh proves the whole path against a locally started server:
dynamic client registration, PKCE (S256, 64-character verifier), authorization, token
exchange, initialize, tools/list, and a tools/call exercising every behavior above. For
determinism it asserts that two deterministic_sample calls pinned to the same cursor return
identical seed, samples and derived word, that a different cursor differs, and that
chaos_text pinned to the same cursor is identical by md5 of the returned text. It requires
jq and openssl.
./scripts/test-loadtest-tools.sh # defaults to PORT=8090
PORT=9100 ./scripts/test-loadtest-tools.sh
RUN_NEAR_TIMEOUT=1 ./scripts/test-loadtest-tools.sh # also exercises the 55s bucketSingle-command run on a VPS, with in-process auth and in-memory sessions:
npm ci && npm run build && AUTH_MODE=internal PORT=3232 BASE_URI=https://mcp.example.com NODE_ENV=production npm startPORT— port the Node process binds (all interfaces).BASE_URI— the public HTTPS origin clients reach. It is what the server advertises in its OAuth metadata and audience checks, so it must be the external URL, notlocalhost.AUTH_MODE=internalkeeps the demo OAuth server in the same process; no second service.
TLS terminates in front of this process. The server speaks plain HTTP; run caddy or nginx
as a reverse proxy that holds the certificate for BASE_URI and forwards to PORT. Do not
expose the Node port directly — DCR and the token endpoint are unauthenticated by design in
this demo server.
Dependencies of that command, stated honestly: Node >= 20.16, and nothing else. Redis is
optional; without REDIS_URL the server falls back to in-memory sessions, which means a
single process only — sessions are lost on restart and cannot be shared across instances. For
a multi-process load test, run docker compose up -d redis and add REDIS_URL=redis://localhost:6379.
Size the box for the big payloads. A single chaos_text at 4mb or chaos_image at
huge exists transiently three to four times over — the generated buffer, its JSON-encoded
copy, and the transport's copy on the way out — so budget roughly 12–16MB of transient heap
per concurrent multi-MB call, on top of Node's baseline. Concurrency on those two sizes is the
memory limit of this deployment, not CPU.
Deliberate, so the fork stays shallow — not defects to fix here:
- Session state lives in a module-level map keyed by session id. It is disposed with the session's server, but a session abandoned without a clean close leaves its (tiny) entry behind, and the map is otherwise unbounded.
- The transport chaos middleware keys on the
Mcp-Session-Idheader and does not re-check session ownership; the upstream handler does that a moment later. Requests with no session header share one"no-session"bucket, which in practice only pre-initialize requests use. enormous_schema's 250 fields are decorative: the handler reports which were provided and does not enforce their bounds.- The
bulk_op_*filler tools exist to maketools/listlarge; the proof script calls only a couple of them.
- Node.js >= 16
- npm or yarn
- TypeScript (installed automatically via npm install, required for building)
- Docker (optional, for Redis)
The codebase supports a number of configurations ranging from simple/exploratory to something closer to how a production deployment would look.
| Development/Exploration | Productionesque | |
|---|---|---|
| Auto-restart | npm run dev:* • Auto-restarts on file changes • Verbose logging • Source maps enabled |
npm run start:* • Requires build step first • Optimized performance • No auto-restart |
| Auth Mode | internal • OAuth in same process • Single port (3232) • Easier to debug |
external • Separate auth server • Multiple ports (3001 + 3232) • Can point to commercial auth provider instead |
| Session Storage | In-memory • No dependencies • Sessions lost on restart • Single instance only |
Redis • Requires Docker/Redis • Sessions persist • Multi-instance ready |
Server configuration is determined by environment variables. To set up a non-default configuration, copy .env.example to .env and edit as desired, or pass non-defaults on the command line.
Some example commands for different configurations are listed below. See the Authentication Config and Session Management Config sections below for detailed instructions on changing those configurations.
# Development mode - watches for file changes and auto-restarts
npm run dev:internal # Internal auth
# or
npm run dev:external # External auth
# Production mode - optimized build, no auto-restart
npm run build # Build TypeScript to JavaScript first
# then
npm run start:internal # Internal auth
# or
npm run start:external # External auth
# Redis-backed sessions
docker compose up -d # Start Redis first
# configure REDIS_URL or pass on command line - see Session Management Config below - e.g.
REDIS_URL=redis://localhost:6379 npm run dev:internal
# Sessions will now persist across restarts
# Verify Redis is being used
npm run dev:internal 2>&1 | grep -i redis
# Should show: "Redis client connected successfully" or similarThis repo implements the separate auth server architecture pattern described in the MCP specification, in which the MCP server is the "resource server", and authorization functionality is hosted separately. (The architecture in which resource and authorization server functions are tightly integrated within the MCP server is deprecated, and is not demonstrated in this codebase.)
For convenience and simplicity during exploration, the server supports an internal auth mode, in which OAuth 2.0 endpoints are hosted in the same process as the MCP server. However, it remains architecturally separate from the MCP server itself: there is no entanglement of MCP and authorization functionality in the codebase. To run the server in this mode, use npm run dev:internal.
External auth mode is the standard configuration in which the MCP server and authentication servers run as separate processes. A demonstration authorization server is provided in this repo, and you can also point to commercial providers like Auth0 or Okta by updating the relevant config options. To run the MCP server in external mode, use npm run dev:external: this command will also start the separate demo auth server.
Note: choice of mode and OAuth server does not affect the MCP server's interaction with clients during authorization. It simply determines the authorization server endpoints returned in Protected Resource Metadata.
Authentication Environment Variables:
-
AUTH_MODE- Sets the authentication mode:internal(default) - Auth endpoints run in-process with the MCP serverexternal- Auth endpoints run on a separate server
-
AUTH_SERVER_URL- URL of the external auth server (required whenAUTH_MODE=external, ignored whenAUTH_MODE=internal)- Example for local demo:
http://localhost:3001 - Example for Auth0:
https://your-tenant.auth0.com - Example for Okta:
https://your-domain.okta.com
- Example for local demo:
By default, the server uses in-memory session storage for development and local single-session testing. This simplifies getting the server up and running for exploration, but confines sessions to a single server instance and destroys them on server restarts.
For multi-instance testing and persistent sessions, the server also supports Redis-managed session storage.
Setting up Redis:
-
Install Docker (if not already installed):
- macOS: Docker Desktop for Mac
- Windows: Docker Desktop for Windows
- Linux: Docker Engine
-
Start Redis using Docker Compose:
docker compose up -d # Starts Redis in the backgroundTo stop Redis later:
docker compose down
-
Configure the server to use Redis by setting environment variables:
Session Storage Environment Variables:
-
REDIS_URL- Redis connection URL (optional)- When set: Sessions are stored in Redis (persistent across restarts)
- When not set: Sessions use in-memory storage (lost on restart)
- Default: Not set (in-memory storage)
- Example:
redis://localhost:6379(Redis default port)
-
REDIS_TLS- Enable TLS for Redis connection- Set to
1ortrueto enable TLS - Default:
0(disabled)
- Set to
-
REDIS_PASSWORD- Redis password for authentication (if required) -
NODE_ENV- Controls Redis connection failure behavior:development(default) - Server continues with warning if Redis failsproduction- Server exits if Redis connection fails
Note: Docker container config can be found in
.devcontainer/docker-compose.yml. -
As noted above, MCP Inspector is the recommended way to explore the server's capabilities:
# With server running
npx -y @modelcontextprotocol/inspector
# 1. Connect to http://localhost:3232/mcp (adjust port to match current config is needed)
# 2. Go through authorization steps
# 3. Explore OAuth authentication in the Auth tab
# 4. Test tools, resources, and prompts interactivelyThe examples/ directory contains scripts that interact with MCP endpoints directly, without use of SDK functionality. These can help build intuition for how the protocol works under the hood:
client.js- Node.js client demonstrating OAuth and MCP operationscurl-examples.sh- Shell script showing raw HTTP usage
npm run lint # Code linting
npm run typecheck # Type checking
npm test # Unit tests
npm run test:e2e # End-to-end tests.
├── src/ # Source code
│ ├── index.ts # Server entry point
│ ├── config.ts # Configuration management
│ ├── interfaces/
│ │ └── auth-validator.ts # Clean auth/MCP boundary
│ ├── modules/
│ │ ├── auth/ # Demo OAuth 2.0 implementation
│ │ │ ├── auth/ # Core auth logic and providers
│ │ │ ├── handlers/ # Mock upstream IdP handler
│ │ │ ├── services/ # Auth and Redis-backed session services
│ │ │ ├── static/ # OAuth frontend assets
│ │ │ ├── index.ts # Auth module router
│ │ │ └── types.ts # Auth type definitions
│ │ ├── mcp/ # MCP protocol implementation
│ │ │ ├── handlers/ # Streamable HTTP and SSE handlers
│ │ │ ├── services/ # MCP core and Redis transport
│ │ │ ├── index.ts # MCP module router
│ │ │ └── types.ts # MCP type definitions
│ │ └── shared/ # Shared utilities
│ │ ├── logger.ts # Logging configuration
│ │ └── redis.ts # Redis client with mock fallback
│ └── static/ # Static web assets
├── examples/ # Example client implementations
│ ├── client.js # Node.js client with OAuth flow
│ └── curl-examples.sh # Shell script with curl examples
├── docs/ # Additional Documentation
├── tests/ # Test files
├── .env.example # Environment variable template
├── docker-compose.yml # Docker setup for Redis
├── package.json # Node.js dependencies
└── tsconfig.json # TypeScript configuration
Additional documentation can be found in the docs/ directory:
- OAuth Implementation - Complete OAuth 2.0 + PKCE guide with architecture, flows, and commercial provider integration
- Session Ownership - Multi-user session isolation and Redis-backed ownership tracking
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
MIT License - see LICENSE file for details.