JSPM

  • Created
  • Published
  • Downloads 1439
  • Score
    100M100P100Q91830F
  • License Apache-2.0

The definitive, production-grade template for building powerful and scalable Model Context Protocol (MCP) servers with TypeScript, featuring built-in observability (OpenTelemetry), declarative tooling, robust error handling, and a modular, DI-driven architecture.

Package Exports

  • mcp-ts-template
  • mcp-ts-template/index.js

Readme

mcp-ts-template

Production-grade TypeScript template for building Model Context Protocol (MCP) servers. Ships with declarative tools/resources, robust error handling, DI, easy auth, optional OpenTelemetry, and first-class support for both local and edge (Cloudflare Workers) runtimes.

Version MCP Spec MCP SDK License Status TypeScript Bun Code Coverage


✨ Features

  • Declarative Tools & Resources: Define capabilities in single, self-contained files. The framework handles registration and execution.
  • Elicitation Support: Tools can interactively prompt the user for missing parameters during execution, streamlining user workflows.
  • Robust Error Handling: A unified McpError system ensures consistent, structured error responses across the server.
  • Pluggable Authentication: Secure your server with zero-fuss support for none, jwt, or oauth modes.
  • Abstracted Storage: Swap storage backends (in-memory, filesystem, Supabase, Cloudflare KV/R2) without changing business logic.
  • Full-Stack Observability: Get deep insights with structured logging (Pino) and optional, auto-instrumented OpenTelemetry for traces and metrics.
  • Dependency Injection: Built with tsyringe for a clean, decoupled, and testable architecture.
  • Edge-Ready: Write code once and run it seamlessly on your local machine or at the edge on Cloudflare Workers.

🚀 Getting Started

Prerequisites

Installation

  1. Clone the repository:
git clone https://github.com/cyanheads/mcp-ts-template.git
  1. Navigate into the directory:
cd mcp-ts-template
  1. Install dependencies:
bun install

🛠️ Understanding the Template: Tools & Resources

This template includes working examples of tools and resources.

1. Example Tool: template_echo_message

This tool echoes back a message with optional formatting. You can find the full source at src/mcp-server/tools/definitions/template-echo-message.tool.ts.

Click to see the `echoTool` definition structure
// Located at: src/mcp-server/tools/definitions/template-echo-message.tool.ts
import { z } from 'zod';
import type {
  SdkContext,
  ToolDefinition,
} from '@/mcp-server/tools/utils/toolDefinition.js';
import { withToolAuth } from '@/mcp-server/transports/auth/lib/withAuth.js';
import { type RequestContext, logger } from '@/utils/index.js';

// 1. Define Input and Output Schemas with Zod for validation.
const InputSchema = z.object({
  message: z.string().min(1).describe('The message to echo back.'),
  mode: z
    .enum(['standard', 'uppercase', 'lowercase'])
    .default('standard')
    .describe('Formatting mode.'),
  repeat: z
    .number()
    .int()
    .min(1)
    .max(5)
    .default(1)
    .describe('Number of times to repeat the message.'),
});

const OutputSchema = z.object({
  repeatedMessage: z
    .string()
    .describe('The final, formatted, and repeated message.'),
  // ... other fields from the actual file
});

// 2. Implement the pure business logic for the tool.
async function echoToolLogic(
  input: z.infer<typeof InputSchema>,
  appContext: RequestContext,
  sdkContext: SdkContext,
): Promise<z.infer<typeof OutputSchema>> {
  // ... logic to format and repeat the message
  const formattedMessage = input.message.toUpperCase(); // simplified for example
  const repeatedMessage = Array(input.repeat).fill(formattedMessage).join(' ');
  return { repeatedMessage };
}

// 3. Assemble the final Tool Definition.
export const echoTool: ToolDefinition<typeof InputSchema, typeof OutputSchema> =
  {
    name: 'template_echo_message', // The official tool name
    title: 'Template Echo Message',
    description:
      'Echoes a message back with optional formatting and repetition.',
    inputSchema: InputSchema,
    outputSchema: OutputSchema,
    logic: withToolAuth(['tool:echo:read'], echoToolLogic), // Secure the tool
  };

The echoTool is registered in src/mcp-server/tools/definitions/index.ts, making it available to the server on startup. For an example of how to use the new elicitation feature, see template_madlibs_elicitation.tool.ts.

2. Example Resource: echo-resource

This resource provides a simple echo response via a URI. The source is located at src/mcp-server/resources/definitions/echo.resource.ts.

Click to see the `echoResourceDefinition` structure
// Located at: src/mcp-server/resources/definitions/echo.resource.ts
import { z } from 'zod';
import type { ResourceDefinition } from '@/mcp-server/resources/utils/resourceDefinition.js';
import { withResourceAuth } from '@/mcp-server/transports/auth/lib/withAuth.js';
import { type RequestContext, logger } from '@/utils/index.js';

// 1. Define Parameter and Output Schemas.
const ParamsSchema = z.object({
  message: z.string().optional().describe('Message to echo from the URI.'),
});

const OutputSchema = z.object({
  message: z.string().describe('The echoed message.'),
  timestamp: z.string().datetime().describe('Timestamp of the response.'),
  requestUri: z.string().url().describe('The original request URI.'),
});

// 2. Implement the pure read logic for the resource.
function echoLogic(
  uri: URL,
  params: z.infer<typeof ParamsSchema>,
  context: RequestContext,
): z.infer<typeof OutputSchema> {
  const messageToEcho = params.message || uri.hostname || 'Default echo';
  return {
    message: messageToEcho,
    timestamp: new Date().toISOString(),
    requestUri: uri.href,
  };
}

// 3. Assemble the final Resource Definition.
export const echoResourceDefinition: ResourceDefinition<
  typeof ParamsSchema,
  typeof OutputSchema
> = {
  name: 'echo-resource', // The official resource name
  title: 'Echo Message Resource',
  description: 'A simple echo resource that returns a message.',
  uriTemplate: 'echo://{message}',
  paramsSchema: ParamsSchema,
  outputSchema: OutputSchema,
  logic: withResourceAuth(['resource:echo:read'], echoLogic), // Secure the resource
};

Like the tool, echoResourceDefinition is registered in src/mcp-server/resources/definitions/index.ts.

⚙️ Core Concepts

Configuration

All configuration is centralized and validated at startup in src/config/index.ts. Key environment variables in your .env file include:

Variable Description Default
MCP_TRANSPORT_TYPE The transport to use: stdio or http. http
MCP_HTTP_PORT The port for the HTTP server. 3010
MCP_AUTH_MODE Authentication mode: none, jwt, or oauth. none
STORAGE_PROVIDER_TYPE Storage backend: in-memory, filesystem, supabase, cloudflare-kv, r2. in-memory
OTEL_ENABLED Set to true to enable OpenTelemetry. false
LOG_LEVEL The minimum level for logging. info

Authentication & Authorization

  • Modes: none (default), jwt (requires MCP_AUTH_SECRET_KEY), or oauth (requires OAUTH_ISSUER_URL and OAUTH_AUDIENCE).
  • Enforcement: Wrap your tool/resource logic functions with withToolAuth([...]) or withResourceAuth([...]) to enforce scope checks. Scope checks are bypassed for developer convenience when auth mode is none.

Storage

  • Service: A DI-managed StorageService provides a consistent API for persistence. Never access fs or other storage SDKs directly from tool logic.
  • Providers: The default is in-memory. Node-only providers include filesystem. Edge-compatible providers include supabase, cloudflare-kv, and cloudflare-r2.
  • Multi-Tenancy: The StorageService requires context.tenantId. This is automatically propagated from the tid claim in a JWT when auth is enabled.

Observability

  • Structured Logging: Pino is integrated out-of-the-box. All logs are JSON and include the RequestContext.
  • OpenTelemetry: Disabled by default. Enable with OTEL_ENABLED=true and configure OTLP endpoints. Traces, metrics (duration, payload sizes), and errors are automatically captured for every tool call.

▶️ Running the Server

Local Development

  • Build and run the production version:

    # One-time build
    bun rebuild
    
    # Run the built server
    bun start:http
    # or
    bun start:stdio
  • Run checks and tests:

    bun devcheck # Lints, formats, type-checks, and more
    bun test # Runs the test suite

Cloudflare Workers

  1. Build the Worker bundle:
bun build:worker
  1. Run locally with Wrangler:
bun deploy:dev
  1. Deploy to Cloudflare: sh bun deploy:prod > Note: The wrangler.toml file is pre-configured to enable nodejs_compat for best results.

📂 Project Structure

Directory Purpose & Contents
src/mcp-server/tools/definitions Your tool definitions (*.tool.ts). This is where you add new capabilities.
src/mcp-server/resources/definitions Your resource definitions (*.resource.ts). This is where you add new data sources.
src/mcp-server/transports Implementations for HTTP and STDIO transports, including auth middleware.
src/storage The StorageService abstraction and all storage provider implementations.
src/services Integrations with external services (e.g., the default OpenRouter LLM provider).
src/container Dependency injection container registrations and tokens.
src/utils Core utilities for logging, error handling, performance, security, and telemetry.
src/config Environment variable parsing and validation with Zod.
tests/ Unit and integration tests, mirroring the src/ directory structure.

🧑‍💻 Agent Development Guide

For a strict set of rules when using this template with an AI agent, please refer to AGENTS.md. Key principles include:

  • Logic Throws, Handlers Catch: Never use try/catch in your tool/resource logic. Throw an McpError instead.
  • Use Elicitation for Missing Input: If a tool requires user input that wasn't provided, use the elicitInput function from the SdkContext to ask the user for it.
  • Pass the Context: Always pass the RequestContext object through your call stack.
  • Use the Barrel Exports: Register new tools and resources only in the index.ts barrel files.

❓ FAQ

  • Does this work with both STDIO and Streamable HTTP?
    • Yes. Both transports are first-class citizens. Use bun run dev:stdio or bun run dev:http.
  • Can I deploy this to the edge?
    • Yes. The template is designed for Cloudflare Workers. Run bun run build:worker and deploy with Wrangler.
  • Do I have to use OpenTelemetry?
    • No, it is disabled by default. Enable it by setting OTEL_ENABLED=true in your .env file.
  • How do I publish my server to the MCP Registry?
    • Follow the step-by-step guide in docs/publishing-mcp-server-registry.md.

🤝 Contributing

Issues and pull requests are welcome! If you plan to contribute, please run the local checks and tests before submitting your PR.

bun run devcheck
bun test

📜 License

This project is licensed under the Apache 2.0 License. See the LICENSE file for details.