alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
1,027 lines (1,026 loc) • 31.6 kB
JavaScript
import { $atom, $inject, $module, $state, Alepha, AlephaError, KIND, Primitive, TypeBoxError, createPrimitive, z } from "alepha";
import { $logger } from "alepha/logger";
import { $route } from "alepha/server";
//#region ../../src/mcp/helpers/jsonrpc.ts
const JSONRPC_VERSION = "2.0";
/**
* The latest MCP protocol revision Alepha targets.
* See {@link SUPPORTED_PROTOCOL_VERSIONS} for the full negotiation list.
*/
const MCP_PROTOCOL_VERSION = "2025-11-25";
/**
* Protocol versions Alepha will accept during `initialize` negotiation,
* highest preference first. The server echoes back whichever version the
* client requested if it appears here, otherwise picks the first entry.
*/
const SUPPORTED_PROTOCOL_VERSIONS = [
"2025-11-25",
"2025-06-18",
"2025-03-26",
"2024-11-05"
];
const isSupportedProtocolVersion = (v) => typeof v === "string" && SUPPORTED_PROTOCOL_VERSIONS.includes(v);
const JsonRpcErrorCodes = {
PARSE_ERROR: -32700,
INVALID_REQUEST: -32600,
METHOD_NOT_FOUND: -32601,
INVALID_PARAMS: -32602,
INTERNAL_ERROR: -32603
};
function createResponse(id, result) {
return {
jsonrpc: "2.0",
id,
result
};
}
function createErrorResponse(id, error) {
return {
jsonrpc: "2.0",
id,
error
};
}
function createNotification(method, params) {
return {
jsonrpc: "2.0",
method,
params
};
}
function createParseError(message = "Parse error") {
return {
code: JsonRpcErrorCodes.PARSE_ERROR,
message
};
}
function createInvalidRequestError(message = "Invalid request") {
return {
code: JsonRpcErrorCodes.INVALID_REQUEST,
message
};
}
function createMethodNotFoundError(method) {
return {
code: JsonRpcErrorCodes.METHOD_NOT_FOUND,
message: `Method not found: ${method}`
};
}
function createInvalidParamsError(message) {
return {
code: JsonRpcErrorCodes.INVALID_PARAMS,
message
};
}
function createInternalError(message) {
return {
code: JsonRpcErrorCodes.INTERNAL_ERROR,
message
};
}
function parseMessage(data) {
let parsed;
try {
parsed = JSON.parse(data);
} catch {
throw new JsonRpcParseError("Invalid JSON");
}
if (!isValidJsonRpcRequest(parsed)) throw new JsonRpcParseError("Invalid JSON-RPC request");
return parsed;
}
function isValidJsonRpcRequest(value) {
if (typeof value !== "object" || value === null) return false;
const obj = value;
if (obj.jsonrpc !== "2.0") return false;
if (typeof obj.method !== "string") return false;
if (obj.id !== void 0 && typeof obj.id !== "string" && typeof obj.id !== "number") return false;
if (obj.params !== void 0 && typeof obj.params !== "object") return false;
return true;
}
function isNotification(request) {
return request.id === void 0;
}
var JsonRpcParseError = class extends AlephaError {
name = "JsonRpcParseError";
};
//#endregion
//#region ../../src/mcp/errors/McpError.ts
/**
* MCP-specific error codes (application-specific codes in the -32000 to -32099 range).
*/
const McpErrorCodes = {
UNAUTHORIZED: -32001,
FORBIDDEN: -32003
};
var McpError = class extends Error {
name = "McpError";
code;
constructor(message, code = JsonRpcErrorCodes.INTERNAL_ERROR) {
super(message);
this.code = code;
}
};
var McpMethodNotFoundError = class extends McpError {
name = "McpMethodNotFoundError";
constructor(method) {
super(`Method not found: ${method}`, JsonRpcErrorCodes.METHOD_NOT_FOUND);
}
};
var McpToolNotFoundError = class extends McpError {
name = "McpToolNotFoundError";
tool;
constructor(tool) {
super(`Tool not found: ${tool}`, JsonRpcErrorCodes.METHOD_NOT_FOUND);
this.tool = tool;
}
};
var McpResourceNotFoundError = class extends McpError {
name = "McpResourceNotFoundError";
uri;
constructor(uri) {
super(`Resource not found: ${uri}`, JsonRpcErrorCodes.METHOD_NOT_FOUND);
this.uri = uri;
}
};
var McpPromptNotFoundError = class extends McpError {
name = "McpPromptNotFoundError";
prompt;
constructor(prompt) {
super(`Prompt not found: ${prompt}`, JsonRpcErrorCodes.METHOD_NOT_FOUND);
this.prompt = prompt;
}
};
var McpInvalidParamsError = class extends McpError {
name = "McpInvalidParamsError";
constructor(message) {
super(message, JsonRpcErrorCodes.INVALID_PARAMS);
}
};
var McpUnauthorizedError = class extends McpError {
name = "McpUnauthorizedError";
constructor(message = "Unauthorized") {
super(message, McpErrorCodes.UNAUTHORIZED);
}
};
var McpForbiddenError = class extends McpError {
name = "McpForbiddenError";
constructor(message = "Forbidden") {
super(message, McpErrorCodes.FORBIDDEN);
}
};
//#endregion
//#region ../../src/mcp/providers/McpServerProvider.ts
/**
* Core MCP server provider that handles protocol messages.
*
* This provider maintains registries of tools, resources, and prompts,
* and routes incoming JSON-RPC requests to the appropriate handlers.
*
* It is transport-agnostic - actual communication is handled by
* transport providers like StdioMcpTransport or SseMcpTransport.
*/
var McpServerProvider = class {
log = $logger();
alepha = $inject(Alepha);
tools = /* @__PURE__ */ new Map();
resources = /* @__PURE__ */ new Map();
prompts = /* @__PURE__ */ new Map();
initialized = false;
/**
* Protocol version negotiated with the client during `initialize`.
* Used by transports to validate the `MCP-Protocol-Version` header on
* subsequent HTTP requests (per spec 2025-06-18+).
*/
negotiatedVersion = MCP_PROTOCOL_VERSION;
/**
* Server identity returned during `initialize`. Consumers may override
* fields directly (e.g. `mcpServer.serverInfo = { name: "lore-mcp",
* version: "0.20.3", description: "..." }`) — the `description` field
* is supported per spec 2025-11-25 (minor change #2).
*/
serverInfo = {
name: "alepha-mcp",
version: "1.0.0"
};
/**
* Register a tool with the MCP server.
*/
registerTool(tool) {
this.log.trace(`Registering MCP tool: ${tool.name}`);
this.tools.set(tool.name, tool);
}
/**
* Register a resource with the MCP server.
*/
registerResource(resource) {
this.log.trace(`Registering MCP resource: ${resource.uri}`);
this.resources.set(resource.uri, resource);
}
/**
* Register a prompt with the MCP server.
*/
registerPrompt(prompt) {
this.log.trace(`Registering MCP prompt: ${prompt.name}`);
this.prompts.set(prompt.name, prompt);
}
/**
* Get the server capabilities based on registered primitives.
*/
getCapabilities() {
return {
tools: this.tools.size > 0 ? {} : void 0,
resources: this.resources.size > 0 ? {} : void 0,
prompts: this.prompts.size > 0 ? {} : void 0
};
}
/**
* Get all registered tools.
*/
getTools() {
return Array.from(this.tools.values());
}
/**
* Get all registered resources.
*/
getResources() {
return Array.from(this.resources.values());
}
/**
* Get all registered prompts.
*/
getPrompts() {
return Array.from(this.prompts.values());
}
/**
* Get a tool by name.
*/
getTool(name) {
return this.tools.get(name);
}
/**
* Get a resource by URI.
*/
getResource(uri) {
return this.resources.get(uri);
}
/**
* Get a prompt by name.
*/
getPrompt(name) {
return this.prompts.get(name);
}
/**
* Handle an incoming JSON-RPC request.
*
* @param request - The parsed JSON-RPC request
* @param context - Optional context from the transport layer (headers, auth, etc.)
* @returns The JSON-RPC response, or null for notifications
*/
async handleMessage(request, context) {
const id = request.id;
if (id === void 0) {
await this.handleNotification(request);
return null;
}
try {
return createResponse(id, await this.handleRequest(request, context));
} catch (error) {
this.log.error("MCP request failed", error);
if (error instanceof McpError) return createErrorResponse(id, {
code: error.code,
message: error.message
});
return createErrorResponse(id, createInternalError(error.message));
}
}
/**
* Handle a JSON-RPC request that expects a response.
*/
async handleRequest(request, context) {
const { method, params = {} } = request;
switch (method) {
case "initialize": return this.handleInitialize(params);
case "ping": return this.handlePing();
case "tools/list": return this.handleToolsList();
case "tools/call": return this.handleToolsCall(params, context);
case "resources/list": return this.handleResourcesList();
case "resources/read": return this.handleResourcesRead(params, context);
case "prompts/list": return this.handlePromptsList();
case "prompts/get": return this.handlePromptsGet(params, context);
default: throw new McpMethodNotFoundError(method);
}
}
/**
* Handle a notification (no response expected).
*/
async handleNotification(request) {
const { method } = request;
switch (method) {
case "notifications/initialized":
this.log.debug("MCP client initialized");
break;
case "notifications/cancelled":
this.log.debug("MCP request cancelled", request.params);
break;
default: this.log.debug(`Unknown MCP notification: ${method}`);
}
}
handleInitialize(params) {
const requested = params.protocolVersion;
const negotiated = isSupportedProtocolVersion(requested) ? requested : SUPPORTED_PROTOCOL_VERSIONS[0];
this.log.info("MCP client initializing", {
clientInfo: params.clientInfo,
requestedProtocolVersion: requested,
negotiatedProtocolVersion: negotiated
});
this.initialized = true;
this.negotiatedVersion = negotiated;
return {
protocolVersion: negotiated,
capabilities: this.getCapabilities(),
serverInfo: this.serverInfo
};
}
handlePing() {
return {};
}
handleToolsList() {
return { tools: Array.from(this.tools.values()).map((t) => t.toDescriptor()) };
}
async handleToolsCall(params, context) {
const name = params.name;
const args = params.arguments ?? {};
const tool = this.tools.get(name);
if (!tool) throw new McpToolNotFoundError(name);
try {
const result = await tool.execute(args, context);
if (!tool.hasOutputSchema()) {
const raw = this.asRawToolContent(result);
if (raw) return raw;
}
const callResult = { content: [{
type: "text",
text: typeof result === "string" ? result : JSON.stringify(result ?? null)
}] };
if (tool.hasOutputSchema() && result !== void 0) callResult.structuredContent = result;
return callResult;
} catch (error) {
if (error instanceof TypeBoxError) {
const path = error.value?.path || "/";
const message = error.value?.message || error.message;
return {
content: [{
type: "text",
text: `Validation error at ${path}: ${message}`
}],
structuredContent: { errors: [{
path,
message
}] },
isError: true
};
}
return {
content: [{
type: "text",
text: `Error: ${error.message}`
}],
isError: true
};
}
}
/**
* Recognize a tool handler's return value as a pre-built MCP tool result —
* i.e. it already carries a `content` array of content blocks (text, image,
* audio, resource, resource_link). Returns the normalized
* {@link McpToolCallResult} when matched, or `undefined` to fall back to the
* default JSON/text encoding. Only ever consulted for tools that did NOT
* declare an output schema (see {@link handleToolCall}).
*/
asRawToolContent(result) {
if (!result || typeof result !== "object") return;
const candidate = result;
if (!Array.isArray(candidate.content) || candidate.content.length === 0) return;
if (!candidate.content.every((block) => !!block && typeof block === "object" && typeof block.type === "string")) return;
return {
content: candidate.content,
isError: typeof candidate.isError === "boolean" ? candidate.isError : void 0,
_meta: candidate._meta && typeof candidate._meta === "object" ? candidate._meta : void 0
};
}
handleResourcesList() {
return { resources: Array.from(this.resources.values()).map((r) => r.toDescriptor()) };
}
async handleResourcesRead(params, context) {
const uri = params.uri;
const resource = this.resources.get(uri);
if (!resource) throw new McpResourceNotFoundError(uri);
const content = await resource.read(context);
const resourceContent = {
uri,
mimeType: resource.mimeType
};
if (content.text !== void 0) resourceContent.text = content.text;
if (content.blob !== void 0) resourceContent.blob = Buffer.from(content.blob).toString("base64");
return { contents: [resourceContent] };
}
handlePromptsList() {
return { prompts: Array.from(this.prompts.values()).map((p) => p.toDescriptor()) };
}
async handlePromptsGet(params, context) {
const name = params.name;
const args = params.arguments ?? {};
const prompt = this.prompts.get(name);
if (!prompt) throw new McpPromptNotFoundError(name);
const mcpMessages = (await prompt.get(args, context)).map((msg) => ({
role: msg.role,
content: {
type: "text",
text: msg.content
}
}));
return {
description: prompt.description,
messages: mcpMessages
};
}
};
//#endregion
//#region ../../src/mcp/primitives/$prompt.ts
/**
* Creates an MCP prompt primitive for defining reusable prompt templates.
*
* Prompts allow you to define templated messages that can be filled in
* with arguments at runtime. They're useful for creating consistent
* interaction patterns.
*
* @example
* ```ts
* class Prompts {
* greeting = $prompt({
* description: "Generate a personalized greeting",
* args: z.object({
* name: z.text({ description: "Name of the person to greet" }),
* style: z.enum(["formal", "casual"]).optional(),
* }),
* handler: async ({ args }) => [
* {
* role: "user",
* content: args.style === "formal"
* ? `Please greet ${args.name} in a formal manner.`
* : `Say hi to ${args.name}!`,
* },
* ],
* });
*
* codeReview = $prompt({
* description: "Request a code review",
* args: z.object({
* code: z.text({ description: "The code to review" }),
* language: z.text({ description: "Programming language" }),
* }),
* handler: async ({ args }) => [
* {
* role: "user",
* content: `Please review this ${args.language} code:\n\n${args.code}`,
* },
* ],
* });
* }
* ```
*/
const $prompt = (options) => {
return createPrimitive(PromptPrimitive, options);
};
var PromptPrimitive = class extends Primitive {
mcpServer = $inject(McpServerProvider);
/**
* Returns the name of the prompt.
*/
get name() {
return this.options.name ?? this.config.propertyKey;
}
/**
* Returns the description of the prompt.
*/
get description() {
return this.options.description;
}
onInit() {
this.mcpServer.registerPrompt(this);
}
/**
* Get the prompt messages with the given arguments.
*
* @param rawArgs - Raw arguments to validate and pass to the handler
* @param context - Optional context from the transport layer
* @returns Array of prompt messages
*/
async get(rawArgs, context) {
let args = rawArgs ?? {};
if (this.options.args) args = this.alepha.codec.decode(this.options.args, rawArgs ?? {});
return this.options.handler({
args,
context
});
}
/**
* Convert the prompt to an MCP prompt descriptor for protocol messages.
*/
toDescriptor() {
const descriptor = {
name: this.name,
description: this.description,
arguments: this.options.args ? this.schemaToArguments(this.options.args) : []
};
if (this.options.title) descriptor.title = this.options.title;
if (this.options.icons && this.options.icons.length > 0) descriptor.icons = this.options.icons;
return descriptor;
}
/**
* Convert a TypeBox schema to an array of prompt arguments.
*/
schemaToArguments(schema) {
const args = [];
for (const [name, propSchema] of Object.entries(schema.properties)) {
const prop = z.schema.unwrap(propSchema);
args.push({
name,
description: prop.description,
required: !z.schema.isOptional(propSchema)
});
}
return args;
}
};
$prompt[KIND] = PromptPrimitive;
//#endregion
//#region ../../src/mcp/primitives/$resource.ts
/**
* Creates an MCP resource primitive for exposing read-only data.
*
* Resources represent any kind of data that an LLM might want to read,
* such as files, database records, API responses, or computed data.
*
* **Key Features**
* - URI-based identification for resources
* - Support for text and binary content
* - MIME type specification
* - Lazy loading via handler function
*
* @example
* ```ts
* class ProjectResources {
* readme = $resource({
* uri: "file:///readme",
* description: "Project README file",
* mimeType: "text/markdown",
* handler: async () => ({
* text: await fs.readFile("README.md", "utf-8"),
* }),
* });
*
* config = $resource({
* uri: "config://app",
* name: "Application Configuration",
* mimeType: "application/json",
* handler: async () => ({
* text: JSON.stringify(this.configService.getConfig()),
* }),
* });
* }
* ```
*/
const $resource = (options) => {
return createPrimitive(ResourcePrimitive, options);
};
var ResourcePrimitive = class extends Primitive {
mcpServer = $inject(McpServerProvider);
/**
* Returns the name of the resource.
*/
get name() {
return this.options.name ?? this.config.propertyKey;
}
/**
* Returns the URI of the resource.
*/
get uri() {
return this.options.uri;
}
/**
* Returns the description of the resource.
*/
get description() {
return this.options.description;
}
/**
* Returns the MIME type of the resource.
*/
get mimeType() {
return this.options.mimeType ?? "text/plain";
}
onInit() {
this.mcpServer.registerResource(this);
}
/**
* Read the resource content.
*
* @param context - Optional context from the transport layer
* @returns The resource content
*/
async read(context) {
return this.options.handler({ context });
}
/**
* Convert the resource to an MCP resource descriptor for protocol messages.
*/
toDescriptor() {
const descriptor = {
uri: this.uri,
name: this.name,
description: this.description,
mimeType: this.mimeType
};
if (this.options.title) descriptor.title = this.options.title;
if (this.options.icons && this.options.icons.length > 0) descriptor.icons = this.options.icons;
return descriptor;
}
};
$resource[KIND] = ResourcePrimitive;
//#endregion
//#region ../../src/mcp/primitives/$tool.ts
/**
* Creates an MCP tool primitive for defining callable functions.
*
* Tools are the primary way for LLMs to interact with external systems through MCP.
* Each tool has a name, description, typed parameters, and a handler function.
*
* **Key Features**
* - Full TypeScript inference for parameters and results
* - Automatic schema validation using TypeBox
* - JSON Schema generation for MCP protocol
* - Integration with MCP server provider
*
* @example
* ```ts
* class CalculatorTools {
* add = $tool({
* description: "Add two numbers together",
* schema: {
* params: z.object({
* a: z.number(),
* b: z.number(),
* }),
* result: z.number(),
* },
* handler: async ({ params }) => {
* return params.a + params.b;
* },
* });
*
* greet = $tool({
* description: "Generate a greeting message",
* schema: {
* params: z.object({
* name: z.text(),
* }),
* result: z.text(),
* },
* handler: async ({ params }) => {
* return `Hello, ${params.name}!`;
* },
* });
* }
* ```
*/
const $tool = (options) => {
return createPrimitive(ToolPrimitive, options);
};
var ToolPrimitive = class extends Primitive {
mcpServer = $inject(McpServerProvider);
/**
* Returns the name of the tool.
*/
get name() {
return this.options.name ?? this.config.propertyKey;
}
/**
* Returns the description of the tool.
*/
get description() {
return this.options.description;
}
/**
* Whether the tool declared a result schema. When true, `tools/call`
* responses include `structuredContent` populated with the validated
* result (spec 2025-06-18).
*/
hasOutputSchema() {
return !!this.options.schema?.result;
}
onInit() {
this.mcpServer.registerTool(this);
}
/**
* Execute the tool with the given parameters.
*
* @param params - Raw parameters to validate and pass to the handler
* @param context - Optional context from the transport layer
* @returns The tool result
*/
async execute(params, context) {
let validatedParams = params ?? {};
if (this.options.schema?.params) validatedParams = this.alepha.codec.decode(this.options.schema.params, validatedParams);
const result = await this.options.handler({
params: validatedParams,
context
});
if (this.options.schema?.result && result !== void 0) return this.alepha.codec.encode(this.options.schema.result, result);
return result;
}
/**
* Convert the tool to an MCP tool descriptor for protocol messages.
*
* Emits the spec 2025-11-25 surface: `title`, `annotations`, `icons`,
* and (when `schema.result` is defined) `outputSchema` so the server
* can populate `structuredContent` on call results.
*/
toDescriptor() {
const inputSchema = this.options.schema?.params ? this.schemaToJsonSchema(this.options.schema.params) : {
type: "object",
properties: {},
required: []
};
const descriptor = {
name: this.name,
description: this.description,
inputSchema
};
if (this.options.title) descriptor.title = this.options.title;
if (this.options.annotations) descriptor.annotations = this.options.annotations;
if (this.options.icons && this.options.icons.length > 0) descriptor.icons = this.options.icons;
if (this.options.schema?.result) {
const out = this.propertyToJsonSchema(this.options.schema.result);
descriptor.outputSchema = typeof out === "object" && out !== null && "type" in out ? out : {
type: "object",
properties: {},
required: []
};
}
return descriptor;
}
/**
* Convert a TypeBox schema to JSON Schema format.
*
* Emits the 2020-12 dialect annotation at the root (spec 2025-11-25 /
* SEP-1613 — JSON Schema 2020-12 is the default dialect for MCP).
* The TypeBox shapes Alepha emits today are already 2020-12-compatible;
* this is just the dialect declaration.
*/
schemaToJsonSchema(schema, options) {
let json;
try {
json = z.toJSONSchema(schema);
} catch {
json = {
type: "object",
properties: {},
required: []
};
}
if (options?.root === false) json.$schema = void 0;
else json.$schema = "https://json-schema.org/draft/2020-12/schema";
return json;
}
/**
* Convert a single property schema to JSON Schema format (zod-native).
*/
propertyToJsonSchema(schema) {
try {
const json = z.toJSONSchema(schema);
json.$schema = void 0;
return json;
} catch {
return { type: "string" };
}
}
};
$tool[KIND] = ToolPrimitive;
//#endregion
//#region ../../src/mcp/transports/StreamableHttpMcpTransport.ts
const mcpStreamableHttpOptions = $atom({
name: "alepha.mcp.streamableHttp.options",
description: "Configuration options for the MCP Streamable HTTP transport.",
schema: z.object({
/**
* Path for the MCP endpoint. Single endpoint for both requests and
* (optional) server-streamed responses, per spec 2025-03-26+.
*/
path: z.text({ default: "/mcp" }),
/**
* Allow-list of `Origin` header values accepted on incoming requests.
* Empty array (default) means "allow any". When set, browser-originated
* requests with a non-matching `Origin` are rejected with 403 Forbidden,
* blocking DNS-rebinding attacks against localhost MCP servers.
*
* Server-to-server callers (no `Origin` header) are always allowed.
*
* Spec 2025-11-25, PR #1439.
*/
allowedOrigins: z.array(z.text()).default([]),
/**
* When true, an unauthenticated POST to the MCP endpoint is rejected
* with `401 Unauthorized` and an RFC 9728 `WWW-Authenticate` challenge
* instead of being dispatched. MCP clients (Claude, etc.) rely on that
* challenge to discover the OAuth authorization server.
*
* The transport stays OAuth-agnostic: it only knows "auth is required".
* `$realm({ features: { oauth: true } })` flips this on automatically.
*
* @default false
*/
requireAuth: z.boolean().default(false),
/**
* Path of the RFC 9728 protected-resource metadata document, advertised
* (as an absolute URL, resolved against the request origin) in the
* `WWW-Authenticate` challenge emitted when {@link requireAuth} rejects
* a request.
*/
resourceMetadataPath: z.text({ default: "/.well-known/oauth-protected-resource" })
}),
default: {
path: "/mcp",
allowedOrigins: [],
requireAuth: false,
resourceMetadataPath: "/.well-known/oauth-protected-resource"
}
});
const mcpSseOptions = mcpStreamableHttpOptions;
/**
* Streamable HTTP transport for MCP communication.
*
* Implements the 2025-03-26+ Streamable HTTP transport: a single `/mcp`
* endpoint that accepts JSON-RPC over POST and returns either
* `application/json` (single response, the default) or
* `text/event-stream` (when the server wants to stream multiple messages).
*
* Designed for serverless deployment (Cloudflare Workers, etc.) — there is
* no long-lived GET stream. GET on the endpoint returns 405 Method Not
* Allowed; clients that want server-initiated push must rely on the POST
* response stream when the server upgrades to SSE for that particular call.
*
* Spec compliance:
* - 2025-06-18: validates `MCP-Protocol-Version` header on every request
* after `initialize` against the version negotiated and stored on
* `McpServerProvider`.
* - 2025-11-25: rejects requests with a non-allow-listed `Origin` header
* (PR #1439). See {@link mcpStreamableHttpOptions.allowedOrigins}.
*
* @example
* ```ts
* import { Alepha, run } from "alepha";
* import { AlephaServer } from "alepha/server";
* import { AlephaMcp, StreamableHttpMcpTransport } from "alepha/mcp";
*
* class MyTools {
* // ... tool definitions
* }
*
* run(
* Alepha.create()
* .with(AlephaServer)
* .with(AlephaMcp)
* .with(StreamableHttpMcpTransport)
* .with(MyTools)
* );
* ```
*/
var StreamableHttpMcpTransport = class {
log = $logger();
options = $state(mcpStreamableHttpOptions);
mcpServer = $inject(McpServerProvider);
/**
* GET on the MCP endpoint is not supported in this transport. Returning
* 405 (rather than serving the legacy two-endpoint SSE pattern) is the
* spec-allowed response for servers that don't offer server-initiated
* push outside of an active POST.
*/
notAllowed = $route({
method: "GET",
path: this.options.path,
handler: (request) => {
request.reply.status = 405;
request.reply.headers.allow = "POST";
request.reply.headers["content-type"] = "application/json";
request.reply.body = JSON.stringify({ error: "Method Not Allowed. Use POST for MCP messages." });
}
});
/**
* POST endpoint for client-to-server JSON-RPC messages.
* Returns `application/json` for single responses; tools that need to
* stream progress would upgrade to `text/event-stream` (deferred until a
* concrete need exists).
*/
message = $route({
method: "POST",
path: this.options.path,
schema: { body: z.json() },
handler: async (request) => {
try {
const originRaw = request.headers.origin;
const origin = Array.isArray(originRaw) ? originRaw[0] : originRaw;
if (origin && this.options.allowedOrigins.length > 0 && !this.options.allowedOrigins.includes(origin)) {
this.log.warn("Rejected MCP request with non-allowed Origin", {
origin,
allowed: this.options.allowedOrigins
});
request.reply.status = 403;
request.reply.headers["content-type"] = "application/json";
request.reply.body = JSON.stringify({ error: "Forbidden: Origin not allowed" });
return;
}
if (this.options.requireAuth && !request.user) {
const url = typeof request.url === "string" ? new URL(request.url) : request.url;
const resourceMetadataUrl = `${url.protocol}//${url.host}${this.options.resourceMetadataPath}`;
this.log.debug("Rejecting unauthenticated MCP request", { resourceMetadataUrl });
request.reply.status = 401;
request.reply.headers["www-authenticate"] = `Bearer resource_metadata="${resourceMetadataUrl}"`;
request.reply.headers["content-type"] = "application/json";
request.reply.body = JSON.stringify({ error: "Unauthorized" });
return;
}
const body = typeof request.body === "string" ? request.body : JSON.stringify(request.body);
this.log.debug("MCP request body", {
body,
bodyType: typeof request.body
});
const rpcRequest = parseMessage(body);
const headers = { ...request.headers };
if (rpcRequest.method !== "initialize") {
const headerRaw = headers["mcp-protocol-version"];
const headerVersion = Array.isArray(headerRaw) ? headerRaw[0] : headerRaw;
if (headerVersion && headerVersion !== this.mcpServer.negotiatedVersion) {
this.log.warn("MCP-Protocol-Version header mismatch", {
header: headerVersion,
negotiated: this.mcpServer.negotiatedVersion
});
request.reply.status = 400;
request.reply.headers["content-type"] = "application/json";
request.reply.body = JSON.stringify({ error: `MCP-Protocol-Version mismatch: expected ${this.mcpServer.negotiatedVersion}, got ${headerVersion}` });
return;
}
}
const context = { headers };
const response = await this.mcpServer.handleMessage(rpcRequest, context);
if (response) {
request.reply.headers["content-type"] = "application/json";
request.reply.body = JSON.stringify(response);
} else request.reply.status = 204;
} catch (error) {
if (error instanceof JsonRpcParseError) {
request.reply.status = 400;
request.reply.headers["content-type"] = "application/json";
request.reply.body = JSON.stringify(createErrorResponse(0, createParseError(error.message)));
} else {
this.log.error("Failed to process MCP message", error);
request.reply.status = 500;
request.reply.body = JSON.stringify({ error: error.message });
}
}
}
});
};
/**
* @deprecated Use {@link StreamableHttpMcpTransport}. The 2024-11-05
* two-endpoint HTTP+SSE pattern was replaced by Streamable HTTP in spec
* 2025-03-26. This alias is preserved for one release to ease migration.
*/
const SseMcpTransport = StreamableHttpMcpTransport;
//#endregion
//#region ../../src/mcp/index.ts
/**
* Model Context Protocol for AI tool integration.
*
* **Features:**
* - MCP resource definitions
* - MCP tool definitions
* - MCP prompt definitions
* - JSON-RPC protocol
* - Streamable HTTP transport (spec 2025-03-26+)
*
* @module alepha.mcp
*/
const AlephaMcp = $module({
name: "alepha.mcp",
primitives: [
$tool,
$resource,
$prompt
],
services: [McpServerProvider],
variants: [StreamableHttpMcpTransport]
});
//#endregion
export { $prompt, $resource, $tool, AlephaMcp, JSONRPC_VERSION, JsonRpcErrorCodes, JsonRpcParseError, MCP_PROTOCOL_VERSION, McpError, McpErrorCodes, McpForbiddenError, McpInvalidParamsError, McpMethodNotFoundError, McpPromptNotFoundError, McpResourceNotFoundError, McpServerProvider, McpToolNotFoundError, McpUnauthorizedError, PromptPrimitive, ResourcePrimitive, SUPPORTED_PROTOCOL_VERSIONS, SseMcpTransport, StreamableHttpMcpTransport, ToolPrimitive, createErrorResponse, createInternalError, createInvalidParamsError, createInvalidRequestError, createMethodNotFoundError, createNotification, createParseError, createResponse, isNotification, isSupportedProtocolVersion, isValidJsonRpcRequest, mcpSseOptions, mcpStreamableHttpOptions, parseMessage };
//# sourceMappingURL=index.js.map