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.
✨ 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
, oroauth
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
- Bun v1.2.0 or higher.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/mcp-ts-template.git
- Navigate into the directory:
cd mcp-ts-template
- 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
(requiresMCP_AUTH_SECRET_KEY
), oroauth
(requiresOAUTH_ISSUER_URL
andOAUTH_AUDIENCE
). - Enforcement: Wrap your tool/resource
logic
functions withwithToolAuth([...])
orwithResourceAuth([...])
to enforce scope checks. Scope checks are bypassed for developer convenience when auth mode isnone
.
Storage
- Service: A DI-managed
StorageService
provides a consistent API for persistence. Never accessfs
or other storage SDKs directly from tool logic. - Providers: The default is
in-memory
. Node-only providers includefilesystem
. Edge-compatible providers includesupabase
,cloudflare-kv
, andcloudflare-r2
. - Multi-Tenancy: The
StorageService
requirescontext.tenantId
. This is automatically propagated from thetid
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
- Build the Worker bundle:
bun build:worker
- Run locally with Wrangler:
bun deploy:dev
- Deploy to Cloudflare:
sh bun deploy:prod
> Note: Thewrangler.toml
file is pre-configured to enablenodejs_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/resourcelogic
. Throw anMcpError
instead. - Use Elicitation for Missing Input: If a tool requires user input that wasn't provided, use the
elicitInput
function from theSdkContext
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
orbun run dev:http
.
- Yes. Both transports are first-class citizens. Use
- Can I deploy this to the edge?
- Yes. The template is designed for Cloudflare Workers. Run
bun run build:worker
and deploy with Wrangler.
- Yes. The template is designed for Cloudflare Workers. Run
- Do I have to use OpenTelemetry?
- No, it is disabled by default. Enable it by setting
OTEL_ENABLED=true
in your.env
file.
- No, it is disabled by default. Enable it by setting
- How do I publish my server to the MCP Registry?
- Follow the step-by-step guide in
docs/publishing-mcp-server-registry.md
.
- Follow the step-by-step guide in
🤝 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.