A small, simple, web-first framework for building MCP servers.
Tip
The Model Context Protocol (MCP) is an open standard that enables secure connections between host applications and external data sources and tools, allowing AI assistants to reason over information and execute functions with user permission.
- Lightweight and zero dependencies
- Supports Streamable HTTP transport
- Composable middleware system
- Standard Schema validation (Zod, Valibot, etc.)
- Type-safe tool, resource, and prompt registration
npm install mcp-lite
# or
bun add mcp-lite
# or
pnpm add mcp-lite
import { Hono } from "hono";
import { McpServer, StreamableHttpTransport } from "mcp-lite";
import { z } from "zod";
// Create MCP server with Zod schema adapter
const mcp = new McpServer({
name: "example-server",
version: "1.0.0",
schemaAdapter: (schema) => z.toJSONSchema(schema as z.ZodType),
});
// Define schema
const EchoSchema = z.object({
message: z.string(),
});
// Add a tool
mcp.tool("echo", {
description: "Echoes the input message",
inputSchema: EchoSchema,
handler: (args) => ({
// args is automatically typed as { message: string }
content: [{ type: "text", text: args.message }],
}),
});
// Create HTTP transport
const transport = new StreamableHttpTransport();
const httpHandler = transport.bind(mcp);
// Integrate with HTTP framework
const app = new Hono();
app.all("/mcp", async (c) => {
const response = await httpHandler(c.req.raw);
return response;
});
Basic constructor usage:
import { McpServer } from "mcp-lite";
const server = new McpServer({
name: "my-server",
version: "1.0.0",
});
Schema adapters are needed when using Standard Schema validators (like Zod or Valibot) to convert them to JSON Schema format that MCP clients can understand.
import { z } from "zod";
const server = new McpServer({
name: "my-server",
version: "1.0.0",
schemaAdapter: (schema) => z.toJSONSchema(schema as z.ZodType),
});
const server = new McpServer({
name: "my-server",
version: "1.0.0",
});
import { Hono } from "hono";
import { StreamableHttpTransport } from "mcp-lite";
// Create transport and bind server
const transport = new StreamableHttpTransport();
const httpHandler = transport.bind(mcp);
// Setup Hono app with MCP endpoint
const app = new Hono();
app.all("/mcp", async (c) => {
const response = await httpHandler(c.req.raw);
return response;
});
Streamable HTTP transport supports two operational modes:
No session support, no GET endpoint for SSE streaming.
import { StreamableHttpTransport } from "mcp-lite";
// Stateless mode - no session management
const transport = new StreamableHttpTransport();
const httpHandler = transport.bind(mcp);
Enable sessions and SSE streaming by providing a SessionAdapter
:
import { StreamableHttpTransport, InMemorySessionAdapter } from "mcp-lite";
// Stateful mode with sessions and SSE support
const transport = new StreamableHttpTransport({
sessionAdapter: new InMemorySessionAdapter({
maxEventBufferSize: 1024
})
});
const httpHandler = transport.bind(mcp);
Implement the SessionAdapter
interface for custom session storage:
import type { SessionAdapter, SessionMeta, SessionData, EventId } from "mcp-lite";
class CustomSessionAdapter implements SessionAdapter {
generateSessionId(): string {
return crypto.randomUUID();
}
// Implement session storage methods...
async create(id: string, meta: SessionMeta): Promise<SessionData> { /* ... */ }
async has(id: string): Promise<boolean> { /* ... */ }
async get(id: string): Promise<SessionData | undefined> { /* ... */ }
async appendEvent(id: string, streamId: string, message: unknown): Promise<EventId | undefined> { /* ... */ }
async replay(id: string, lastEventId: EventId, write: (eventId: EventId, message: unknown) => Promise<void> | void): Promise<void> { /* ... */ }
async delete(id: string): Promise<void> { /* ... */ }
}
mcp.tool("add", {
description: "Adds two numbers",
inputSchema: {
type: "object",
properties: {
a: { type: "number" },
b: { type: "number" },
},
required: ["a", "b"],
},
handler: (args: { a: number; b: number }) => ({
content: [{ type: "text", text: String(args.a + args.b) }],
}),
});
import { z } from "zod";
const AddSchema = z.object({
a: z.number(),
b: z.number(),
});
mcp.tool("add", {
description: "Adds two numbers",
inputSchema: AddSchema,
handler: (args: z.infer<typeof AddSchema>) => ({
content: [{ type: "text", text: String(args.a + args.b) }],
}),
});
mcp.tool("status", {
description: "Returns server status",
handler: () => ({
content: [{ type: "text", text: "Server is running" }],
}),
});
mcp.resource(
"file://config.json",
{
name: "App Configuration",
description: "Application configuration file",
mimeType: "application/json",
},
async (uri) => ({
contents: [{
uri: uri.href,
type: "text",
text: JSON.stringify({ name: "my-app" }),
mimeType: "application/json",
}],
})
);
mcp.resource(
"github://repos/{owner}/{repo}",
{ description: "GitHub repository" },
async (uri, { owner, repo }) => ({
contents: [{
uri: uri.href,
type: "text",
text: `Repository: ${owner}/${repo}`,
}],
})
);
mcp.prompt("greet", {
description: "Generate a greeting message",
handler: () => ({
messages: [{
role: "user",
content: { type: "text", text: "Hello, how are you?" }
}]
}),
});
import { z } from "zod";
const SummarySchema = z.object({
text: z.string(),
length: z.enum(["short", "medium", "long"]).optional(),
});
mcp.prompt("summarize", {
description: "Create a summary prompt",
arguments: SummarySchema,
handler: (args: z.infer<typeof SummarySchema>) => ({
description: "Summarization prompt",
messages: [{
role: "user",
content: {
type: "text",
text: `Please summarize: ${args.text}`
}
}]
}),
});
Elicitation enables MCP servers to request input from the client on behalf of the user during tool execution. This allows tools to gather additional information, confirm sensitive operations, or present choices to users through the connected AI application.
import { z } from "zod";
const DeleteRecordSchema = z.object({
recordId: z.string(),
tableName: z.string(),
});
mcp.tool("delete_database_record", {
description: "Delete a database record with user confirmation",
inputSchema: DeleteRecordSchema,
handler: async (args, ctx) => {
// Check if client supports elicitation
if (!ctx.client.supports("elicitation")) {
throw new Error("This tool requires a client that supports elicitation");
}
// Request user confirmation through elicitation
const response = await ctx.elicit({
message: `Are you sure you want to delete record "${args.recordId}" from table "${args.tableName}"? This action cannot be undone.`,
schema: z.object({
confirmed: z.boolean(),
}),
});
// Handle different response types
switch (response.type) {
// Accept means the response came back, but we still need to check the value of "confirmed"
case "accept": {
if (response.content.confirmed) {
// User confirmed - proceed with deletion
await deleteFromDatabase(args.tableName, args.recordId);
return {
content: [{
type: "text",
text: `Record "${args.recordId}" has been deleted from "${args.tableName}".`
}],
};
}
return {
content: [{
type: "text",
text: "Record deletion declined by user."
}],
};
}
case "decline":
return {
content: [{
type: "text",
text: "Record deletion cancelled by user."
}],
};
case "cancel":
throw new Error("Operation was cancelled");
default:
throw new Error("Unexpected elicitation response");
}
},
});
To use elicitation, configure your transport with a ClientRequestAdapter
alongside your SessionAdapter
:
import {
StreamableHttpTransport,
InMemorySessionAdapter,
InMemoryClientRequestAdapter
} from "mcp-lite";
const transport = new StreamableHttpTransport({
sessionAdapter: new InMemorySessionAdapter({
maxEventBufferSize: 1024
}),
clientRequestAdapter: new InMemoryClientRequestAdapter({
defaultTimeoutMs: 30000 // 30 second timeout for server-to-client requests
})
});
const httpHandler = transport.bind(mcp);
The ClientRequestAdapter
manages pending server-to-client requests (such as elicitation), storing them temporarily while waiting for client responses. This enables the server to pause execution, send a request to the client, and resume once the client provides a response.
For an example using Cloudflare KV as the ClientRequestAdapter
see the Elicitation README.
Basic middleware pattern for logging, authentication, or request processing:
// Logging middleware
mcp.use(async (ctx, next) => {
console.log(`Request: ${ctx.request.method}`);
await next();
});
// Authentication middleware
mcp.use(async (ctx, next) => {
// Access request context
ctx.state.user = "authenticated-user";
await next();
});
import { RpcError, JSON_RPC_ERROR_CODES } from "mcp-lite";
mcp.tool("divide", {
description: "Divides two numbers",
inputSchema: {
type: "object",
properties: {
a: { type: "number" },
b: { type: "number" },
},
required: ["a", "b"],
},
handler: (args: { a: number; b: number }) => {
if (args.b === 0) {
throw new RpcError(JSON_RPC_ERROR_CODES.INVALID_PARAMS, "Division by zero");
}
return {
content: [{ type: "text", text: String(args.a / args.b) }],
};
},
});
This framework supports MCP protocol version 2025-06-18
with full JSON-RPC 2.0 compliance.
See the playground/
directory for complete working examples demonstrating all features.