nest-mcp-learning-server
README.md
# NestJS MCP Learning Server
A deliberately small [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server built with NestJS. It teaches how a remote MCP server exposes documentation as a **resource** and safe operations as **tools**.
The server documents and reads from [JSONPlaceholder](https://jsonplaceholder.typicode.com/). Codex is the AI/MCP client, so this project needs no model SDK, API key, frontend, or database.
## Architecture
```mermaid
flowchart LR
Codex["Codex MCP client"] -->|"Streamable HTTP + bearer token"| Nest["NestJS /mcp"]
Nest --> Resource["API documentation resource"]
Nest --> Tools["Three read-only tools"]
Tools --> API["JSONPlaceholder REST API"]
```
One MCP request follows this path:
1. Codex sends an MCP JSON-RPC request to `POST /mcp`.
2. `McpAccessGuard` checks the bearer token and any supplied `Origin`.
3. `McpController` creates a fresh server and Streamable HTTP transport.
4. `McpServerFactory` registers the resource and tools.
5. A selected tool calls `JsonPlaceholderService`.
6. The controller returns the MCP response and closes the server and transport.
Creating a fresh transport per request makes the server **stateless**. It does not issue session IDs or require sticky sessions.
## What the server exposes
### Resource
`jsonplaceholder://api-docs` is a short Markdown reference for the supported JSONPlaceholder endpoints. Resources provide context that a client can list and read without pretending the documentation is an action.
### Tools
| Tool | Input | Purpose |
| --- | --- | --- |
| `list_posts` | `userId?`, `limit = 3` | Lists at most 10 posts, optionally filtered by user. |
| `get_post` | `id` from 1–100 | Gets one post. |
| `get_user` | `id` from 1–10 | Gets one user. |
Every tool has a Zod input schema, structured output, a five-second upstream timeout, and MCP annotations marking it read-only, non-destructive, and idempotent. There is intentionally no generic URL caller and no write tool.
## Code map
```text
src/
├── main.ts # Starts Nest on PORT
├── app.module.ts # Wires configuration and providers
├── health.controller.ts # Render/local health check
├── json-placeholder/
│ └── json-placeholder.service.ts # Typed REST calls and timeout
└── mcp/
├── mcp-access.guard.ts # Token and Origin checks
├── mcp.controller.ts # Streamable HTTP transport
└── mcp-server.factory.ts # Resource and tool definitions
```
The official SDK is pinned to `@modelcontextprotocol/sdk@1.29.0`. Version 1 remains the stable production line while the SDK's version 2 is pre-release.
## Run locally
Requirements: Node.js 22 or newer and npm.
```bash
npm install
cp .env.example .env
openssl rand -hex 32
```
Copy the generated token into `MCP_AUTH_TOKEN` in `.env`, then start Nest:
```bash
npm run start:dev
```
The endpoints are:
- `http://localhost:3000/health`
- `http://localhost:3000/mcp`
Check health:
```bash
curl http://localhost:3000/health
```
Opening `/mcp` in a browser is not a valid test: MCP uses JSON-RPC over HTTP and this stateless server intentionally rejects `GET`. Use an MCP client.
## Test with MCP Inspector
Run the official Inspector without installing it globally:
```bash
npx @modelcontextprotocol/inspector
```
In the Inspector UI:
1. Select **Streamable HTTP**.
2. Enter `http://localhost:3000/mcp`.
3. Add header `Authorization: Bearer <your MCP_AUTH_TOKEN>`.
4. Connect and open the Resources and Tools tabs.
5. Read `jsonplaceholder://api-docs`, then call each tool.
If the Inspector sends an `Origin` header, add that exact origin to the comma-separated `MCP_ALLOWED_ORIGINS` value. The example allows `http://localhost:6274`.
## Automated verification
```bash
npm run lint
npm test
npm run build
```
The end-to-end test starts Nest on a random port and connects with the official `Client` and `StreamableHTTPClientTransport`. It verifies real MCP initialization, resources, tools, structured responses, input validation, authentication, and independent stateless clients. JSONPlaceholder is mocked only in automated tests so tests stay deterministic.
## Deploy free on Render
This repository includes a [Render Blueprint](./render.yaml). Render runs the normal Nest HTTP server; MCP does not require a special hosting product.
1. Push this repository to GitHub.
2. Open [Render's Blueprint page](https://dashboard.render.com/blueprints).
3. Create a Blueprint from the GitHub repository.
4. Choose the Free instance when prompted.
5. Set `MCP_AUTH_TOKEN` to a random 32-byte hex token. Do not commit it.
6. Deploy and copy the resulting `https://<service>.onrender.com` URL.
`render.yaml` configures:
- `npm ci && npm run build` as the build command;
- `npm run start:prod` as the start command;
- `/health` as the health check;
- Node.js 22;
- `MCP_AUTH_TOKEN` as a dashboard-supplied secret.
Render Free services sleep after 15 idle minutes. Wake the service before an MCP test:
```bash
until curl -fsS https://<service>.onrender.com/health; do sleep 5; done
```
Then repeat the Inspector steps with `https://<service>.onrender.com/mcp`.
## Connect the deployed server to Codex
Keep the token out of `config.toml`. Codex can read it from an environment variable:
```bash
read -s NEST_MCP_DEMO_TOKEN
export NEST_MCP_DEMO_TOKEN
launchctl setenv NEST_MCP_DEMO_TOKEN "$NEST_MCP_DEMO_TOKEN"
codex mcp add jsonplaceholder-learning \
--url https://<service>.onrender.com/mcp \
--bearer-token-env-var NEST_MCP_DEMO_TOKEN
```
The `launchctl` step makes the variable available to GUI applications started on macOS. Fully quit and reopen Codex after setting it.
Because Render can cold start slowly, add these non-secret settings to the generated server table in `~/.codex/config.toml`:
```toml
[mcp_servers.jsonplaceholder-learning]
url = "https://<service>.onrender.com/mcp"
bearer_token_env_var = "NEST_MCP_DEMO_TOKEN"
startup_timeout_sec = 90
tool_timeout_sec = 30
```
After restarting Codex, check **Settings → MCP servers** or type `/mcp`. Try:
- “Read the API documentation exposed by the JSONPlaceholder learning server.”
- “List the first three posts and summarize them.”
- “Get post 1, then find its author.”
- “Show only two posts belonging to user 3.”
- “Create a new post.” The correct response is that no write tool exists.
## Security boundaries
This is a temporary read-only learning server:
- Every `/mcp` request requires the shared bearer token.
- Requests with an `Origin` are accepted only when explicitly allowed.
- Tool inputs cannot supply arbitrary URLs.
- Result sizes and IDs are bounded.
- Upstream errors are replaced by a safe tool error.
- Secrets are excluded from Git and from MCP results.
A shared token is intentionally simpler than production authorization. For user-specific or sensitive data, implement the MCP OAuth 2.1 authorization model, per-user authorization, auditing, rate limiting, and secret rotation.
## Troubleshooting
- **`401 Unauthorized`**: Codex/Inspector is missing the token or it differs from Render.
- **`403 Forbidden`**: add the exact supplied Origin to `MCP_ALLOWED_ORIGINS`.
- **Codex says the server timed out**: wake `/health`, wait for `200`, and keep `startup_timeout_sec = 90`.
- **Browser shows 405 on `/mcp`**: expected; use Inspector or Codex instead of browser navigation.
- **Tool returns a retry message**: JSONPlaceholder timed out or returned an error.
- **Server does not appear in Codex**: fully restart Codex after adding the server and environment variable.
## Remove the experiment
```bash
codex mcp remove jsonplaceholder-learning
launchctl unsetenv NEST_MCP_DEMO_TOKEN
unset NEST_MCP_DEMO_TOKEN
```
Then suspend or delete the Render service. Rotating or deleting `MCP_AUTH_TOKEN` immediately prevents further MCP access.
## Primary references
- [MCP Streamable HTTP specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports)
- [Official TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
- [Official stateless Streamable HTTP example](https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.x/src/examples/server/simpleStatelessStreamableHttp.ts)
- [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector)
- [JSONPlaceholder guide](https://jsonplaceholder.typicode.com/guide/)
- [Render Free documentation](https://render.com/docs/free)
- [Codex MCP documentation](https://learn.chatgpt.com/docs/extend/mcp)
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues