Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@MCP Streamable HTTP Server TemplateList all tools and resources you have available"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
MCP Streamable HTTP Server Template
What is this?
A template for building MCP servers. Clone it, strip what you don't need, wire your API client, define tools. It's designed to be readable and easy to build on.
Ships with dual-runtime support (Node.js and Cloudflare Workers from the same codebase), five auth strategies, encrypted token storage, and pretty much everything the latest MCP spec supports.
What is MCP?
Model Context Protocol is a JSON-RPC 2.0 wire protocol where servers expose typed capabilities (tools for actions, resources for data, prompts for templates) and clients (IDEs, agents, chat apps) invoke them based on LLM decisions.
Neither side implements the other's logic: servers know nothing about which LLM uses them, clients know nothing about how tools work internally. This decoupling solves the N×M integration problem. One server serves any compliant client, one client consumes any compliant server.
What's supported?
Feature | Node.js | Workers | Notes |
Tools (list, call) | ✅ | ✅ | Core capability, both runtimes |
Resources (list, read, templates) | ✅ | ✅ | Static and dynamic resources |
Prompts (list, get) | ✅ | ✅ | Template-based prompt generation |
Progress notifications | ✅ | ✅ | Long-running tool feedback |
Cancellation | ✅ | ✅ | AbortSignal-based |
Pagination | ✅ | ✅ | Cursor-based for large lists |
Logging | ✅ | ✅ | Server→client log messages |
Sampling (server→client LLM) | ✅ | ❌ | Requires persistent SSE stream |
Elicitation (user input) | ✅ | ❌ | Requires persistent SSE stream |
Roots (filesystem access) | ✅ | ❌ | Requires client capability check |
Protocol versions supported: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05.
Getting started
First, generate an encryption key (you'll need this for both runtimes):
Node.js
Cloudflare Workers
Server endpoints
Endpoint | Method | Purpose |
| POST, GET, DELETE | MCP protocol (JSON-RPC) |
| GET | Health check + readiness |
| GET | OAuth AS metadata |
| GET | Protected resource metadata |
| GET | Start OAuth flow |
| GET | Provider redirect target |
| POST | Token exchange |
| POST | Dynamic client registration |
| POST | Token revocation |
Discovery endpoints also available under /mcp/.well-known/* prefix.
Node.js vs Cloudflare Workers
The template produces two runtimes from the same codebase. Here's what you need to know:
Node.js (Hono + @hono/node-server)
Entry:
src/index.tsTransport: SDK's
StreamableHTTPServerTransportSessions:
MemorySessionStore(default) orSqliteSessionStorefor persistenceFull MCP features including bidirectional requests (sampling, elicitation, roots)
Local development:
bun dev
Cloudflare Workers
Entry:
src/worker.tsTransport: Custom JSON-RPC dispatcher (
shared/mcp/dispatcher.ts)Sessions:
KvSessionStorewith memory fallback (persists across requests)Request→response only; no server-initiated messages
Deploy:
wrangler deploy
Shared code lives in src/shared/ (tools, storage interfaces, OAuth flow, utilities). Runtime-specific adapters live in src/adapters/http-hono/ and src/adapters/http-workers/.
When to use which:
Node.js: Local development, full MCP features, self-hosted servers
Workers: Production deployment, global edge, simple tool wrappers
Authorization
Naming conventions (important!)
Use generic , not service-specific names. This keeps the template portable and configuration consistent across all MCP servers.
✅ Correct | ❌ Wrong |
|
|
|
|
|
|
|
|
Why?
Same env var names work across all servers (Spotify, Linear, Gmail, etc.)
Deployment scripts don't need service-specific logic
.env.exampleandwrangler.tomlremain generic templatesEasier to audit security (one pattern to check)
Example
Exception: If a server integrates multiple providers simultaneously (rare), prefix with provider name: GITHUB_CLIENT_ID, GITLAB_CLIENT_ID. Single-provider servers should always use PROVIDER_*.
Auth strategies
Five auth strategies, configured via AUTH_STRATEGY env var:
Strategy | Header | Use Case |
|
| Full OAuth 2.1 PKCE flow with RS token → provider token mapping |
|
| Static token from |
|
| Static key from |
| Multiple headers | Custom headers from |
| — | No authentication |
OAuth flow (strategy=oauth):
Client discovers AS metadata via
/.well-known/oauth-authorization-serverClient initiates PKCE flow →
/authorize→ provider loginProvider callback → server issues RS tokens (access + refresh)
Client sends RS token → server maps to provider token → tool executes with provider API
Token storage (RS token → provider token mapping):
FileTokenStore— Node.js, file-based with optional encryptionMemoryTokenStore— Both runtimes, in-memory with TTLKvTokenStore— Workers, Cloudflare KV with optional encryptionAll support AES-256-GCM encryption via
RS_TOKENS_ENC_KEY
Sessions
Sessions enable multi-tenant operation. One server instance can serve multiple users with isolated state. Both runtimes use SessionStore for this.
What sessions give you:
API key → session binding (who owns this connection)
Session limits per API key (default: 5, LRU eviction)
Session validation on every request (404 for invalid/expired sessions)
Protocol version tracking per session
Server→client request routing (sampling/elicitation need to know which client)
What sessions don't give you (that's on the agent):
Conversation memory ("reply to that email")
Workflow state (draft continuation, last issue ID)
Context carryover between tool calls
Storage implementations:
Store | Runtime | Backend | Persistence |
| Both | In-memory Map | Process lifetime |
| Node.js | SQLite via Drizzle | Disk |
| Workers | Cloudflare KV | Global |
Session lifecycle (per MCP spec):
Client sends
initializerequest withoutMcp-Session-IdheaderServer creates session via
SessionStore.create(sessionId, apiKey), returns session ID in response headerClient sends
initializednotification withMcp-Session-Id→ server marks session as initializedAll subsequent requests must include
Mcp-Session-Id(400 Bad Request if missing)Server validates session exists on every request (404 Not Found if invalid/expired)
Session expires after TTL (default: 24h) or client sends DELETE request
API key resolution (for session binding):
X-Api-KeyorX-Auth-Tokenheader (direct API key auth)Bearer token from
Authorizationheader (OAuth RS token)Static
API_KEYfrom config (fallback)"public"(unauthenticated)
Multi-tenant model:
Adding tools
Location: src/shared/tools/
Pattern: schema → metadata → handler → register
Annotations control how clients display/invoke: readOnlyHint, destructiveHint, idempotentHint, openWorldHint.
Services: For complex integrations, put business logic in src/shared/services/. Extract when: handler exceeds ~30 lines, multiple tools share logic, or external API needs rate limiting/retries. Simple tools can keep logic inline.
Known limitations
Node.js runtime — Full MCP support including server→client requests (sampling, elicitation, roots) via SDK's StreamableHTTPServerTransport. Sessions persist via MemorySessionStore (default) or SqliteSessionStore for disk persistence.
Cloudflare Workers runtime — Request→response mode only. Sessions persist via KvSessionStore across requests, but transport state is stateless (no SSE streams). Server→client requests (sampling, elicitation, roots) aren't available because they require an active SSE stream which Workers can't maintain. Use Workers for simple tool servers; for full MCP features, use Node.js or implement Durable Objects.
License
MIT