MCP
The MCP (Model Context Protocol) module lets you expose your application's functionality as AI tools that can be used by LLM-powered agents, AI assistants, and other MCP clients. Instead of building custom integrations for every AI platform, you implement the standardized MCP protocol once and it works everywhere.
Installation
$ pnpm add @vercube/mcp
$ npm install @vercube/mcp
$ bun install @vercube/mcp
Quick Start
Add the MCP Plugin
Enable MCP in your application:
import { createApp } from '@vercube/core';
import { MCPPlugin } from '@vercube/mcp';
const app = createApp({
setup: async (app) => {
app.addPlugin(MCPPlugin);
}
});
This automatically registers an MCP endpoint at /api/mcp that handles the protocol communication.
Create Your First Tool
Create a tool by extending the Tool base class:
import { MCPTool, Tool } from '@vercube/mcp';
import { z } from 'zod';
const inputSchema = z.object({
name: z.string().describe('The name of the person to greet')
});
const outputSchema = z.object({
greeting: z.string().describe('The greeting message')
});
export class GreetTool extends Tool<
z.infer<typeof inputSchema>,
z.infer<typeof outputSchema>
> {
@MCPTool({
name: 'greet',
description: 'Greets a user with a personalized message',
inputSchema,
outputSchema
})
async execute(args: z.infer<typeof inputSchema>) {
return {
greeting: `Hello, ${args.name}!`
};
}
}
Register Your Tool
Bind your tool to the DI container:
import { Container } from '@vercube/di';
import { GreetTool } from './tools/GreetTool';
export function useContainer(container: Container) {
container.bind(GreetTool);
}
http://localhost:3000/api/mcp.What is MCP?
Model Context Protocol (MCP) is an open standard that defines how AI applications can discover and use tools from external services. Think of it like a USB port for AI - one standard interface that works with any compatible device.
The Problem Without MCP module
// ❌ Building custom integrations for every AI platform
app.post('/openai-function', handleOpenAIFormat);
app.post('/anthropic-tool', handleAnthropicFormat);
app.post('/google-action', handleGoogleFormat);
// ... different endpoint for each platform
The Solution With MCP module
// ✅ One standard, works everywhere
@MCPTool({
name: 'getWeather',
description: 'Get current weather',
inputSchema: weatherSchema
})
async execute(args: { location: string }) {
return await this.weatherService.get(args.location);
}
Now any MCP client (Claude Desktop, AI agents, custom apps) can discover and use your tool automatically.
How MCP Tools Work
When you create an MCP tool, Vercube handles all the protocol details:
- Tool Registration -
@MCPTooldecorator registers your tool in the ToolRegistry - Schema Validation - Input arguments are validated against your Zod schema
- Execution - Your
executemethod runs with validated data - Response Formatting - Output is automatically formatted for MCP protocol
- Error Handling - Errors are caught and returned as MCP error responses
// Client calls tool
{
"method": "tools/call",
"params": {
"name": "greet",
"arguments": { "name": "Alice" }
}
}
// Your execute() runs
async execute(args) {
return { greeting: `Hello, ${args.name}!` };
}
// MCP formats response
{
"content": [{
"type": "text",
"text": "{\"greeting\":\"Hello, Alice!\"}"
}]
}
Creating Tools
Basic Tool Structure
Every tool follows this pattern:
import { MCPTool, Tool } from '@vercube/mcp';
import { z } from 'zod';
// 1. Define input schema
const inputSchema = z.object({
// Your parameters here
});
// 2. Define output schema
const outputSchema = z.object({
// Your return type here
});
// 3. Create tool class
export class MyTool extends Tool<
z.infer<typeof inputSchema>,
z.infer<typeof outputSchema>
> {
// 4. Decorate execute method
@MCPTool({
name: 'myTool',
description: 'What this tool does',
inputSchema,
outputSchema
})
async execute(args: z.infer<typeof inputSchema>) {
// 5. Implement your logic
return { /* your result */ };
}
}
Input Schema
Define parameters using Zod schemas with descriptions:
const inputSchema = z.object({
query: z.string()
.min(1)
.max(100)
.describe('The search query'),
limit: z.number()
.int()
.positive()
.max(50)
.optional()
.default(10)
.describe('Maximum number of results'),
filters: z.object({
category: z.string().optional(),
tags: z.array(z.string()).optional()
}).optional().describe('Optional filters')
});
.describe() to your schema fields! These descriptions help AI models understand how to use your tool correctly.Output Schema
Define your return type for documentation and type safety:
const outputSchema = z.object({
results: z.array(z.object({
id: z.string().describe('Result ID'),
title: z.string().describe('Result title'),
score: z.number().describe('Relevance score')
})).describe('Search results'),
total: z.number().describe('Total number of results'),
hasMore: z.boolean().describe('Whether more results are available')
});
Tool with Dependencies
Inject services into your tools using dependency injection:
import { Inject } from '@vercube/di';
import { WeatherService } from '../services/WeatherService';
export class WeatherTool extends Tool<WeatherInput, WeatherOutput> {
@Inject(WeatherService)
private weatherService!: WeatherService;
@MCPTool({
name: 'getCurrentWeather',
description: 'Get current weather for a location',
inputSchema,
outputSchema
})
async execute(args: WeatherInput) {
// Use injected service
const weather = await this.weatherService.getCurrent(args.location);
return {
temperature: weather.temp,
conditions: weather.description,
humidity: weather.humidity
};
}
}
Accessing MCP Request Context
Tools receive an optional extra parameter with MCP request context:
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
import type { ServerRequest, ServerNotification } from '@modelcontextprotocol/sdk/types.js';
@MCPTool({ /* ... */ })
async execute(
args: MyArgs,
extra?: RequestHandlerExtra<ServerRequest, ServerNotification>
) {
// Access request metadata
console.log('Request ID:', extra?.request?.id);
// Send progress notifications
if (extra) {
await extra.sendNotification({
method: 'progress',
params: { progress: 50 }
});
}
return { /* result */ };
}
Return Value Handling
Vercube automatically formats your return values for the MCP protocol:
Simple Object (Recommended)
@MCPTool({ /* ... */ })
async execute(args) {
return {
temperature: 72,
conditions: 'Sunny',
humidity: 45
};
}
// Automatically becomes:
// { "content": [{ "type": "text", "text": "{\"temperature\":72,...}" }] }
String Response
@MCPTool({ /* ... */ })
async execute(args) {
return 'Operation completed successfully';
}
// Automatically becomes:
// { "content": [{ "type": "text", "text": "Operation completed successfully" }] }
MCP-Formatted Response
@MCPTool({ /* ... */ })
async execute(args) {
return {
content: [{
type: 'text' as const,
text: 'Custom formatted response'
}]
};
}
// Passed through unchanged
Error Handling
Errors are automatically caught and formatted as MCP error responses:
@MCPTool({ /* ... */ })
async execute(args) {
if (!args.id) {
throw new Error('ID is required');
}
const user = await this.db.findUser(args.id);
if (!user) {
throw new Error('User not found');
}
return user;
}
// Errors become:
// {
// "content": [{ "type": "text", "text": "Error: User not found" }],
// "isError": true
// }
Testing Your Tools
Test the MCP endpoint directly:
# List all available tools
curl -X POST http://localhost:3000/api/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "tools/list",
"id": 1
}'
# Call a tool
curl -X POST http://localhost:3000/api/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "greet",
"arguments": { "name": "Alice" }
},
"id": 2
}'
Tool Examples
import { MCPTool, Tool } from '@vercube/mcp';
import { z } from 'zod';
const inputSchema = z.object({
operation: z.enum(['add', 'subtract', 'multiply', 'divide'])
.describe('Mathematical operation to perform'),
a: z.number().describe('First number'),
b: z.number().describe('Second number')
});
const outputSchema = z.object({
result: z.number().describe('Calculation result'),
operation: z.string().describe('Operation performed')
});
export class CalculatorTool extends Tool<
z.infer<typeof inputSchema>,
z.infer<typeof outputSchema>
> {
@MCPTool({
name: 'calculate',
description: 'Performs basic mathematical operations',
inputSchema,
outputSchema,
annotations: {
readOnlyHint: true,
idempotentHint: true
}
})
async execute(args: z.infer<typeof inputSchema>) {
let result: number;
switch (args.operation) {
case 'add':
result = args.a + args.b;
break;
case 'subtract':
result = args.a - args.b;
break;
case 'multiply':
result = args.a * args.b;
break;
case 'divide':
if (args.b === 0) {
throw new Error('Cannot divide by zero');
}
result = args.a / args.b;
break;
}
return {
result,
operation: args.operation
};
}
}
import { MCPTool, Tool } from '@vercube/mcp';
import { Inject } from '@vercube/di';
import { z } from 'zod';
import { DatabaseService } from '../services/DatabaseService';
const inputSchema = z.object({
table: z.string().describe('Table name to query'),
filters: z.record(z.string(), z.any()).optional()
.describe('Filter conditions'),
limit: z.number().int().min(1).max(100).default(10)
.describe('Maximum results to return')
});
const outputSchema = z.object({
data: z.array(z.record(z.string(), z.any()))
.describe('Query results'),
count: z.number().describe('Number of results returned')
});
export class QueryTool extends Tool<
z.infer<typeof inputSchema>,
z.infer<typeof outputSchema>
> {
@Inject(DatabaseService)
private db!: DatabaseService;
@MCPTool({
name: 'queryDatabase',
description: 'Query database tables with optional filters',
inputSchema,
outputSchema,
annotations: {
readOnlyHint: true
}
})
async execute(args: z.infer<typeof inputSchema>) {
const results = await this.db.query(
args.table,
args.filters,
args.limit
);
return {
data: results,
count: results.length
};
}
}
Troubleshooting
Tool not appearing in tools/list
Make sure your tool is bound to the container:
container.bind(MyTool); // Don't forget this!
Schema validation errors
Check that your Zod schema matches your actual data:
// Input must match schema exactly
const inputSchema = z.object({
name: z.string() // Required
});
// This will fail validation
execute({ name: 123 }) // number instead of string
TypeScript errors with z.infer
Make sure you're using the correct Zod version and import:
import { z } from 'zod'; // Not 'zod/v3' or 'zod/v4'