#MCP Server
The Model Context Protocol (MCP) lets AI assistants call your application's tools, read its resources, and use its prompt templates over a standard JSON-RPC protocol.
Alepha ships with first-class MCP support. You define tools, resources, and prompts with the same primitive pattern you already use for routes and actions. The framework handles protocol negotiation, schema validation, and transport.
#Quick Start
1import { t, run } from "alepha"; 2import { AlephaMcp, $tool, $resource } from "alepha/mcp"; 3import { AlephaServer } from "alepha/server"; 4 5class MyMcp { 6 add = $tool({ 7 description: "Add two numbers", 8 schema: { 9 params: t.object({10 a: t.number(),11 b: t.number(),12 }),13 result: t.number(),14 },15 handler: async ({ params }) => params.a + params.b,16 });17 18 readme = $resource({19 uri: "docs://readme",20 description: "Project README",21 mimeType: "text/markdown",22 handler: async () => ({23 text: "# My App\nWelcome to my application.",24 }),25 });26}27 28run(29 Alepha.create()30 .with(AlephaServer)31 .with(AlephaMcp)32 .with(MyMcp),33);
Your MCP server is now available at GET /mcp (SSE) and POST /mcp (JSON-RPC).
#Three Primitives
MCP defines three types of capabilities. Each maps to an Alepha primitive.
#$tool -- Callable Functions
Tools let an AI assistant perform actions: query a database, create records, call external APIs.
1import { $tool } from "alepha/mcp"; 2 3class TaskTools { 4 protected readonly tasks = $inject(TaskController); 5 6 task_list = $tool({ 7 description: "List tasks. Filter by status or search by title.", 8 schema: { 9 params: t.object({10 status: t.optional(t.enum(["new", "accepted", "completed"])),11 search: t.optional(t.string({ description: "Search by title" })),12 limit: t.optional(t.integer({ minimum: 1, maximum: 100 })),13 }),14 result: t.object({15 tasks: t.array(t.object({16 id: t.integer(),17 title: t.string(),18 status: t.string(),19 })),20 total: t.integer(),21 }),22 },23 handler: async ({ params }) => {24 const result = await this.tasks.list({25 status: params.status,26 search: params.search,27 limit: params.limit ?? 20,28 });29 return { tasks: result.items, total: result.total };30 },31 });32 33 task_create = $tool({34 description: "Create a new task.",35 schema: {36 params: t.object({37 title: t.text(),38 description: t.optional(t.text()),39 priority: t.optional(t.enum(["low", "medium", "high"])),40 }),41 result: t.object({42 id: t.integer(),43 title: t.string(),44 }),45 },46 handler: async ({ params }) => {47 return await this.tasks.create(params);48 },49 });50}
Options:
| Option | Type | Description |
|---|---|---|
description |
string |
Required. Tells the AI what the tool does. |
schema.params |
TObject |
TypeBox schema for input parameters. |
schema.result |
TSchema |
TypeBox schema for the return value. |
handler |
function |
Receives { params, context }. Returns the result. |
name |
string |
Override the tool name. Defaults to the property key. |
Parameters and results are validated automatically. If validation fails, the client receives a JSON-RPC error.
#$resource -- Read-Only Data
Resources expose data that an AI can read but not modify: configuration, documentation, database snapshots.
1import { $resource } from "alepha/mcp"; 2 3class Resources { 4 projectList = $resource({ 5 uri: "app://projects", 6 description: "All projects the user has access to.", 7 mimeType: "application/json", 8 handler: async () => { 9 const projects = await this.projectController.list();10 return {11 text: JSON.stringify(projects, null, 2),12 };13 },14 });15 16 logo = $resource({17 uri: "app://logo",18 mimeType: "image/png",19 handler: async () => ({20 blob: await fs.readFile("logo.png"),21 }),22 });23}
Options:
| Option | Type | Description |
|---|---|---|
uri |
string |
Required. Unique identifier (e.g. app://projects, file:///readme). |
description |
string |
What this resource contains. |
mimeType |
string |
Content type. Defaults to text/plain. |
handler |
function |
Returns { text } for text content or { blob } for binary. |
name |
string |
Display name. Defaults to the property key. |
#$prompt -- Message Templates
Prompts define reusable conversation templates with typed arguments.
1import { $prompt } from "alepha/mcp"; 2 3class Prompts { 4 codeReview = $prompt({ 5 description: "Request a code review", 6 args: t.object({ 7 code: t.text({ description: "The code to review" }), 8 language: t.text({ description: "Programming language" }), 9 }),10 handler: async ({ args }) => [11 {12 role: "user",13 content: `Review this ${args.language} code:\n\n\`\`\`${args.language}\n${args.code}\n\`\`\``,14 },15 ],16 });17}
Options:
| Option | Type | Description |
|---|---|---|
description |
string |
What this prompt does. |
args |
TObject |
TypeBox schema for template arguments. |
handler |
function |
Returns an array of { role, content } messages. |
name |
string |
Override the prompt name. Defaults to the property key. |
#Wiring It Up
Register the AlephaMcp module and your tool/resource/prompt classes:
1import { Alepha, run } from "alepha"; 2import { AlephaServer } from "alepha/server"; 3import { AlephaMcp } from "alepha/mcp"; 4 5run( 6 Alepha.create() 7 .with(AlephaServer) 8 .with(AlephaMcp) 9 .with(TaskTools)10 .with(Resources)11 .with(Prompts),12);
Primitives auto-register with the MCP server when instantiated. No manual wiring needed.
For larger apps, group MCP classes into a module:
1import { $module } from "alepha";2import { SseMcpTransport } from "alepha/mcp";3 4export const MyAppMcp = $module({5 name: "myapp.mcp",6 services: [SseMcpTransport, TaskTools, ProjectTools, Resources],7});
Then register the module alongside your other modules:
1run(2 Alepha.create()3 .with(AlephaServer)4 .with(MyAppApi)5 .with(MyAppMcp),6);
#Using DI in Tools
Tools, resources, and prompts are regular Alepha classes. Use $inject() to access any service:
1class PostTools { 2 protected posts = $repository(postEntity); 3 protected markdown = $inject(MarkdownProvider); 4 5 post_create = $tool({ 6 description: "Create a new blog post.", 7 schema: { 8 params: t.object({ 9 title: t.text(),10 content: t.text({ description: "Markdown content" }),11 tags: t.optional(t.array(t.string())),12 }),13 },14 handler: async ({ params }) => {15 const html = this.markdown.render(params.content);16 return await this.posts.create({17 title: params.title,18 content: params.content,19 contentHtml: html,20 tags: params.tags ?? [],21 });22 },23 });24}
#Schemas
TypeBox schemas on tools serve double duty:
- Runtime validation -- params are validated before your handler runs, results are validated before being sent back
- JSON Schema generation -- the MCP protocol advertises your tool's input schema so AI clients know what to send
Add description to individual fields to help the AI understand what each parameter does:
1schema: { 2 params: t.object({ 3 project: t.optional(t.integer({ description: "Project ID" })), 4 project_name: t.optional(t.string({ description: "Case-insensitive project name" })), 5 limit: t.optional(t.integer({ 6 description: "Max results to return (default: 20)", 7 minimum: 1, 8 maximum: 100, 9 })),10 }),11}
Extract shared schemas to keep tool definitions clean:
1// schemas/common.ts 2export const projectParamsSchema = t.object({ 3 project: t.optional(t.integer({ description: "Project ID" })), 4 project_name: t.optional(t.string({ description: "Project name (case-insensitive)" })), 5}); 6 7// tools/TaskTools.ts 8import { projectParamsSchema } from "../schemas/common.ts"; 9 10task_list = $tool({11 description: "List tasks for a project.",12 schema: {13 params: t.extend(projectParamsSchema, {14 status: t.optional(t.enum(["new", "accepted", "completed"])),15 }),16 },17 handler: async ({ params }) => { /* ... */ },18});
#Context
Every handler receives an optional context with HTTP headers and custom data. Use it for authentication or multi-tenancy:
1task_list = $tool({ 2 description: "List user tasks.", 3 handler: async ({ params, context }) => { 4 const auth = context?.headers?.authorization; 5 if (!auth?.toString().startsWith("Bearer ")) { 6 throw new McpUnauthorizedError("Missing authentication"); 7 } 8 // ... 9 },10});
#Error Handling
Throw errors in handlers and they are returned as tool results the AI can read:
1import { McpUnauthorizedError, McpForbiddenError } from "alepha/mcp"; 2 3handler: async ({ params, context }) => { 4 if (!context?.headers?.authorization) { 5 throw new McpUnauthorizedError("Missing token"); 6 } 7 const project = await this.projects.findById(params.id); 8 if (!project) { 9 throw new NotFoundError(`Project ${params.id} not found`);10 }11 return project;12}
Available error classes:
| Error | Code | When to use |
|---|---|---|
McpUnauthorizedError |
-32001 | Missing or invalid credentials |
McpForbiddenError |
-32003 | Authenticated but not allowed |
McpToolNotFoundError |
-32601 | Unknown tool name |
McpResourceNotFoundError |
-32601 | Unknown resource URI |
McpPromptNotFoundError |
-32601 | Unknown prompt name |
McpInvalidParamsError |
-32602 | Bad parameters |
#Transport
The default transport uses Server-Sent Events (SSE):
GET /mcp-- SSE stream for server-to-client messagesPOST /mcp-- JSON-RPC endpoint for client-to-server requests
The SSE path is configurable:
1import { mcpSseOptions } from "alepha/mcp";2 3alepha.store.set(mcpSseOptions, { path: "/api/mcp" });
#Naming Convention
Use entity_action for tool names (snake_case with underscore separator):
project_list, project_info
task_create, task_update, task_complete
chapter_start, chapter_close, chapter_changelog
This groups related tools together and reads naturally in AI conversations.
#Project Structure
For apps with multiple MCP tools, organize by domain:
src/
mcp/
index.ts # $module definition
schemas/
common.ts # Shared schemas (pagination, project params)
taskSchemas.ts
projectSchemas.ts
tools/
TaskTools.ts
ProjectTools.ts
resources/
ProjectResources.ts