MCP Server Template
by RUverse
README.md
# MCP Skill-and-Tools Bundle Template
A Node.js implementation of an MCP (Model Context Protocol) server built on the
**2026-07-28** specification: stateless, per-request metadata, Streamable HTTP.
## Features
- **MCP 2026-07-28 only**: stateless core, `server/discover`,
`subscriptions/listen`, `resultType`, cacheable list results,
request-metadata headers — no handshake-era code paths to maintain
- **Dynamic Tool Loading**: automatically discovers and loads tools from `/tools`
- **Typed tool results**: `outputSchema` + `structuredContent`, input validation
- **Canonical skill metadata** loaded from `skill/SKILL.md` into discovery
- **Bearer authentication** with an explicitly configured production secret
- **Production container** running as a non-root user with a health check
- **Sample Tools**: calculator and timestamp for demonstration
## What changed in 2026-07-28
The 2026-07-28 revision made MCP a stateless request/response protocol. This
server implements that revision and nothing older — a client on 2025-11-25 or
earlier gets a `400` telling it which version to use. If you are pointing an
existing client at this server, these are the changes that matter:
| Removed | Replacement |
| --- | --- |
| `initialize` / `notifications/initialized` | Per-request `_meta` on every request |
| `Mcp-Session-Id` header, `DELETE /mcp` | No protocol sessions — pass explicit handles as tool arguments |
| `GET /mcp` SSE stream, `resources/subscribe` | `subscriptions/listen` (one long-lived POST-response stream) |
| `ping`, `logging/setLevel`, `notifications/roots/list_changed` | Removed; log level is per-request via `_meta` |
| `Last-Event-ID` resumability, SSE event ids | None — re-issue the request with a new id |
| Server-initiated `sampling/createMessage`, `elicitation/create`, `roots/list` | Multi Round-Trip Requests (`resultType: "input_required"`) |
| JSON-RPC batching | One JSON-RPC message per POST |
Added: `server/discover`, the required `resultType` field on every result,
`ttlMs`/`cacheScope` on list results, required `MCP-Protocol-Version` /
`Mcp-Method` / `Mcp-Name` headers, `x-mcp-header` tool parameters, and the
`-32020`/`-32021`/`-32022` error codes. Roots, Sampling and Logging are now
deprecated and are not advertised by this server.
## Quick Start
1. **Describe the skill:** edit `skill/SKILL.md`. Its frontmatter becomes the
server identity and marketplace description; its Markdown body becomes
`server/discover.instructions`.
2. **Add or edit tools:** place one module per tool under `tools/`.
3. **Install dependencies:**
```bash
npm install
```
4. **Set up environment and replace the example Bearer key:**
```bash
cp .env.example .env
```
5. **Run the tests:**
```bash
npm test
```
Run `npm run test:container` when Docker is available to verify non-root
startup, the image health check, and failure when `MCP_API_KEY` is missing.
6. **Start the server:**
```bash
npm start
```
Server runs on `http://127.0.0.1:3202`.
## 🔐 Authentication
Set `MCP_API_KEY` to a non-placeholder secret before the process starts. The
server exits immediately when it is absent or still set to the example value.
MCP requests accept credentials only through the standard Bearer header:
```bash
curl -X POST -H "Authorization: Bearer your-api-key" http://127.0.0.1:3202/mcp
```
Query-string credentials and `X-API-Key` are rejected. A 401 carries a plain
`WWW-Authenticate: Bearer` challenge. OAuth is intentionally not advertised by
this template until a complete OAuth 2.1/OIDC implementation is provided.
## 📡 Talking to the server
Every request carries its protocol version, client identity and capabilities in
`params._meta`, and mirrors `method` (and `name`/`uri`) into HTTP headers. A
mismatch between headers and body is rejected with `400` and error `-32020`.
### Discovery
```bash
curl -X POST http://127.0.0.1:3202/mcp -H "Authorization: Bearer your-api-key" -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "MCP-Protocol-Version: 2026-07-28" -H "Mcp-Method: server/discover" -d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
```
### Calling a tool
`tools/call` additionally requires the `Mcp-Name` header, matching `params.name`:
```bash
curl -X POST http://127.0.0.1:3202/mcp -H "Authorization: Bearer your-api-key" -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "MCP-Protocol-Version: 2026-07-28" -H "Mcp-Method: tools/call" -H "Mcp-Name: calculator" -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"calculator","arguments":{"operation":"multiply","operand1":7,"operand2":8},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
```
Response:
```json
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"resultType": "complete",
"content": [{ "type": "text", "text": "{\"operation\":\"multiply\",\"operands\":[7,8],\"result\":56}" }],
"structuredContent": { "operation": "multiply", "operands": [7, 8], "result": 56 },
"_meta": { "io.modelcontextprotocol/serverInfo": { "name": "mcp-utility-tools", "version": "2.0.0" } }
}
}
```
Add `"progressToken"` to `_meta` and send `Accept: text/event-stream` to get the
response as a stream with `notifications/progress` ahead of the result.
### Change notifications
```bash
curl -N -X POST http://127.0.0.1:3202/mcp -H "Authorization: Bearer your-api-key" -H "Content-Type: application/json" -H "Accept: text/event-stream" -H "MCP-Protocol-Version: 2026-07-28" -H "Mcp-Method: subscriptions/listen" -d '{"jsonrpc":"2.0","id":1,"method":"subscriptions/listen","params":{"notifications":{"toolsListChanged":true},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
```
The first message is `notifications/subscriptions/acknowledged`, echoing the
filters the server will honour. Every message on the stream is tagged with
`io.modelcontextprotocol/subscriptionId`. Run with `MCP_WATCH_TOOLS=true` and
edit a file in `tools/` to see `notifications/tools/list_changed` arrive.
Closing the stream is the cancellation signal — there is no `DELETE`.
## 🛠️ Available Tools
| Tool | Description |
| --- | --- |
| `calculator` | Add, subtract, multiply, divide — returns `structuredContent` |
| `timestamp` | Current time as ISO 8601, Unix epoch, or human-readable |
## ⚙️ Creating Your Bundle
The repeatable workflow is: edit `skill/SKILL.md`, add modular tools, run
`npm test`, deploy the server over public HTTPS, then use **Connect & import**
in RUverse. The default bundle already satisfies RUverse's name, 20–240
character description, and 2,000-character instruction limits.
### 1. Tool File Structure
Create `tools/your-tool.js`:
```javascript
const TOOL_DEFINITION = {
name: "your_tool",
title: "Your Tool",
description: "What your tool does",
inputSchema: {
type: "object",
properties: {
param1: { type: "string", description: "Parameter description" }
},
required: ["param1"],
additionalProperties: false
},
// Optional but recommended: lets clients validate structuredContent.
outputSchema: {
type: "object",
properties: { result: { type: "string" } },
required: ["result"]
}
};
async function execute(args = {}, context = {}) {
const { param1 } = args;
// context.reportProgress({ progress, total, message }) streams progress
// when the client sent a progressToken.
// context.signal aborts when the client closes the stream.
// context.clientInfo / context.clientCapabilities describe the caller.
const structuredContent = { result: `Processed: ${param1}` };
return {
content: [{ type: "text", text: JSON.stringify(structuredContent) }],
structuredContent
};
}
module.exports = { definition: TOOL_DEFINITION, execute };
```
Arguments are validated against `inputSchema` before `execute` runs. Throwing
from `execute` produces a tool execution error (`isError: true`) rather than a
JSON-RPC error, so the model can self-correct.
### 2. Auto-Loading
Save the file in `/tools` and restart the server (or set `MCP_WATCH_TOOLS=true`
to hot-reload and notify subscribers).
### 3. Stateful tools
MCP has no protocol-level session. If a tool needs state across calls, return an
opaque handle and accept it as an argument on later calls — document its
lifetime in the tool description so the model knows when to create a new one.
### 4. Asking the client for input (MRTR)
Instead of sending a server-initiated `elicitation/create` request, return an
input-required result and let the client retry:
```javascript
return {
resultType: "input_required",
inputRequests: {
github_login: {
method: "elicitation/create",
params: {
mode: "form",
message: "Please provide your GitHub username",
requestedSchema: {
type: "object",
properties: { name: { type: "string" } },
required: ["name"]
}
}
}
},
// Anything you need to resume; it comes back on the retry.
requestState: "..."
};
```
The retry arrives as a new `tools/call` with `context.inputResponses` and
`context.requestState` populated.
### 5. Exposing a parameter as an HTTP header
Annotate a primitive, statically reachable property with `x-mcp-header` so
intermediaries can route on it without parsing the body:
```javascript
region: { type: "string", description: "...", "x-mcp-header": "Region" }
```
Conforming clients then send `Mcp-Param-Region: us-west1`, and the server
rejects any request where the header and the argument disagree. Never annotate
secrets — header values are visible to every intermediary on the path.
## 🔗 MCP Client Integration
```json
{
"mcpServers": {
"template": {
"type": "http",
"url": "http://127.0.0.1:3202/mcp",
"headers": {
"Authorization": "Bearer your-api-key"
}
}
}
}
```
## Production deployment
Build and run the included non-root container behind a public HTTPS reverse
proxy. Never bake the Bearer secret into the image or repository:
```bash
docker build -t my-mcp-bundle .
docker run --read-only --tmpfs /tmp --cap-drop=ALL \
-e MCP_API_KEY='replace-with-a-long-random-secret' \
-p 127.0.0.1:3202:3202 my-mcp-bundle
```
Terminate TLS at your ingress, proxy a public URL such as
`https://mcp.example.com/mcp` to port 3202, and pass the same key to RUverse as
the endpoint's Bearer credential. Keep `/health` available to the container
platform. Configure `MCP_ALLOWED_ORIGINS` only when browser clients need direct
access. The default `private` cache scope prevents authenticated discovery and
tool lists from being shared across authorization contexts.
## 📁 Layout
```
mcp-server.js Express app, routing, request validation, dispatch
skill/SKILL.md Canonical marketplace metadata and runtime instructions
lib/skill.js Startup loader and RUverse constraint validation
lib/protocol.js Version constants, _meta keys, error codes, message builders
lib/headers.js Request-metadata headers, base64 sentinel, x-mcp-header
lib/schema.js Tool argument validation
lib/sse.js SSE response streams (no resumability, per spec)
lib/subscriptions.js subscriptions/listen stream management
lib/security.js Origin validation and Bearer-only authentication
tools/ Auto-loaded tools
test/ Protocol, bundle, security, and official-client tests
Dockerfile Non-root production image with /health check
```
## Endpoints
| Endpoint | Purpose |
| --- | --- |
| `POST /mcp` | The MCP endpoint — the only method it accepts |
| `GET /mcp`, `DELETE /mcp` | `405`: the standalone SSE stream and session termination are gone |
| `GET /health` | Status, protocol version, open subscriptions |
| `GET /tools/config` | Tool definitions for wiring into an agent config |
## License
MIT — see [LICENSE](LICENSE).
This server is provided as-is for demonstration purposes. Please review and
enhance security measures before production use.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues