elysiajs-mcp
v0.1.0
Published
Model Context Protocol (MCP) server transport and plugin for Elysia
Maintainers
Readme
elysiajs-mcp
Model Context Protocol (MCP) server transport and plugin for the Elysia web framework (Bun).
Connect AI clients to your Elysia app over the MCP Streamable HTTP transport. Built on the official @modelcontextprotocol/sdk web-standard transport, with an idiomatic Elysia plugin that handles session lifecycle for you.
Features
mcp()plugin — mount a full MCP server in one line, with automatic stateful (per-session) and stateless modes.StreamableHTTPTransport— a low-level transport you can wire up manually (mirrors the@hono/mcpAPI).- Permissive
Acceptheader handling by default — works out of the box with Gemini CLI, the Java MCP SDK, Open WebUI, andcurl. Toggle strict mode if you prefer. MemoryEventStore— in-memory event store enabling SSE resumability.- Auth helpers —
bearerAuth()for token validation andmcpAuthMetadata()for the/.well-knownOAuth discovery endpoints. - Runs on any web-standard runtime (Bun, Node, Deno, Workers).
Install
bun add elysiajs-mcp @modelcontextprotocol/sdk elysiaQuick start
import { Elysia } from "elysia";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { mcp } from "elysiajs-mcp";
new Elysia()
.use(
mcp({
server: () => {
const server = new McpServer({ name: "my-server", version: "1.0.0" });
server.registerTool(
"greet",
{
description: "Greet someone by name",
inputSchema: { name: z.string().default("world") },
},
async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name}!` }],
}),
);
return server;
},
}),
)
.listen(3000);Connect any MCP client (e.g. the MCP Inspector) to http://localhost:3000/mcp.
How it works
mcp() mounts a single .all('/mcp') route. A server() factory is invoked for every new session (stateful mode) or every request (stateless mode), so each session gets isolated tools, prompts, and resources.
| Option | Default | Description |
| ---------------------- | --------------------------- | ------------------------------------------------------------ |
| server | (required) | Factory returning an MCP Server / McpServer. |
| path | '/mcp' | Endpoint path. |
| sessionIdGenerator | () => crypto.randomUUID() | Pass undefined for stateless mode. |
| strictAcceptHeader | false | Strictly enforce the MCP Accept header spec. |
| enableJsonResponse | false | Return JSON instead of SSE for POST requests. |
| eventStore | none | Provide a MemoryEventStore (or your own) for resumability. |
| auth | none | (request) => AuthInfo \| Response \| undefined hook. |
| onsessioninitialized | none | Called when a session is created. |
| onsessionclosed | none | Called when a session is closed via DELETE. |
Stateful vs. stateless
// Stateful (default): one server + transport kept alive per session.
mcp({ server: () => new McpServer({ name: "s", version: "1" }) });
// Stateless: a fresh server + transport per request, torn down after.
mcp({ server: () => new McpServer({ name: "s", version: "1" }), sessionIdGenerator: undefined });Resumability (event store)
Pass an eventStore so clients that disconnect can resume missed messages via Last-Event-ID.
import { mcp, MemoryEventStore } from 'elysiajs-mcp'
mcp({ server: () => …, eventStore: new MemoryEventStore() })MemoryEventStore keeps the last 100 streams × 100 events by default (both configurable).
Auth
Bearer tokens
import { mcp, bearerAuth } from 'elysiajs-mcp'
new Elysia().use(
mcp({
server: () => …,
auth: bearerAuth({
require: true,
verify: async (token) => {
const user = await verifyToken(token)
return user ? { token, clientId: user.id, scopes: user.scopes } : undefined
},
}),
})
)OAuth discovery endpoints
Advertise where clients should authenticate (RFC 8414 / RFC 9728):
import { mcpAuthMetadata } from "elysiajs-mcp";
new Elysia().use(
mcpAuthMetadata({
issuerUrl: "https://auth.example.com",
resourceServerUrl: new URL("http://localhost:3000/mcp"),
}),
);This serves /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource.
Low-level transport
If you need full control (mirrors @hono/mcp's API):
import { Elysia } from "elysia";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPTransport } from "elysiajs-mcp";
const server = new McpServer({ name: "low-level", version: "1.0.0" });
const transport = new StreamableHTTPTransport({ sessionIdGenerator: () => crypto.randomUUID() });
new Elysia().all("/mcp", async ({ request }) => {
if (!server.isConnected()) await server.connect(transport);
return transport.handleRequest(request);
});StreamableHTTPTransport accepts the same options as the SDK's transport, plus strictAcceptHeader.
API
mcp(options): Elysia
Mount an MCP Streamable HTTP server.
StreamableHTTPTransport
Extends WebStandardStreamableHTTPServerTransport. Adds strictAcceptHeader (default false).
MemoryEventStore
In-memory EventStore implementation with bounded ring buffers.
bearerAuth(options) / unauthorizedResponse(request, url?)
Bearer-token auth extractor (401 challenge helper).
mcpAuthMetadata(options) / createOAuthMetadata(options)
Elysia plugin + helper serving OAuth discovery metadata.
Scripts
bun run lint # oxlint
bun run lint:fix # oxlint --fix
bun run format # oxfmt (write)
bun run format:check # oxfmt --check
bun run typecheck # tsc --noEmit
bun run check # lint + format:check + typecheck
bun test # run the test suite
bun run dev # run the example server (example/server.ts)
bun run build # bundle to dist/Credits & license
Inspired by @hono/mcp by Aditya Mathur.
