npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tmcp

v1.19.3

Published

The main tmcp library

Readme

[!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

Quick Start

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}` }],
				};
			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: `${a} ÷ ${b} = ${a / b}` }],
				};
		}
	},
);

// Process incoming requests
server.receive(request);

Return helpers to skip boilerplate

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.

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

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)
  2. if you pass even one tool.error to the tool.mix the whole return value will be an error
const structuredContent = {
	cool: true,
};

tool.mix(
	[
		tool.text(JSON.stringify(structuredContent)),
		tool.media('image', png, 'image/png'),
	],
	structuredContent,
);

Helper catalog

  • 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.

Defining Tools, Prompts, Resources, and Templates in Separate Files

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

Example Structure

// 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}` }],
	}),
);
// 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}`,
				},
			},
		],
	}),
);
// 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'),
			},
		],
	}),
);
// 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),
			},
		],
	}),
);
// 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.

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) / tools(definitions)

Register one or more tools with optional schema validation.

// 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.

// 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.

// 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.

// 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<T>()

Specify the type of custom context for type-safe access to application-specific data.

interface MyCustomContext {
    userId: string;
    permissions: string[];
    database: DatabaseConnection;
}

const server = new McpServer(serverInfo, options).withContext<MyCustomContext>();

// 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.

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.

// 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.

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 name
  • params (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.

const result = await server.elicitation(schema);
message(request)

Request language model sampling from the client.

const result = await server.message({
	messages: [
		{
			role: 'user',
			content: { type: 'text', text: 'Hello!' },
		},
	],
});
refreshRoots()

Refresh the roots list from the client.

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.

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.

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.

// 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 log
  • logger (string, optional): Logger name/category
on(event, callback)

Listen to server events.

Available events:

  • initialize – fired after the client handshake with the parsed initialize payload
  • send – emitted for point-to-point responses/notifications destined for the active session
  • broadcast – emitted for fan-out notifications (for example notifications/resources/updated)
  • subscription – raised when the client subscribes to a resource URI
  • loglevelchange – fired when the client requests a different log level
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);
});

Advanced Examples

Progress Reporting

Provide real-time progress updates for long-running operations:

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 };
	},
);

Client Interaction Features

// 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);

Event Handling

// 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);
	}
});

Resource Subscriptions

// 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');

Completion API

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

{
  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

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}`,
			messages: [
				{
					role: 'user',
					content: {
						type: 'text',
						text: `Write a ${input.length} ${input.genre} story featuring a ${input.character}.`,
					},
				},
			],
		};
	},
);

Resource Template Completion

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

// 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}`,
			messages: [
				{
					role: 'user',
					content: {
						type: 'text',
						text: `Found ${results.length} documents matching "${input.query}"`,
					},
				},
			],
		};
	},
);

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;
		const result = await createUser(user, preferences, tags);
		return {
			content: [
				{
					type: 'text',
					text: `User created: ${user.name} (${user.email})`,
				},
			],
		};
	},
);

Dynamic Enabling/Disabling

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.

Basic Usage

The enabled function is called before each list operation and determines whether the capability should be included in the response:

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' }] };
	},
);

Async Enabled Functions

The enabled function can be synchronous or asynchronous:

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),
				},
			],
		};
	},
);

Context-Aware Enabling

Within the enabled function you can read the session context with server.ctx, allowing for user-specific or session-specific logic:

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

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

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: `Your repositories are ${repos.join(', ')}`,
					},
				},
			],
		};
	},
);

Template Enabling

URI templates also support the enabled function:

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,
				},
			],
		};
	},
);

Error Handling

If an enabled function throws an error, the capability will be treated as disabled:

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' }],
		};
	},
);

Performance Considerations

  • 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

Use Cases

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

Dynamic Properties with Getters

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:

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.

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