Model Context Protocol (MCP)
@tsed/platform-mcp brings Model Context Protocol support to every Ts.ED HTTP adapter. The module exposes a configurable /mcp endpoint, lets you register tools/resources/prompts through DI-aware helpers or decorators, and reuses the same MCP primitives that power the CLI integration.
Need a standalone MCP server?
Import @tsed/platform-mcp/cli to run the same tools, resources, and prompts over stdio or Streamable HTTP. The CLI entry point does not mount a Ts.ED HTTP application.
Installation
npm install @tsed/platform-mcpyarn add @tsed/platform-mcppnpm add @tsed/platform-mcpbun add @tsed/platform-mcpimport {Configuration} from "@tsed/di";
import "@tsed/platform-express";
import "@tsed/platform-mcp";
@Configuration({
mcp: {
path: "/mcp" // defaults to "/mcp"
}
})
export class Server {}Register your MCP providers in the mcp configuration so the module can expose them through the HTTP endpoint:
import {Configuration} from "@tsed/di";
import "@tsed/platform-express";
import "@tsed/platform-mcp";
import {TestPrompt} from "./prompts/TestPrompt.js";
import {TestResource} from "./resources/TestResource.js";
import {TestTool} from "./tools/TestTool.js";
@Configuration({
mcp: {
prompts: [TestPrompt],
resources: [TestResource],
tools: [TestTool]
}
})
export class Server {}Explicit registration required
@Tool, @Prompt, and @Resource do not expose a class automatically. Add each decorated class to the tools, prompts, or resources array of every MCP server that should expose its handlers. This lets multiple MCP servers select different provider classes.
All registration helpers return DI tokens. Add those tokens to a module providers array, or expose them from a feature module. Both the decorators and the function API execute handlers inside a Ts.ED DIContext, so you can reuse your existing providers and services.
Run an MCP server from a CLI
The @tsed/platform-mcp/cli entry point creates one MCP server after your CLI has initialized the Ts.ED DI container. It supports two transports:
stdiofor local clients such as MCP Inspector, Claude Desktop, or editor agents;streamable-httpto expose aPOST /mcpendpoint. It listens onPORT, defaulting to3000.
The CLI supports one mcp configuration object. Use the HTTP module when you need multiple MCP server endpoints.
Import CLI helpers from the CLI entry point and configure the same mcp provider arrays used by the HTTP module:
import {Configuration} from "@tsed/di";
import {defineTool, mcpServerConnect} from "@tsed/platform-mcp/cli";
const helloTool = defineTool({
name: "hello",
description: "Greets the MCP client",
handler({name}: {name: string}) {
return {content: [{type: "text", text: `Hello, ${name}!`}]};
}
});
@Configuration({
mcp: {
name: "my-cli-mcp",
version: "1.0.0",
tools: [helloTool]
}
})
export class McpConfiguration {}
// Call this after the CLI bootstrap has initialized the Ts.ED injector.
await mcpServerConnect("stdio");
// Or expose POST /mcp on process.env.PORT (default: 3000):
// await mcpServerConnect("streamable-http");When using stdio, reserve standard output for the MCP protocol: do not write application logs or diagnostic output to stdout. The transport helper stops the Ts.ED logger before connecting, but child processes and custom logging must also write to stderr. For Streamable HTTP, protect the endpoint with authentication and run it behind the same network controls as other privileged CLI services.
Register tools
Tools expose executable actions to MCP clients. Use them for operations that take structured input and return content or structured results, such as querying a service, triggering a workflow, or composing data from your application. With decorators, the preferred approach is to declare Ts.ED models and let the framework derive both the input and the output schemas from the method signature and response metadata.
import {Injectable} from "@tsed/di";
import {Tool} from "@tsed/platform-mcp";
import {Description, Property, Returns} from "@tsed/schema";
class HelloInput {
@Property()
name: string;
}
class HelloOutput {
@Property()
message: string;
}
@Injectable()
export class HelloTool {
@Tool("hello")
@Description("Greets callers from any MCP client")
@Returns(200, HelloOutput)
async handle(input: HelloInput) {
return {
content: [
{
type: "text",
text: `Hello, ${input.name}!`
}
],
structuredContent: {
message: `Hello, ${input.name}!`
}
};
}
}import {defineTool} from "@tsed/platform-mcp";
import {s} from "@tsed/schema";
export const helloTool = defineTool({
name: "hello",
title: "Hello",
description: "Greets callers from any MCP client",
inputSchema: s
.object({
name: s.string().required()
})
.required(),
outputSchema: s
.object({
message: s.string().required()
})
.required(),
async handler({name}) {
return {
content: [
{
type: "text",
text: `Hello, ${name}!`
}
],
structuredContent: {
message: `Hello, ${name}!`
}
};
}
});Structured Response serialization v8.36.0+
When a defineTool handler returns a plain object, Ts.ED serializes it with the tool name and the tools group, then creates both the JSON text content and structuredContent required by MCP. This lets handlers return their domain result directly instead of manually duplicating it in an MCP response.
The generated input and output schemas use the same groups and preserve property aliases. Return a complete MCP result only when you need custom content, such as multiple messages or a non-JSON content type; it is passed through unchanged.
import {Injectable} from "@tsed/di";
import {Tool} from "@tsed/platform-mcp";
import {Description, Property, Returns} from "@tsed/schema";
class HelloInput {
@Property()
name: string;
}
class HelloOutput {
@Property()
message: string;
}
@Injectable()
export class HelloTool {
@Tool("hello")
@Description("Greets callers from any MCP client")
@Returns(HelloOutput)
async handle(input: HelloInput) {
return new HelloOutput({
message: `Hello, ${input.name}!`
});
}
}import {defineTool} from "@tsed/platform-mcp";
import {s} from "@tsed/schema";
export const helloTool = defineTool({
name: "hello",
title: "Hello",
description: "Greets callers from any MCP client",
inputSchema: s
.object({
name: s.string().required()
})
.required(),
outputSchema: s
.object({
message: s.string().required()
})
.required(),
async handler({name}) {
return {
message: `Hello, ${name}!`
};
}
});Register resources
Resources expose addressable content that clients can discover and read later by URI. Use them for static or dynamic documents, generated files, configuration snapshots, or any other content that should be fetched as a resource.
import {Injectable} from "@tsed/di";
import {Resource} from "@tsed/platform-mcp";
@Injectable()
export class McpResources {
@Resource("tsed://docs/index", {
name: "docs",
title: "Internal documentation",
description: "Returns the internal MCP documentation"
})
readDocs() {
return {
contents: [
{
uri: "tsed://docs/index",
mimeType: "text/markdown",
text: "Internal doc content"
}
]
};
}
}import {defineResource} from "@tsed/platform-mcp";
export const docsResource = defineResource({
name: "docs",
title: "Internal documentation",
description: "Returns the internal MCP documentation",
uri: "tsed://docs/index",
handler() {
return {
contents: [
{
uri: "tsed://docs/index",
mimeType: "text/markdown",
text: "Internal doc content"
}
]
};
}
});Register prompts
Prompts expose reusable prompt templates that MCP clients can request on demand. Use them to generate consistent conversation starters, assistant instructions, or parameterized user messages from your Ts.ED application.
import {Injectable} from "@tsed/di";
import {Prompt} from "@tsed/platform-mcp";
@Injectable()
export class McpPrompts {
@Prompt({
name: "ask-tsed",
title: "Ask Ts.ED",
description: "Creates a prompt message for Ts.ED questions"
})
askTsed({question}: {question: string}) {
return {
messages: [
{
role: "user",
content: {
type: "text",
text: question
}
}
]
};
}
}import {definePrompt} from "@tsed/platform-mcp";
import {s} from "@tsed/schema";
export const askTsedPrompt = definePrompt({
name: "ask-tsed",
title: "Ask Ts.ED",
description: "Creates a prompt message for Ts.ED questions",
argsSchema: s
.object({
question: s.string().required()
})
.required(),
handler({question}) {
return {
messages: [
{
role: "user",
content: {
type: "text",
text: question
}
}
]
};
}
});Error handling
defineTool, defineResource, and definePrompt wrap handler execution in try/catch. When a handler throws, Ts.ED logs a structured event and returns a fallback MCP payload instead of re-throwing.
Error code resolution follows this rule:
- if
error.nameanderror.statusare present:E_MCP_<KIND>_<CONSTANT_CASE(error.name)>(viachange-case) - otherwise:
E_MCP_<KIND>_ERROR
Tool errors
- log event:
MCP_TOOL_ERROR - fallback response:
{
"content": [],
"structuredContent": {
"status_code": 500,
"code": "E_MCP_TOOL_INTERNAL_SERVER_ERROR",
"message": "Something went wrong",
"request_id": "<tsed-di-context-id>",
"tool": "my-tool"
}
}Resource errors
- log event:
MCP_RESOURCE_ERROR - fallback response:
{
"contents": [],
"_meta": {
"status_code": 404,
"code": "E_MCP_RESOURCE_NOT_FOUND",
"message": "Resource not found",
"request_id": "<tsed-di-context-id>",
"resource": "docs"
}
}Prompt errors
- log event:
MCP_PROMPT_ERROR - fallback response:
{
"description": "Prompt execution failed",
"messages": [],
"_meta": {
"status_code": 400,
"code": "E_MCP_PROMPT_BAD_REQUEST",
"message": "Prompt execution failed",
"request_id": "<tsed-di-context-id>",
"prompt": "ask-tsed"
}
}Customising the endpoint
Set mcp.path or mcp.enabled to control how the transport is exposed:
@Configuration({
mcp: {
path: "/ai/mcp",
enabled: process.env.MCP_DISABLED !== "true"
}
})Expose multiple MCP servers
mcp also accepts an array. Each entry creates an independent MCP server and mounts its own POST endpoint. This is useful when clients or domains need distinct endpoint paths, server metadata, or transport options.
import {Configuration} from "@tsed/di";
import "@tsed/platform-express";
import "@tsed/platform-mcp";
@Configuration({
mcp: [
{
name: "catalog",
path: "/mcp/catalog"
},
{
name: "administration",
path: "/mcp/admin",
transportOptions: {enableJsonResponse: true}
}
]
})
export class Server {}Each configuration is resolved once while routes are initialized. A new McpServer and Streamable HTTP transport are created for every request, so concurrent requests and server shutdowns remain isolated. Set enabled: false on one entry to leave that endpoint unmounted without affecting the others.
Provider registration is declarative: a server only registers the tokens and decorated classes listed in its tools, resources, and prompts arrays. A decorated class contributes all of its handlers to that MCP server; it is not automatically exposed by other MCP configurations.
The CLI exposes a single /mcp endpoint; use one MCP configuration when starting it with mcpServerConnect.
All Ts.ED adapters (Express, Fastify, Koa) forward POST <path> requests to @modelcontextprotocol/sdk's StreamableHTTPServerTransport, so any MCP-capable client (Claude Desktop, etc.) can talk with your server regardless of the underlying framework.
Testing and inspector
With @tsed/platform-mcp, your MCP server is exposed through your Ts.ED HTTP application. The usual integration test strategy is to bootstrap the server with PlatformTest, then exercise POST /mcp with supertest to verify that tools, resources, and prompts are correctly registered and reachable through the transport.
const response = await request.post("/mcp").set({
Accept: "application/json,text/event-stream",
"Content-Type": "application/json"
});If you want to inspect the server manually, start your Ts.ED application and point the MCP Inspector to the HTTP endpoint exposed by @tsed/platform-mcp:
npx @modelcontextprotocol/inspectorThen configure the inspector to use the Streamable HTTP transport with your server URL, for example:
http://localhost:8083/mcpTIP
If you also need a standalone CLI distribution that speaks MCP over stdio or standalone HTTP, use @tsed/cli-mcp. The decorators and function helpers are designed to stay close across both packages, so moving handlers between CLI and platform integrations does not require rewriting the MCP logic.