tmcp
Version:
The main tmcp library
1,524 lines (1,292 loc) โข 36.3 kB
Markdown
> [!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
```bash
pnpm install tmcp
# Choose your preferred schema library adapter
pnpm install @tmcp/adapter-zod zod
```
## Quick Start
```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}` }],
};
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.
```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)
2. if you pass even one `tool.error` 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,
);
```
### 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
```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.
## API Reference
### McpServer
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<T>()`
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<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.
```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 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.
```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 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
```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);
});
```
## Advanced Examples
### Progress Reporting
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 };
},
);
```
### Client Interaction Features
```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);
```
### Event Handling
```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);
}
});
```
### Resource Subscriptions
```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');
```
### 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
```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}`,
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Write a ${input.length} ${input.genre} story featuring a ${input.character}.`,
},
},
],
};
},
);
```
#### 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}`,
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Found ${results.length} documents matching "${input.query}"`,
},
},
],
};
},
);
```
### Complex Validation
```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})`,
},
],
};
},
);
```
## 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:
```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' }] };
},
);
```
### Async Enabled Functions
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),
},
],
};
},
);
```
### Context-Aware Enabling
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
```javascript
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:
```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,
},
],
};
},
);
```
### Error Handling
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' }],
};
},
);
```
### 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:
```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.
## Contributing
Contributions are welcome! Please see our [contributing guidelines](../../CONTRIBUTING.md) for details.
## Acknowledgments
Huge thanks to Sean O'Bannon that provided us with the `@tmcp` scope on npm.
## License
MIT ยฉ Paolo Ricciuti