alepha@docs:~/docs/guides/server$
cat 8-mcp.md | pretty
4 min read
Last commit:

#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

typescript
 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.

typescript
 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.

typescript
 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.

typescript
 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:

typescript
 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:

typescript
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:

typescript
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:

typescript
 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:

  1. Runtime validation -- params are validated before your handler runs, results are validated before being sent back
  2. 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:

typescript
 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:

typescript
 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:

typescript
 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:

typescript
 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 messages
  • POST /mcp -- JSON-RPC endpoint for client-to-server requests

The SSE path is configurable:

typescript
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):

bash
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:

bash
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