Skip to main content
Glama
dvaJi
by dvaJi
README.md
# elysiajs-mcp

[![npm version](https://img.shields.io/npm/v/elysiajs-mcp.svg)](https://www.npmjs.com/package/elysiajs-mcp)
[![CI](https://github.com/dvaJi/elysiajs-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/dvaJi/elysiajs-mcp/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/dvaJi/elysiajs-mcp#license)

[Model Context Protocol](https://modelcontextprotocol.io) (MCP) server transport and plugin for the [Elysia](https://elysiajs.com) web framework (Bun).

Connect AI clients to your Elysia app over the MCP **Streamable HTTP** transport. Built on the official [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-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/mcp` API).
- **Permissive `Accept` header handling by default** — works out of the box with Gemini CLI, the Java MCP SDK, Open WebUI, and `curl`. Toggle strict mode if you prefer.
- **`MemoryEventStore`** — in-memory event store enabling SSE **resumability**.
- **Auth helpers** — `bearerAuth()` for token validation and `mcpAuthMetadata()` for the `/.well-known` OAuth discovery endpoints.
- Runs on any web-standard runtime (Bun, Node, Deno, Workers).

## Install

```bash
bun add elysiajs-mcp @modelcontextprotocol/sdk elysia
```

## Quick start

```ts
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](https://github.com/modelcontextprotocol/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

```ts
// 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`.

```ts
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

```ts
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):

```ts
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):

```ts
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

```bash
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`](https://github.com/honojs/middleware/tree/main/packages/mcp) by [Aditya Mathur](https://github.com/mathuraditya724).

## License

[MIT](./LICENSE) © [Francisco Pizarro](https://github.com/dvaJi)