Warning
Unfortunately i published the 1.0 by mistake...this package is currently under heavy development so there will be breaking changes in minors...threat this 1.x as the 0.x of any other package. Sorry for the disservice, every breaking will be properly labeled in the PR name.
tmcp
A lightweight, schema-agnostic Model Context Protocol (MCP) server implementation with unified API design.
Why tmcp?
tmcp offers significant advantages over the official MCP SDK:
- 🔄 Schema Agnostic: Works with any validation library through adapters
- 📦 No Weird Dependencies: Minimal footprint with only essential dependencies (looking at you
express) - 🎯 Unified API: Consistent, intuitive interface across all MCP capabilities
- 🔌 Extensible: Easy to add support for new schema libraries
- âš¡ Lightweight: No bloat, just what you need
Supported Schema Libraries
tmcp works with all major schema validation libraries through its adapter system:
- Zod -
@tmcp/adapter-zod - Valibot -
@tmcp/adapter-valibot - ArkType -
@tmcp/adapter-arktype - Effect Schema -
@tmcp/adapter-effect - Zod v3 -
@tmcp/adapter-zod-v3
Installation
pnpm install tmcp
# Choose your preferred schema library adapter
pnpm install @tmcp/adapter-zod zod
# Choose your preferred transport
pnpm install @tmcp/transport-stdio # For CLI/desktop apps
pnpm install @tmcp/transport-http # For web-based clientsQuick Start
Standard I/O Transport (CLI/Desktop)
import { McpServer } from 'tmcp';
import { ZodJsonSchemaAdapter } from '@tmcp/adapter-zod';
import { StdioTransport } from '@tmcp/transport-stdio';
import { z } from 'zod';
const adapter = new ZodJsonSchemaAdapter();
const server = new McpServer(
{
name: 'my-server',
version: '1.0.0',
description: 'My awesome MCP server',
},
{
adapter,
capabilities: {
tools: { listChanged: true },
prompts: { listChanged: true },
resources: { listChanged: true },
},
},
);
// While the adapter is optional (you can opt out by explicitly passing `adapter: undefined`) without an adapter the server cannot accept inputs, produce structured outputs, or request elicitations at all only do this for very simple servers.
// Define a tool with type-safe schema
server.tool(
{
name: 'calculate',
description: 'Perform mathematical calculations',
schema: z.object({
operation: z.enum(['add', 'subtract', 'multiply', 'divide']),
a: z.number(),
b: z.number(),
}),
},
async ({ operation, a, b }) => {
switch (operation) {
case 'add':
return a + b;
case 'subtract':
return a - b;
case 'multiply':
return a * b;
case 'divide':
return a / b;
}
},
);
// Start the server with stdio transport
const transport = new StdioTransport(server);
transport.listen();HTTP Transport (Web-based)
import { McpServer } from 'tmcp';
import { ZodJsonSchemaAdapter } from '@tmcp/adapter-zod';
import { HttpTransport } from '@tmcp/transport-http';
import { z } from 'zod';
const adapter = new ZodJsonSchemaAdapter();
const server = new McpServer(/* ... same server config ... */);
// Add tools as above...
// Create HTTP transport
const transport = new HttpTransport(server);
// Use with your preferred HTTP server (Bun example)
Bun.serve({
port: 3000,
async fetch(req) {
const response = await transport.respond(req);
if (response === null) {
return new Response('Not Found', { status: 404 });
}
return response;
},
});API Reference
McpServer
The main server class that handles MCP protocol communications.
Constructor
new McpServer(serverInfo, options);serverInfo: Server metadata (name, version, description)options: Configuration object with optional adapter (for schema conversion) and capabilities
Important
While the adapter is optional (you can opt out by explicitly passing adapter: undefined) without an adapter the server cannot accept inputs, produce structured outputs, or request elicitations at all only do this for very simple servers.
Methods
tool(definition, handler)
Register a tool with optional schema validation.
server.tool(
{
name: 'tool-name',
description: 'Tool description',
schema: yourSchema, // optional
},
async (input) => {
// Tool implementation
return result;
},
);prompt(definition, handler)
Register a prompt template with optional schema validation.
server.prompt(
{
name: 'prompt-name',
description: 'Prompt description',
schema: yourSchema, // optional
complete: (arg, context) => ['completion1', 'completion2'] // optional
},
async (input) => {
// Prompt implementation
return { messages: [...] };
}
);resource(definition, handler)
Register a static resource.
server.resource(
{
name: 'resource-name',
description: 'Resource description',
uri: 'file://path/to/resource'
},
async (uri, params) => {
// Resource implementation
return { contents: [...] };
}
);template(definition, handler)
Register a URI template for dynamic resources.
server.template(
{
name: 'template-name',
description: 'Template description',
uri: 'file://path/{id}/resource',
complete: (arg, context) => ['id1', 'id2'] // optional
},
async (uri, params) => {
// Template implementation using params.id
return { contents: [...] };
}
);receive(request, context?)
Process an incoming MCP request with optional context information.
const response = server.receive(jsonRpcRequest, {
sessionId: 'session-123',
auth: authInfo,
sessionInfo: {
clientCapabilities, // cached from the client's initialize call
clientInfo, // client name/version metadata
logLevel, // last log level requested by the client
},
custom: {
userId: 'alice',
},
});Tip
sessionInfo is automatically populated by the built-in transports. You can read it from handlers via server.ctx.sessionInfo to tailor behaviour to the connected client.
Advanced Examples
Multiple Schema Libraries
// Use different schemas for different tools
import { z } from 'zod';
import * as v from 'valibot';
server.tool(
{
name: 'zod-tool',
schema: z.object({ name: z.string() }),
},
async ({ name }) => `Hello ${name}`,
);
server.tool(
{
name: 'valibot-tool',
schema: v.object({ age: v.number() }),
},
async ({ age }) => `Age: ${age}`,
);Resource Templates with Completion
server.template(
{
name: 'user-profile',
description: 'Get user profile by ID',
uri: 'users/{userId}/profile',
complete: (arg, context) => {
// Provide completions for userId parameter
return ['user1', 'user2', 'user3'];
},
},
async (uri, params) => {
const user = await getUserById(params.userId);
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(user),
},
],
};
},
);Complex Validation
const complexSchema = z.object({
user: z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().min(18).max(120),
}),
preferences: z
.object({
theme: z.enum(['light', 'dark']),
notifications: z.boolean(),
})
.optional(),
tags: z.array(z.string()).default([]),
});
server.tool(
{
name: 'create-user',
description: 'Create a new user with preferences',
schema: complexSchema,
},
async (input) => {
// Input is fully typed and validated
const { user, preferences, tags } = input;
return await createUser(user, preferences, tags);
},
);Contributing
Contributions are welcome! Please see our contributing guidelines for details.
Acknowledgments
Huge thanks to Sean O'Bannon that provided us with the @tmcp scope on npm.
License
MIT © Paolo Ricciuti