The main tmcp library
npm install tmcp> [!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.
A lightweight, schema-agnostic Model Context Protocol (MCP) server implementation with unified API design.
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
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
``bash`
pnpm install tmcpChoose your preferred schema library adapter
pnpm install @tmcp/adapter-zod zod
`javascript
import { McpServer } from 'tmcp';
import { ZodJsonSchemaAdapter } from '@tmcp/adapter-zod';
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 {
content: [{ type: 'text', text: ${a} + ${b} = ${a + b} }],${a} - ${b} = ${a - b}
};
case 'subtract':
return {
content: [{ type: 'text', text: }],${a} Γ ${b} = ${a * b}
};
case 'multiply':
return {
content: [{ type: 'text', text: }],${a} Γ· ${b} = ${a / b}
};
case 'divide':
return {
content: [{ type: 'text', text: }],
};
}
},
);
// Process incoming requests
server.receive(request);
`
Even the tiny calculator tool above ends with four near-identical return { content: [{ type: 'text', text: ... }] } blocks. Most MCP handlers have to build the same shapes: CallToolResult, ReadResourceResult, GetPromptResult, or CompleteResult. Writing them by hand is repetitive boilerplate. The tmcp/utils entry point ships a handful of factories that emit the correct shape for you.
`ts
import { tool, resource, prompt, complete } from 'tmcp/utils';
// Without helpers β lots of envelope noise
server.tool(
{ name: 'health-check', description: 'A health check tool' },
async () => ({
content: [{ type: 'text', text: 'ok' }],
}),
);
// With helpers β just describe the payload once
server.tool(
{ name: 'health-check', description: 'A health check tool' },
async () => tool.text('ok'),
);
server.tool(
{ name: 'profile-picture', description: 'Your profile picture' },
async () => tool.media('image', await loadPng(), 'image/png'),
);
server.tool(
{
name: 'get-images',
description:
'Get the orders details and the images of all the products',
schema: v.object({
items: v.array(ItemsSchema),
}),
},
async () => {
const orders = await loadOrders();
const images = await loadImages(orders);
return tool.mix(
[
tool.text("Here's your images"),
...images.map((img) => tool.media('image', img)),
],
orders,
);
},
);
server.resource(
{
name: 'readme',
description: 'Top-level README',
uri: 'file://README.md',
},
async (uri) =>
resource.text(uri, await readFile(uri, 'utf8'), 'text/markdown'),
);
server.prompt(
{
name: 'explain',
description: '',
schema: v.object({ topic: v.string() }),
},
async ({ topic }) => prompt.message(Explain ${topic} like I am five.),
);
server.template(
{
name: 'users',
description: 'Supports completion',
uri: 'users/{id}',
complete: {
id: async (arg) =>
complete.values(await findMatchingIds(arg), false),
},
},
async (uri) => resource.blob(uri, await fetchUserBlob(uri)),
);
`
you can also compose different kind of tools with tool.mix
`ts`
tool.mix([
tool.text('Indexed workspace'),
tool.media('image', png, 'image/png'),
]);
however be aware that
1. you can't pass tool.structured to tool.mix (but you can pass a second argument that will be the structured content)tool.error
2. if you pass even one to the tool.mix the whole return value will be an error
`ts
const structuredContent = {
cool: true,
};
tool.mix(
[
tool.text(JSON.stringify(structuredContent)),
tool.media('image', png, 'image/png'),
],
structuredContent,
);
`
- tool β build CallToolResult objects via text, error, media, resource, resourceLink, structured, and mix (merge multiple kind of tool response).resource
- β return ReadResourceResult through text, blob, or mix (merge resource.text and resource.blob).prompt
- β generate GetPromptResult using message (single string), messages (array of strings), text, media, resource, resourceLink and mix (merge multiple kind of tool response).complete
- β create CompleteResult payloads with values(list, hasMore?, total?).
All helpers are typed, so TypeScript will prevent you from returning malformed payloads while cutting down the repetitive envelopes that used to appear in almost every handler.
For better code organization and reusability, you can define your tools, prompts, resources, and templates in separate files using the defineTool, definePrompt, defineResource, and defineTemplate utilities. This approach allows you to:
- Organize by feature: Group related tools in feature-specific modules
- Reuse across servers: Share common tools between different MCP servers
`javascript
// tools/calculator.js
import { defineTool } from 'tmcp/tool';
import { z } from 'zod';
export const addTool = defineTool(
{
name: 'add',
description: 'Add two numbers',
schema: z.object({
a: z.number(),
b: z.number(),
}),
},
async ({ a, b }) => ({
content: [{ type: 'text', text: ${a} + ${b} = ${a + b} }],
}),
);
export const multiplyTool = defineTool(
{
name: 'multiply',
description: 'Multiply two numbers',
schema: z.object({
a: z.number(),
b: z.number(),
}),
},
async ({ a, b }) => ({
content: [{ type: 'text', text: ${a} Γ ${b} = ${a * b} }],`
}),
);
`javascript
// prompts/code-review.js
import { definePrompt } from 'tmcp/prompt';
import { z } from 'zod';
export const codeReviewPrompt = definePrompt(
{
name: 'code-review',
description: 'Generate code review prompt',
schema: z.object({
code: z.string(),
language: z.string(),
}),
},
async ({ code, language }) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: Review this ${language} code:\n\n${code},`
},
},
],
}),
);
`javascript
// resources/files.js
import { defineResource } from 'tmcp/resource';
import { readFile } from 'node:fs/promises';
export const readmeResource = defineResource(
{
name: 'readme',
description: 'Project README file',
uri: 'file://README.md',
},
async (uri) => ({
contents: [
{
uri,
mimeType: 'text/markdown',
text: await readFile(uri.replace('file://', ''), 'utf8'),
},
],
}),
);
`
`javascript
// templates/user-files.js
import { defineTemplate } from 'tmcp/template';
import { getUserFile } from '../api/users.js';
export const userFileTemplate = defineTemplate(
{
name: 'user-files',
description: 'Access user files by ID',
uri: 'users/{userId}/files/{filename}',
complete: {
userId: async (arg) => ({
completion: {
values: await searchUserIds(arg),
hasMore: false,
},
}),
},
},
async (uri, { userId, filename }) => ({
contents: [
{
uri,
mimeType: 'text/plain',
text: await getUserFile(userId, filename),
},
],
}),
);
`
`javascript
// server.js
import { McpServer } from 'tmcp';
import { ZodJsonSchemaAdapter } from '@tmcp/adapter-zod';
import { addTool, multiplyTool } from './tools/calculator.js';
import { codeReviewPrompt } from './prompts/code-review.js';
import { readmeResource } from './resources/files.js';
import { userFileTemplate } from './templates/user-files.js';
const server = new McpServer(
{
name: 'my-server',
version: '1.0.0',
},
{
adapter: new ZodJsonSchemaAdapter(),
},
);
// Register multiple tools at once
server.tools([addTool, multiplyTool]);
// Or register individually
server.tool(addTool);
// Same for prompts, resources, and templates
server.prompts([codeReviewPrompt]);
server.resources([readmeResource]);
server.templates([userFileTemplate]);
`
Adding the primitive to the server will error if the tool uses a different validation library than the one expressed in the adapter.
The main server class that handles MCP protocol communications.
#### Constructor
`javascript`
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) / tools(definitions)
Register one or more tools with optional schema validation.
`javascript
// Register a single tool inline
server.tool(
{
name: 'tool-name',
description: 'Tool description',
schema: yourSchema, // optional
},
async (input) => {
// Tool implementation
return result;
},
);
// Register a tool created with defineTool
import { defineTool } from 'tmcp/tool';
const myTool = defineTool(
{
name: 'tool-name',
description: 'Tool description',
schema: yourSchema,
},
async (input) => {
return result;
},
);
server.tool(myTool);
// Register multiple tools at once
server.tools([tool1, tool2, tool3]);
`
sessionInfo contains the latest clientCapabilities, clientInfo, and logLevel reported by the connected client. Built-in transports populate it automatically, so every handler can branch on client features without additional bookkeeping.
##### prompt(definition, handler) / prompts(definitions)
Register one or more prompt templates with optional schema validation.
`javascript
// Register a single prompt inline
server.prompt(
{
name: 'prompt-name',
description: 'Prompt description',
schema: yourSchema, // optional
complete: (arg, context) => ['completion1', 'completion2'] // optional
},
async (input) => {
// Prompt implementation
return { messages: [...] };
}
);
// Register a prompt created with definePrompt
import { definePrompt } from 'tmcp/prompt';
const myPrompt = definePrompt(
{
name: 'prompt-name',
description: 'Prompt description',
schema: yourSchema,
},
async (input) => {
return { messages: [...] };
}
);
server.prompt(myPrompt);
// Register multiple prompts at once
server.prompts([prompt1, prompt2, prompt3]);
`
##### resource(definition, handler) / resources(definitions)
Register one or more static resources.
`javascript
// Register a single resource inline
server.resource(
{
name: 'resource-name',
description: 'Resource description',
uri: 'file://path/to/resource'
},
async (uri, params) => {
// Resource implementation
return { contents: [...] };
}
);
// Register a resource created with defineResource
import { defineResource } from 'tmcp/resource';
const myResource = defineResource(
{
name: 'resource-name',
description: 'Resource description',
uri: 'file://path/to/resource'
},
async (uri) => {
return { contents: [...] };
}
);
server.resource(myResource);
// Register multiple resources at once
server.resources([resource1, resource2, resource3]);
`
##### template(definition, handler) / templates(definitions)
Register one or more URI templates for dynamic resources.
`javascript
// Register a single template inline
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: [...] };
}
);
// Register a template created with defineTemplate
import { defineTemplate } from 'tmcp/template';
const myTemplate = defineTemplate(
{
name: 'template-name',
description: 'Template description',
uri: 'file://path/{id}/resource',
},
async (uri, params) => {
return { contents: [...] };
}
);
server.template(myTemplate);
// Register multiple templates at once
server.templates([template1, template2, template3]);
`
##### withContext
Specify the type of custom context for type-safe access to application-specific data.
`javascript
interface MyCustomContext {
userId: string;
permissions: string[];
database: DatabaseConnection;
}
const server = new McpServer(serverInfo, options).withContext
// Now you can access typed custom context in handlers
server.tool(
{
name: 'get-user-data',
description: 'Get current user data',
},
async () => {
const { userId, database } = server.ctx.custom!;
const userData = await database.users.findById(userId);
return {
content: [{ type: 'text', text: JSON.stringify(userData) }],
};
},
);
`
##### ctx
Access the current request context, including session ID, auth info, client metadata, and custom context.
`javascript
server.tool(
{
name: 'context-aware-tool',
description: 'Tool that uses request context',
},
async () => {
const { sessionId, auth, sessionInfo, custom } = server.ctx;
if (!custom?.userId) {
throw new Error('User authentication required');
}
if (sessionInfo?.clientInfo) {
console.log(Serving ${sessionInfo.clientInfo.name});
}
return {
content: [
{
type: 'text',
text: Hello user ${custom.userId} in session ${sessionId},`
},
],
};
},
);
##### receive(request, context?)
Process an incoming MCP request with optional context.
`javascript
// Basic usage
const response = server.receive(jsonRpcRequest);
// With custom context (typically used by transports)
const response = server.receive(jsonRpcRequest, {
sessionId: 'session-123',
auth: authInfo,
sessionInfo: {
clientCapabilities,
clientInfo,
logLevel,
},
custom: customContextData,
});
`
##### request({ method, params })
Send a raw JSON-RPC request to the connected client. This lets you call
experimental MCP APIs or proprietary extensions before they gain a dedicated
method on McpServer or to send a request with a custom JSON-schema that is not expressible with your validation library.
`javascript`
const response = await server.request({
method: 'elicitation/create',
params: {
message: 'Describe the deployment plan',
requestedSchema: {
type: 'object',
required: ['region', 'replicas', 'features'],
properties: {
region: {
type: 'string',
enum: ['us-east-1', 'us-west-2', 'eu-central-1'],
},
replicas: { type: 'integer', minimum: 1, maximum: 20 },
features: {
type: 'array',
items: {
type: 'string',
enum: ['canary', 'observability', 'autoscaling'],
},
minItems: 1,
},
},
},
},
});
- method: Fully qualified MCP client method nameparams
- (optional): JSON-RPC params object/array accepted by that method
Handle the resolved payload like any other JSON-RPC responseβcast or (better) validate
as needed when using this escape hatch.
##### elicitation(schema)
Request client elicitation with schema validation.
`javascript`
const result = await server.elicitation(schema);
##### message(request)
Request language model sampling from the client.
`javascript`
const result = await server.message({
messages: [
{
role: 'user',
content: { type: 'text', text: 'Hello!' },
},
],
});
##### refreshRoots()
Refresh the roots list from the client.
`javascript`
await server.refreshRoots();
console.log(server.roots); // Access current roots
##### changed(type, id)
Send notifications for subscriptions. When you call changed('resource', uri) the server now emits a broadcast notification, and the built-in transports look up which sessions subscribed to that URI before delivering it.
`javascript`
server.changed('resource', 'file://path/to/resource');
##### progress(progress, total?, message?)
Report progress during long-running operations. Progress notifications are only sent when a progress token is provided by the client in the request's _meta.progressToken field.
`javascript
server.tool(
{
name: 'process-large-file',
description: 'Process a large file with progress updates',
},
async (input) => {
const totalSteps = 100;
for (let i = 0; i <= totalSteps; i++) {
// Report progress with current step, total steps, and optional message
server.progress(
i,
totalSteps,
Processing step ${i}/${totalSteps},
);
// Simulate work
await processStep(i);
}
return {
content: [{ type: 'text', text: 'Processing complete!' }],
};
},
);
`
Parameters:
- progress (number): Current progress value (should be β€ total and always increase)total
- (number, optional): Maximum progress value (defaults to 1)message
- (string, optional): Descriptive message about current progress
Notes:
- Progress notifications are only sent when the client provides a progressToken in the request
- Progress values should be monotonically increasing within a single operation
- Each session maintains its own progress context
##### log(level, data, logger?)
Send log messages to connected clients when logging is enabled.
`javascript`
// Log at different severity levels
server.log('info', 'Server started successfully');
server.log('warning', 'Configuration missing, using defaults');
server.log('error', 'Failed to connect to database', 'database-logger');
Parameters:
- level (string): Log level ('debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency')data
- (any): Data to loglogger
- (string, optional): Logger name/category
##### on(event, callback)
Listen to server events.
Available events:
- initialize β fired after the client handshake with the parsed initialize payloadsend
- β emitted for point-to-point responses/notifications destined for the active sessionbroadcast
- β emitted for fan-out notifications (for example notifications/resources/updated)subscription
- β raised when the client subscribes to a resource URIloglevelchange
- β fired when the client requests a different log level
`javascript
server.on('initialize', ({ clientInfo }) => {
console.log('Client initialized:', clientInfo?.name);
});
server.on('send', ({ request }) => {
console.log('Sending message:', request);
});
server.on('broadcast', ({ request }) => {
console.log('Broadcasting:', request.method);
});
`
Provide real-time progress updates for long-running operations:
`javascript
server.tool(
{
name: 'analyze-codebase',
description: 'Analyze a large codebase with progress tracking',
schema: z.object({
path: z.string(),
includeTests: z.boolean().default(false),
}),
},
async ({ path, includeTests }) => {
// Discover files to analyze
const files = await discoverFiles(path, includeTests);
const totalFiles = files.length;
server.progress(0, totalFiles, 'Starting analysis...');
const results = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
// Update progress with current file being processed
server.progress(
i + 1,
totalFiles,
Analyzing ${file} (${i + 1}/${totalFiles}),
);
// Analyze the file
const analysis = await analyzeFile(file);
results.push(analysis);
}
// Final progress update
server.progress(totalFiles, totalFiles, 'Analysis complete!');
return {
content: [
{
type: 'text',
text: Analyzed ${totalFiles} files. Found ${results.length} issues.,
},
],
structuredContent: {
totalFiles,
results,
issues: results.length,
},
};
},
);
// Progress also works in prompts and resources
server.prompt(
{
name: 'generate-report',
description: 'Generate a comprehensive report',
schema: z.object({
sections: z.array(z.string()),
}),
},
async ({ sections }) => {
const messages = [];
for (let i = 0; i < sections.length; i++) {
server.progress(
i,
sections.length,
Generating section: ${sections[i]},
);
const content = await generateSection(sections[i]);
messages.push({
role: 'user',
content: { type: 'text', text: content },
});
}
server.progress(sections.length, sections.length, 'Report complete');
return { messages };
},
);
`
`javascript
// Elicitation - Request structured data from client
const userData = await server.elicitation(
z.object({
name: z.string(),
age: z.number(),
preferences: z.array(z.string()),
}),
);
// Message sampling - Request AI responses
const aiResponse = await server.message({
messages: [
{
role: 'user',
content: { type: 'text', text: 'Explain quantum computing' },
},
],
maxTokens: 100,
});
// Roots management - Access client's filesystem roots
await server.refreshRoots();
console.log('Available roots:', server.roots);
`
`javascript
// Listen to server events
server.on('initialize', (data) => {
console.log('Client capabilities:', data.capabilities);
});
server.on('send', ({ request }) => {
console.log('Outgoing request:', request.method);
});
server.on('broadcast', ({ request }) => {
if (request.method === 'notifications/resources/updated') {
console.log('Resource updated:', request.params.uri);
}
});
`
`javascript
// Subscribe to resource changes
server.resource(
{
name: 'file-watcher',
description: 'Watch file changes',
uri: 'file://watched/file.txt',
},
async (uri) => {
return {
contents: [
{
uri,
mimeType: 'text/plain',
text: await readFile(uri),
},
],
};
},
);
// Notify subscribers when resource changes
server.changed('resource', 'file://watched/file.txt');
`
The completion API allows you to provide auto-completion suggestions for prompt and template parameters. Completion functions return an object with completion containing values, total, and hasMore properties.
#### Completion Response Format
`javascript`
{
completion: {
values: string[], // Array of completion values (max 100 items)
total?: number, // Total number of available completions
hasMore?: boolean // Whether there are more completions available
}
}
#### Prompt Parameter Completion
`javascript
server.prompt(
{
name: 'story-generator',
description: 'Generate a story with specific parameters',
schema: z.object({
genre: z.string(),
length: z.enum(['short', 'medium', 'long']),
character: z.string(),
}),
complete: {
genre: (arg, context) => {
const genres = [
'fantasy',
'sci-fi',
'mystery',
'romance',
'thriller',
];
const filtered = genres.filter((g) =>
g.startsWith(arg.toLowerCase()),
);
return {
completion: {
values: filtered,
total: filtered.length,
hasMore: false,
},
};
},
length: (arg, context) => {
const lengths = ['short', 'medium', 'long'];
const filtered = lengths.filter((l) => l.includes(arg));
return {
completion: {
values: filtered,
total: filtered.length,
hasMore: false,
},
};
},
character: (arg, context) => {
// Dynamic completion based on genre
const characters = {
fantasy: ['wizard', 'dragon', 'knight', 'elf'],
'sci-fi': ['robot', 'alien', 'cyborg', 'space-explorer'],
mystery: ['detective', 'suspect', 'witness', 'victim'],
default: ['hero', 'villain', 'sidekick', 'mentor'],
};
const genre = context.params?.genre || 'default';
const charList = characters[genre] || characters.default;
const filtered = charList.filter((c) => c.includes(arg));
return {
completion: {
values: filtered,
total: filtered.length,
hasMore: false,
},
};
},
},
},
async (input) => {
return {
description: A ${input.length} ${input.genre} story featuring a ${input.character},Write a ${input.length} ${input.genre} story featuring a ${input.character}.
messages: [
{
role: 'user',
content: {
type: 'text',
text: ,`
},
},
],
};
},
);
#### Resource Template Completion
`javascript
server.template(
{
name: 'user-profile',
description: 'Get user profile by ID',
uri: 'users/{userId}/profile',
complete: {
userId: (arg, context) => {
// Filter users based on the current input
const allUsers = ['user1', 'user2', 'user3', 'admin-user'];
const filtered = allUsers.filter((id) => id.includes(arg));
return {
completion: {
values: filtered.slice(0, 10), // Limit to 10 results
total: filtered.length,
hasMore: filtered.length > 10,
},
};
},
},
},
async (uri, params) => {
const user = await getUserById(params.userId);
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(user),
},
],
};
},
);
`
#### Advanced Completion Examples
`javascript
// Completion with async data fetching
server.template(
{
name: 'project-files',
description: 'Access project files by path',
uri: 'projects/{projectId}/files/{filePath}',
complete: {
projectId: (arg, context) => {
// Static list of project IDs
const projects = ['web-app', 'mobile-app', 'api-server'];
const filtered = projects.filter((p) => p.includes(arg));
return {
completion: {
values: filtered,
total: filtered.length,
hasMore: false,
},
};
},
filePath: async (arg, context) => {
// Dynamic file path completion based on project
const projectId = context.params?.projectId;
if (!projectId) {
return {
completion: {
values: [],
total: 0,
hasMore: false,
},
};
}
// Simulate fetching files from filesystem
const files = await getProjectFiles(projectId);
const filtered = files.filter((f) => f.includes(arg));
return {
completion: {
values: filtered.slice(0, 20),
total: filtered.length,
hasMore: filtered.length > 20,
},
};
},
},
},
async (uri, params) => {
const content = await readProjectFile(
params.projectId,
params.filePath,
);
return {
contents: [
{
uri,
mimeType: 'text/plain',
text: content,
},
],
};
},
);
// Completion with pagination support
server.prompt(
{
name: 'search-documents',
description: 'Search through large document collection',
schema: z.object({
query: z.string(),
category: z.string(),
}),
complete: {
category: (arg, context) => {
// Large category list with pagination
const allCategories = generateCategoryList(); // Assume this returns 500+ items
const filtered = allCategories.filter((c) =>
c.toLowerCase().includes(arg.toLowerCase()),
);
return {
completion: {
values: filtered.slice(0, 50), // Show first 50 matches
total: filtered.length,
hasMore: filtered.length > 50,
},
};
},
},
},
async (input) => {
const results = await searchDocuments(input.query, input.category);
return {
description: Search results for "${input.query}" in ${input.category},Found ${results.length} documents matching "${input.query}"
messages: [
{
role: 'user',
content: {
type: 'text',
text: ,`
},
},
],
};
},
);
`javascript
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;
const result = await createUser(user, preferences, tags);
return {
content: [
{
type: 'text',
text: User created: ${user.name} (${user.email}),`
},
],
};
},
);
All MCP capabilities (tools, prompts, resources, templates) support dynamic enabling and disabling through the enabled function. This allows you to conditionally show or hide capabilities based on runtime conditions, user permissions, or any other logic.
The enabled function is called before each list operation and determines whether the capability should be included in the response:
`javascript`
server.tool(
{
name: 'admin-only-tool',
description: 'Administrative tool',
enabled: () => {
// Only show this tool if user is admin
return getCurrentUser().isAdmin;
},
},
async (input) => {
return { content: [{ type: 'text', text: 'Admin action completed' }] };
},
);
The enabled function can be synchronous or asynchronous:
`javascript`
server.resource(
{
name: 'private-document',
description: 'Access private documents',
uri: 'private://document.txt',
enabled: async () => {
// Check permissions asynchronously
const user = await getCurrentUser();
return await hasPermission(user.id, 'read-private-docs');
},
},
async (uri) => {
return {
contents: [
{
uri,
mimeType: 'text/plain',
text: await readPrivateDocument(uri),
},
],
};
},
);
Within the enabled function you can read the session context with server.ctx, allowing for user-specific or session-specific logic:
`javascript`
server.prompt(
{
name: 'personalized-prompt',
description: 'User-specific prompt template',
enabled: () => {
// Access session information
const sessionId = server.ctx.sessionId;
const userPrefs = getUserPreferences(sessionId);
return userPrefs.enablePersonalization;
},
},
async (input) => {
return {
messages: [
{
role: 'user',
content: { type: 'text', text: 'Personalized content...' },
},
],
};
},
);
or enable something based on client capabilities or info
`javascript`
server.prompt(
{
name: 'personalized-prompt',
description: 'Claude Code prompt',
enabled: () => {
// Access session information
const clientInfo = server.ctx.sessionInfo?.clientInfo;
return clientInfo?.name === 'Claude Code';
},
},
async (input) => {
return {
messages: [
{
role: 'user',
content: { type: 'text', text: 'Personalized content...' },
},
],
};
},
);
or
`javascriptYour repositories are ${repos.join(', ')}
server.prompt(
{
name: 'fetch-repositories',
description: 'fetch the repositories of the user',
enabled: () => {
// Access session information
const clientCapabilities =
server.ctx.sessionInfo?.clientCapabilities;
return clientCapabilities?.elicitation != null;
},
},
async (input) => {
const username = await server.elicitation(
v.object({
value: v.string(),
}),
);
const repos = await fetchRepos(username.value);
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: ,`
},
},
],
};
},
);
URI templates also support the enabled function:
`javascript`
server.template(
{
name: 'user-files',
description: 'Access user-specific files',
uri: 'users/{userId}/files/{filename}',
enabled: async () => {
// Check if file system is available
return await isFileSystemMounted();
},
complete: {
userId: (arg, context) => ({
completion: {
values: ['user1', 'user2', 'user3'],
total: 3,
hasMore: false,
},
}),
},
},
async (uri, params) => {
const content = await readUserFile(params.userId, params.filename);
return {
contents: [
{
uri,
mimeType: 'text/plain',
text: content,
},
],
};
},
);
If an enabled function throws an error, the capability will be treated as disabled:
`javascript`
server.tool(
{
name: 'network-tool',
description: 'Tool requiring network access',
enabled: () => {
// If network check fails, tool is disabled
if (!checkNetworkConnection()) {
throw new Error('Network unavailable');
}
return true;
},
},
async (input) => {
return {
content: [{ type: 'text', text: 'Network operation completed' }],
};
},
);
- The enabled function is called every time a list is requested
- Keep enabled functions lightweight to avoid performance issues
- Consider caching expensive checks when possible
- Async functions add latency to list operations
Common scenarios where enabled functions are useful:
1. Permission-based access: Show tools only to authorized users
2. Feature flags: Enable/disable features based on configuration
3. Resource availability: Hide resources when underlying systems are unavailable
4. User preferences: Customize available capabilities per user
5. Time-based access: Enable tools only during specific hours
6. License restrictions: Limit features based on subscription level
Sometimes you need properties that are computed dynamically at list-time rather than registration-time. For example, you might want to serve different descriptions based on which client is connected.
tmcp preserves JavaScript getters on the configuration object, allowing you to define properties that are evaluated each time the capability is listed:
`javascript`
server.tool(
{
name: 'search',
get description() {
const client = server.ctx.sessionInfo?.clientInfo?.name;
if (client === 'claude-code') {
return 'Search the codebase for files, symbols, or text patterns';
}
return 'Search for information';
},
},
() => {
return { content: [{ type: 'text', text: 'Search results...' }] };
},
);
The same pattern works for resources, templates, and prompts.
Contributions are welcome! Please see our contributing guidelines for details.
Huge thanks to Sean O'Bannon that provided us with the @tmcp` scope on npm.
MIT Β© Paolo Ricciuti