MCP

Expose AI tools with Model Context Protocol

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

Quick Start

Add the MCP Plugin

Enable MCP in your application:

src/setup.ts
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:

src/tools/GreetTool.ts
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:

src/setup.ts
import { Container } from '@vercube/di';
import { GreetTool } from './tools/GreetTool';

export function useContainer(container: Container) {
  container.bind(GreetTool);
}
That's it! Your tool is now available to MCP clients at 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:

  1. Tool Registration - @MCPTool decorator registers your tool in the ToolRegistry
  2. Schema Validation - Input arguments are validated against your Zod schema
  3. Execution - Your execute method runs with validated data
  4. Response Formatting - Output is automatically formatted for MCP protocol
  5. 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')
});
Always add .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:

@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
    };
  }
}

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'
Previous

Custom Decorator

Learn how to create and use custom decorators in Vercube to extend functionality and create reusable patterns

Next