---
url: /docs/mcp.md
description: >-
  Learn how to expose Model Context Protocol (MCP) endpoints with Ts.ED using
  @tsed/platform-mcp, including functional helpers, decorators, and CLI
  references.
---

# Model Context Protocol (MCP)

`@tsed/platform-mcp` brings [Model Context Protocol](https://modelcontextprotocol.io) 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.

::: tip Need a standalone MCP server?
Import [`@tsed/platform-mcp/cli`](#run-an-mcp-server-from-a-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

::: code-group

```bash [npm]
npm install @tsed/platform-mcp
```

```bash [yarn]
yarn add @tsed/platform-mcp
```

```bash [pnpm]
pnpm add @tsed/platform-mcp
```

```bash [bun]
bun add @tsed/platform-mcp
```

:::

```typescript [src/Server.ts]
import {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:

```typescript [src/Server.ts]
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 {}
```

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 starts the shared `MCP_SERVER` after your CLI has initialized the Ts.ED DI
container. It supports two transports:

* `stdio` for local clients such as MCP Inspector, Claude Desktop, or editor agents;
* `streamable-http` to expose a `POST /mcp` endpoint. It listens on `PORT`, defaulting to `3000`.

Import CLI helpers from the CLI entry point and configure the same `mcp` provider arrays used by the HTTP module:

```typescript [src/mcp.ts]
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.

::: code-group

```typescript [Decorators]
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}!`
      }
    };
  }
}
```

```typescript [Function API]
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&#x20;

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.

::: code-group

```typescript [Decorators]
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}!`
    });
  }
}
```

```typescript [Function API]
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.

::: code-group

```typescript [Decorators]
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"
        }
      ]
    };
  }
}
```

```typescript [Function API]
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.

::: code-group

```typescript [Decorators]
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
          }
        }
      ]
    };
  }
}
```

```typescript [Function API]
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.name` and `error.status` are present: `E_MCP_<KIND>_<CONSTANT_CASE(error.name)>` (via `change-case`)
* otherwise: `E_MCP_<KIND>_ERROR`

### Tool errors

* log event: `MCP_TOOL_ERROR`
* fallback response:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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:

```typescript
@Configuration({
  mcp: {
    path: "/ai/mcp",
    enabled: process.env.MCP_DISABLED !== "true"
  }
})
```

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.

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

```bash
npx @modelcontextprotocol/inspector
```

Then configure the inspector to use the `Streamable HTTP` transport with your server URL, for example:

```text
http://localhost:8083/mcp
```

::: tip
If you also need a standalone CLI distribution that speaks MCP over `stdio` or standalone HTTP, use
[`@tsed/cli-mcp`](https://cli.tsed.dev/guide/cli/mcp.html). 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.
:::
