Skip to content

Build MCP servers

@tsed/cli-mcp lets you expose any Ts.ED CLI command as a Model Context Protocol (MCP) server. MCP-aware clients (Claude Desktop, VS Code Agents, Cursor, etc.) can then invoke your generators without shell access.

Installation

Add the MCP package anywhere you build CLI commands or standalone servers:

bash
npm install @tsed/cli-mcp @modelcontextprotocol/sdk
bash
yarn add @tsed/cli-mcp @modelcontextprotocol/sdk
bash
pnpm add @tsed/cli-mcp @modelcontextprotocol/sdk
bash
bun add @tsed/cli-mcp @modelcontextprotocol/sdk

The package has no global side effects. You opt-in by bootstrapping a server or by importing the helpers in your own CLI binary.

Define tools

Use defineTool (functional) or Tool (decorator) to register MCP tools with the Ts.ED DI container. Each handler still executes inside the CLI’s DI context, so you can reuse existing services, and the request/response shapes follow the @modelcontextprotocol/sdk contract.

ts
import {Tool} from "@tsed/cli-mcp";
import {Injectable} from "@tsed/di";
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}!`
      }
    };
  }
}
ts
import {defineTool} from "@tsed/cli-mcp";
import {s} from "@tsed/schema";

interface HelloArgs {
  name: string;
}

export const helloTool = defineTool<HelloArgs>({
  name: "hello",
  description: "Greets callers from any MCP client",
  inputSchema: () =>
    s.object({
      name: s.string().description("Name to include in the greeting").prompt("Who should we greet?")
    }),
  outputSchema: () =>
    s.object({
      message: s.string().description("Structured greeting payload")
    }),
  async handler({name}) {
    return {
      content: [
        {
          type: "text",
          text: `Hello, ${name}!`
        }
      ],
      structuredContent: {
        message: `Hello, ${name}!`
      }
    };
  }
});

Define resources

Expose immutable documents or live data streams by registering MCP resources through defineResource or Resource. These helpers wrap the response models described in @modelcontextprotocol/sdk so you only need to return the contents array.

ts
import {Resource} from "@tsed/cli-mcp";
import {Injectable} from "@tsed/di";

@Injectable()
export class ChangelogResource {
  @Resource("changelog://latest", {
    title: "Latest CLI releases",
    description: "Surface Ts.ED CLI release notes to MCP clients.",
    mimeType: "text/markdown"
  })
  async latest() {
    return {
      contents: [
        {
          uri: "changelog://latest",
          text: "- feat: interactive CLI docs available at https://cli.tsed.dev/guide/cli/overview"
        }
      ]
    };
  }
}
ts
import {defineResource} from "@tsed/cli-mcp";

export const changelogResource = defineResource({
  name: "changelog",
  uri: "changelog://latest",
  title: "Latest CLI releases",
  description: "Surface Ts.ED CLI release notes to MCP clients.",
  mimeType: "text/markdown",
  async handler() {
    return {
      contents: [
        {
          uri: "changelog://latest",
          text: "- feat: interactive CLI docs available at https://cli.tsed.dev/guide/cli/overview"
        }
      ]
    };
  }
});

Define prompts

definePrompt and Prompt let you publish reusable prompt templates that MCP clients can fill before invoking your CLI. Describe the arguments with @tsed/schema builders— they are converted automatically into the schema format expected by @modelcontextprotocol/sdk.

ts
import {Prompt} from "@tsed/cli-mcp";
import {Injectable} from "@tsed/di";
import {Description, s} from "@tsed/schema";

@Injectable()
export class PlanPrompt {
  @Prompt({
    name: "generate-plan",
    title: "Generation plan",
    description: "Summarize how the CLI will scaffold files.",
    argsSchema: () =>
      s.object({
        name: s.string().description("Project codename"),
        runtime: s.enums(["node", "bun"] as const).description("Runtime selected by the developer")
      })
  })
  @Description("Outline the scaffolding steps for the selected runtime.")
  handle({name, runtime}: {name: string; runtime: "node" | "bun"}) {
    return {
      description: `Outline the steps to scaffold ${name} for ${runtime}.`,
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `Project "${name}" targets the ${runtime} runtime. Produce a checklist of generation steps.`
          }
        }
      ]
    };
  }
}
ts
import {definePrompt} from "@tsed/cli-mcp";
import {s} from "@tsed/schema";

interface PlanArgs {
  name: string;
  runtime: "node" | "bun";
}

export const planPrompt = definePrompt<PlanArgs>({
  name: "generate-plan",
  title: "Generation plan",
  description: "Summarize how the CLI will scaffold files.",
  argsSchema: () =>
    s.object({
      name: s.string().description("Project codename"),
      runtime: s.enums<PlanArgs["runtime"]>(["node", "bun"]).description("Runtime selected by the developer")
    }),
  handler({name, runtime}) {
    return {
      description: `Outline the steps to scaffold ${name} for ${runtime}.`,
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `Project "${name}" targets the ${runtime} runtime. Produce a checklist of generation steps.`
          }
        }
      ]
    };
  }
});

Wiring transports and authentication

The CLI injects the MCP_SERVER token, which exposes connect(mode) to start stdio or HTTP transports:

ts
import {inject} from "@tsed/di";
import {MCP_SERVER} from "@tsed/cli-mcp";

await inject(MCP_SERVER).connect("stdio"); // or "streamable-http"

Use the HTTP/SSE/WS transports when you need to host an MCP server remotely. Always guard these endpoints:

  • Authentication: Require a token or mTLS client certificate before allowing MCP connections.
  • Sandboxing: Tools can execute generators, shell commands, or filesystem writes. Keep the MCP server inside a locked-down container when exposing it outside localhost.
  • Rate limiting: Wrap handlers with Ts.ED interceptors that throttle high-risk calls (e.g., file generation, database migrations).

Integrating with the CLI binary

If you want to ship an MCP server with your CLI distribution, add an entrypoint (for example via command) that calls inject(MCP_SERVER).connect(...). You can stick with decorators or the functional helper:

ts
import {Command, type CommandProvider} from "@tsed/cli-core";
import {MCP_SERVER} from "@tsed/cli-mcp";
import {inject} from "@tsed/di";
import {s} from "@tsed/schema";

const McpSchema = s.object({
  http: s.boolean().default(false).description("Run MCP using HTTP server").opt("--http")
});

@Command({
  name: "mcp",
  description: "Run a MCP server",
  inputSchema: McpSchema
})
export class McpCommand implements CommandProvider<{http: boolean}> {
  async $exec({http}: {http: boolean}) {
    return inject(MCP_SERVER).connect(http ? "streamable-http" : "stdio");
  }
}
ts
import {command} from "@tsed/cli-core";
import {MCP_SERVER} from "@tsed/cli-mcp";
import {inject} from "@tsed/di";
import {s} from "@tsed/schema";

const McpSchema = s.object({
  http: s.boolean().default(false).description("Run MCP using HTTP server").opt("--http")
});

export const McpCommand = command({
  name: "mcp",
  description: "Run a MCP server",
  inputSchema: McpSchema,
  handler({http}) {
    return inject(MCP_SERVER).connect(http ? "streamable-http" : "stdio");
  }
}).token();

Publish the command the same way you register other CLI commands, then launch it through Node + SWC:

bash
node --import @swc-node/register/esm-register src/bin/index.ts mcp --http

Want to smoke-test your tools, prompts, and resources without wiring a full client? Run the MCP Inspector locally so you can call everything interactively:

bash
npx @modelcontextprotocol/inspector node -e NODE_ENV=development --import @swc-node/register/esm-register bin/dev.ts mcp

Released under the MIT License.