Transportation
Follow an MCP request through JSON-RPC, Elysia's lifecycle, and response marshaling.
The adapter exposes MCP over HTTP at POST /mcp by default. It accepts JSON-RPC requests, resolves a registered primitive, and keeps route-backed calls inside Elysia by constructing an internal Request and passing it to app.handle().
Supported JSON-RPC methods
initializepingtools/listandtools/callresources/list,resources/templates/list, andresources/readprompts/listandprompts/getcompletion/completesubscriptions/listen
Modern requests use one POST per message and reject batches. Legacy 2025-11-25 requests retain batches and initialization. Accepted notifications receive an empty 202 response. Protocol errors use JSON-RPC error objects.
Route-backed request lifecycle
This architecture preserves parsers, transforms, TypeBox validation, derived context, local hooks, guards, auth plugins, error handlers, and response mapping. The adapter does not invoke a route handler directly.
Request identity and credentials
The JSON-RPC request itself becomes the invocation context. For route-backed calls, configured inbound headers are copied onto the internal request. The secure defaults pass through authorization, cookie, and x-api-key, while tool arguments cannot set headers unless you explicitly allow them.
mcp({
headers: {
passThroughFromMcpRequest: ['authorization'],
allowFromToolInput: ['x-request-id']
}
})The internal route still sees an ordinary Request; existing Elysia authentication and authorization hooks run in their usual lifecycle stages.
OpenTelemetry
Install Elysia's OpenTelemetry plugin and register it before the routes you want to observe:
import { opentelemetry } from '@elysiajs/opentelemetry'
import { mcp } from '@mwillbanks/elysia-mcp-adapter'
import { Elysia } from 'elysia'
const app = new Elysia()
.use(opentelemetry())
.use(mcp())
.get(
'/reports/:id',
({ params }) => ({ id: params.id }),
{
detail: { operationId: 'reports.get' }
}
)No adapter-specific telemetry setup is required. Because route-backed tools call app.handle(), Elysia's OpenTelemetry plugin observes the internal request and groups its lifecycle spans just like an HTTP request. Name important hooks so trace waterfalls are readable, and use Elysia's record, getCurrentSpan, or setAttributes APIs when a route needs custom spans or attributes.
Export and sampling behavior remains owned by your OpenTelemetry configuration and collector.
Modern streaming and legacy transport options
Modern request-scoped SSE carries progress notifications before the final response. subscriptions/listen provides long-lived change streams. Server closure sends notifications/cancelled and a completion result. Client closure cancels the request and aborts the handler or provider signal without a response.
The removed GET stream and session-delete paths remain legacy compatibility options. They are disabled by default:
mcp({
transport: {
enableGetSse: true,
enableDeleteSession: true
}
})See Security before exposing browser origins or forwarding additional headers.