A Fastify plugin that implements the Model Context Protocol (MCP) server using JSON-RPC 2.0. This plugin enables Fastify applications to expose tools, resources, and prompts following the MCP specification.
npm install @platformatic/mcp
For type-safe schema validation, install TypeBox:
npm install @sinclair/typebox
- Complete MCP Protocol Support: Implements the full Model Context Protocol specification
- TypeBox Validation: Type-safe schema validation with automatic TypeScript inference
- Multiple Transport Support: HTTP/SSE and stdio transports for flexible communication
- SSE Streaming: Server-Sent Events for real-time communication
- Horizontal Scaling: Redis-backed session management and message broadcasting
- Session Persistence: Message history and reconnection support with Last-Event-ID
- Memory & Redis Backends: Seamless switching between local and distributed storage
- Production Ready: Comprehensive test coverage and authentication support
import Fastify from 'fastify'
import mcpPlugin from '@platformatic/mcp'
// Or use named import:
// import { mcpPlugin } from '@platformatic/mcp'
const app = Fastify({ logger: true })
// Register the MCP plugin
await app.register(mcpPlugin, {
serverInfo: {
name: 'my-mcp-server',
version: '1.0.0'
},
capabilities: {
tools: { listChanged: true },
resources: { subscribe: true },
prompts: {}
},
instructions: 'This server provides custom tools and resources'
})
// Add tools, resources, and prompts with handlers
app.mcpAddTool({
name: 'calculator',
description: 'Performs basic arithmetic operations',
inputSchema: {
type: 'object',
properties: {
operation: { type: 'string', enum: ['add', 'subtract', 'multiply', 'divide'] },
a: { type: 'number' },
b: { type: 'number' }
},
required: ['operation', 'a', 'b']
}
}, async (params) => {
const { operation, a, b } = params
let result
switch (operation) {
case 'add': result = a + b; break
case 'subtract': result = a - b; break
case 'multiply': result = a * b; break
case 'divide': result = a / b; break
default: throw new Error('Invalid operation')
}
return {
content: [{ type: 'text', text: `Result: ${result}` }]
}
})
app.mcpAddResource({
uri: 'file://config.json',
name: 'Application Config',
description: 'Server configuration file',
mimeType: 'application/json'
}, async (uri) => {
// Read and return the configuration file
const config = { setting1: 'value1', setting2: 'value2' }
return {
contents: [{
uri,
text: JSON.stringify(config, null, 2),
mimeType: 'application/json'
}]
}
})
app.mcpAddPrompt({
name: 'code-review',
description: 'Generates code review comments',
arguments: [{
name: 'language',
description: 'Programming language',
required: true
}]
}, async (name, args) => {
const language = args?.language || 'javascript'
return {
messages: [{
role: 'user',
content: {
type: 'text',
text: `Please review this ${language} code for best practices, potential bugs, and improvements.`
}
}]
}
})
await app.listen({ port: 3000 })
The plugin supports TypeBox schemas for type-safe validation with automatic TypeScript inference. This eliminates the need for manual type definitions and provides compile-time type checking.
- Type Safety: Automatic TypeScript type inference from schemas
- Runtime Validation: Input validation with structured error messages
- Zero Duplication: Single source of truth for both types and validation
- IDE Support: Full autocomplete and IntelliSense for validated parameters
- Performance: Compiled validators with caching for optimal performance
import { Type } from '@sinclair/typebox'
import Fastify from 'fastify'
import mcpPlugin from '@platformatic/mcp'
const app = Fastify({ logger: true })
await app.register(mcpPlugin, {
serverInfo: { name: 'my-server', version: '1.0.0' },
capabilities: { tools: {} }
})
// Define TypeBox schema
const SearchToolSchema = Type.Object({
query: Type.String({ minLength: 1, description: 'Search query' }),
limit: Type.Optional(Type.Number({ minimum: 1, maximum: 100, description: 'Maximum results' })),
filters: Type.Optional(Type.Array(Type.String(), { description: 'Filter criteria' }))
})
// Register tool with TypeBox schema
app.mcpAddTool({
name: 'search',
description: 'Search for files',
inputSchema: SearchToolSchema
}, async (params) => {
// params is automatically typed as:
// {
// query: string;
// limit?: number;
// filters?: string[];
// }
const { query, limit = 10, filters = [] } = params
return {
content: [{
type: 'text',
text: `Searching for "${query}" with limit ${limit} and filters: ${filters.join(', ')}`
}]
}
})
// Complex nested schema
const ComplexToolSchema = Type.Object({
user: Type.Object({
name: Type.String(),
age: Type.Number({ minimum: 0 })
}),
preferences: Type.Object({
theme: Type.Union([
Type.Literal('light'),
Type.Literal('dark'),
Type.Literal('auto')
]),
notifications: Type.Boolean()
}),
tags: Type.Array(Type.String())
})
app.mcpAddTool({
name: 'update-profile',
description: 'Update user profile',
inputSchema: ComplexToolSchema
}, async (params) => {
// Fully typed nested object
const { user, preferences, tags } = params
return { content: [{ type: 'text', text: `Updated profile for ${user.name}` }] }
})
// URI validation schema
const FileUriSchema = Type.String({
pattern: '^file://.+',
description: 'File URI pattern'
})
app.mcpAddResource({
uriPattern: 'file://documents/*',
name: 'Document Files',
description: 'Access document files',
uriSchema: FileUriSchema
}, async (uri) => {
// uri is validated against the schema
const content = await readFile(uri)
return {
contents: [{ uri, text: content, mimeType: 'text/plain' }]
}
})
// Prompt with automatic argument generation
const CodeReviewSchema = Type.Object({
language: Type.Union([
Type.Literal('javascript'),
Type.Literal('typescript'),
Type.Literal('python')
], { description: 'Programming language' }),
complexity: Type.Optional(Type.Union([
Type.Literal('low'),
Type.Literal('medium'),
Type.Literal('high')
], { description: 'Code complexity level' }))
})
app.mcpAddPrompt({
name: 'code-review',
description: 'Generate code review',
argumentSchema: CodeReviewSchema
// arguments array is automatically generated from schema
}, async (name, args) => {
// args is typed as: { language: 'javascript' | 'typescript' | 'python', complexity?: 'low' | 'medium' | 'high' }
return {
messages: [{
role: 'user',
content: {
type: 'text',
text: `Review this ${args.language} code with ${args.complexity || 'medium'} complexity`
}
}]
}
})
TypeBox validation provides structured error messages:
// When validation fails, structured errors are returned:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"isError": true,
"content": [{
"type": "text",
"text": "Invalid tool arguments: Validation failed with 2 errors:\n/query: Expected string, received number\n/limit: Expected number <= 100, received 150"
}]
}
}
The plugin maintains backward compatibility with JSON Schema and unvalidated tools:
// JSON Schema (still supported)
app.mcpAddTool({
name: 'legacy-tool',
description: 'Uses JSON Schema',
inputSchema: {
type: 'object',
properties: {
param: { type: 'string' }
}
}
}, async (params) => {
// params is typed as 'any'
return { content: [{ type: 'text', text: 'OK' }] }
})
// Unvalidated tool (unsafe)
app.mcpAddTool({
name: 'unsafe-tool',
description: 'No validation'
}, async (params) => {
// params is typed as 'any' - no validation performed
return { content: [{ type: 'text', text: 'OK' }] }
})
TypeBox validation is highly optimized:
- Compiled Validators: Schemas are compiled to optimized validation functions
- Caching: Compiled validators are cached for reuse
- Minimal Overhead: Less than 1ms validation overhead for typical schemas
- Memory Efficient: Shared validator instances across requests
This plugin supports the MCP Streamable HTTP transport specification, enabling both regular JSON responses and Server-Sent Events for streaming communication.
await app.register(mcpPlugin, {
enableSSE: true, // Enable SSE support (default: false)
// ... other options
})
The plugin supports Redis-backed session management and message broadcasting for horizontal scaling across multiple server instances.
Without Redis (Memory-only):
- Each server instance maintains isolated session stores
- SSE connections are tied to specific server instances
- No cross-instance message broadcasting
- Session data is lost when servers restart
- Load balancers can't route clients to different instances
With Redis (Distributed):
- Shared Session State: All instances access the same session data from Redis
- Cross-Instance Broadcasting: Messages sent from any instance reach all connected clients
- Session Persistence: Sessions survive server restarts with 1-hour TTL
- High Availability: Clients can reconnect to any instance and resume from last event
- True Horizontal Scaling: Add more instances without architectural changes
This transforms the plugin from a single-instance application into a distributed system capable of serving thousands of concurrent SSE connections with real-time global synchronization.
import Fastify from 'fastify'
import mcpPlugin from '@platformatic/mcp'
const app = Fastify({ logger: true })
await app.register(mcpPlugin, {
enableSSE: true,
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
db: parseInt(process.env.REDIS_DB || '0'),
password: process.env.REDIS_PASSWORD,
// Additional ioredis options
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3
},
serverInfo: {
name: 'scalable-mcp-server',
version: '1.0.0'
}
})
With Redis configuration, you can run multiple instances of your MCP server:
# Instance 1
PORT=3000 REDIS_HOST=redis.example.com node server.js
# Instance 2
PORT=3001 REDIS_HOST=redis.example.com node server.js
# Instance 3
PORT=3002 REDIS_HOST=redis.example.com node server.js
Automatic Session Management:
- Sessions persist across server restarts
- 1-hour session TTL with automatic cleanup
- Message history stored in Redis Streams
Message Replay:
// Client reconnection with Last-Event-ID
const eventSource = new EventSource('/mcp', {
headers: {
'Accept': 'text/event-stream',
'Last-Event-ID': '1234' // Resume from this event
}
})
Cross-Instance Broadcasting:
// Any server instance can broadcast to all connected clients
app.mcpBroadcastNotification({
jsonrpc: '2.0',
method: 'notifications/message',
params: { message: 'Global update from instance 2' }
})
// Send to specific session (works across instances)
app.mcpSendToSession('session-xyz', {
jsonrpc: '2.0',
method: 'notifications/progress',
params: { progress: 75 }
})
Clients can request SSE streams by including text/event-stream
in the Accept
header:
// Request SSE stream
fetch('/mcp', {
method: 'POST',
headers: {
'Accept': 'text/event-stream',
'Content-Type': 'application/json'
},
body: JSON.stringify({ jsonrpc: '2.0', method: 'ping', id: 1 })
})
// Request regular JSON
fetch('/mcp', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ jsonrpc: '2.0', method: 'ping', id: 1 })
})
The plugin also provides a GET endpoint for server-initiated communication:
// Long-lived SSE stream
const eventSource = new EventSource('/mcp', {
headers: { 'Accept': 'text/event-stream' }
})
The plugin provides methods to send notifications and messages to connected SSE clients:
import Fastify from 'fastify'
import mcpPlugin from '@platformatic/mcp'
const app = Fastify({ logger: true })
await app.register(mcpPlugin, {
enableSSE: true,
// ... other options
})
// Broadcast a notification to all connected SSE clients
app.mcpBroadcastNotification({
jsonrpc: '2.0',
method: 'notifications/message',
params: {
level: 'info',
message: 'Server status update'
}
})
// Send a message to a specific session
const success = app.mcpSendToSession('session-id', {
jsonrpc: '2.0',
method: 'notifications/progress',
params: {
progressToken: 'task-123',
progress: 50,
total: 100
}
})
// Send a request to a specific session (expecting a response)
app.mcpSendToSession('session-id', {
jsonrpc: '2.0',
id: 'req-456',
method: 'sampling/createMessage',
params: {
messages: [
{
role: 'user',
content: { type: 'text', text: 'Hello from server!' }
}
]
}
})
// Example: Broadcast tool list changes
app.mcpBroadcastNotification({
jsonrpc: '2.0',
method: 'notifications/tools/list_changed'
})
// Example: Send resource updates
app.mcpBroadcastNotification({
jsonrpc: '2.0',
method: 'notifications/resources/updated',
params: {
uri: 'file://config.json'
}
})
// Set up a timer to send periodic updates
setInterval(() => {
app.mcpBroadcastNotification({
jsonrpc: '2.0',
method: 'notifications/message',
params: {
level: 'info',
message: `Server time: ${new Date().toISOString()}`
}
})
}, 30000) // Every 30 seconds
// Send updates when data changes
function onDataChange(newData: any) {
app.mcpBroadcastNotification({
jsonrpc: '2.0',
method: 'notifications/resources/list_changed'
})
}
The plugin includes a built-in stdio transport utility for MCP communication over stdin/stdout, following the MCP stdio transport specification. This enables command-line tools and local applications to communicate with your Fastify MCP server.
- Complete MCP stdio transport implementation following the official specification
- Fastify integration using the
.inject()
method for consistency with HTTP routes - Comprehensive error handling with proper JSON-RPC error responses
- Batch request support for processing multiple messages at once
- Debug logging to stderr without interfering with the stdio protocol
import fastify from 'fastify'
import mcpPlugin, { runStdioServer } from '@platformatic/mcp'
const app = fastify({
logger: false // Disable HTTP logging to avoid interference with stdio
})
await app.register(mcpPlugin, {
serverInfo: {
name: 'my-mcp-server',
version: '1.0.0'
},
capabilities: {
tools: {},
resources: {},
prompts: {}
}
})
// Register your tools, resources, and prompts
app.mcpAddTool({
name: 'echo',
description: 'Echo back the input text',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string' }
},
required: ['text']
}
}, async (args) => {
return {
content: [{
type: 'text',
text: `Echo: ${args.text}`
}]
}
})
await app.ready()
// Start the stdio transport
await runStdioServer(app, {
debug: process.env.DEBUG === 'true'
})
# Initialize the server
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}}' | node server.js
# Ping the server
echo '{"jsonrpc":"2.0","id":2,"method":"ping"}' | node server.js
# List available tools
echo '{"jsonrpc":"2.0","id":3,"method":"tools/list"}' | node server.js
# Call a tool
echo '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"echo","arguments":{"text":"Hello, stdio!"}}}' | node server.js
Starts a Fastify MCP server in stdio mode.
Parameters:
app
- Fastify instance with MCP plugin registeredoptions
- Optional stdio transport options
Options:
debug
- Enable debug logging to stderr (default: false)input
- Custom input stream (default: process.stdin)output
- Custom output stream (default: process.stdout)error
- Custom error stream (default: process.stderr)
Creates a stdio transport instance without starting it.
Parameters:
app
- Fastify instance with MCP plugin registeredoptions
- Optional stdio transport options
Returns: StdioTransport
instance with start()
and stop()
methods
The stdio transport follows the MCP stdio transport specification:
- Messages are exchanged over stdin/stdout
- Each message is a single line of JSON
- Messages are delimited by newlines
- Messages must NOT contain embedded newlines
- Server logs can be written to stderr
- Supports both single messages and batch requests
The stdio transport provides comprehensive error handling:
- JSON parsing errors return appropriate JSON-RPC error responses
- Invalid method calls return "Method not found" errors
- Tool execution errors are captured and returned in the response
- Connection errors are logged to stderr
The stdio transport is particularly useful for:
- Command-line tools that need to communicate with MCP servers
- Local development and testing without HTTP overhead
- Integration with text editors and IDEs that support stdio protocols
- Simple client-server communication in controlled environments
- Batch processing of MCP requests from scripts
For production deployments, it's recommended to secure the MCP endpoint using the @fastify/bearer-auth
plugin:
npm install @fastify/bearer-auth
import Fastify from 'fastify'
import mcpPlugin from '@platformatic/mcp'
const app = Fastify({ logger: true })
// Register bearer authentication
await app.register(import('@fastify/bearer-auth'), {
keys: new Set(['your-secret-bearer-token']),
auth: {
// Apply to all routes matching this prefix
extractToken: (request) => {
return request.headers.authorization?.replace('Bearer ', '')
}
}
})
// Register MCP plugin (routes will inherit authentication)
await app.register(mcpPlugin, {
// ... your configuration
})
// Usage with authentication
fetch('/mcp', {
method: 'POST',
headers: {
'Authorization': 'Bearer your-secret-bearer-token',
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ jsonrpc: '2.0', method: 'ping', id: 1 })
})
await app.register(import('@fastify/bearer-auth'), {
keys: new Set([process.env.MCP_BEARER_TOKEN || 'default-dev-token']),
auth: {
extractToken: (request) => {
return request.headers.authorization?.replace('Bearer ', '')
}
}
})
serverInfo
: Server identification (name, version)capabilities
: MCP capabilities configurationinstructions
: Optional server instructionsenableSSE
: Enable Server-Sent Events support (default: false)redis
: Redis configuration for horizontal scaling (optional)host
: Redis server hostnameport
: Redis server portdb
: Redis database numberpassword
: Redis authentication password- Additional ioredis connection options supported
The plugin adds the following decorators to your Fastify instance:
// With TypeBox schema (recommended)
app.mcpAddTool<TSchema extends TObject>(
definition: { name: string, description: string, inputSchema: TSchema },
handler?: (params: Static<TSchema>, context?: { sessionId?: string }) => Promise<CallToolResult>
)
// Without schema (unsafe)
app.mcpAddTool(
definition: { name: string, description: string },
handler?: (params: any, context?: { sessionId?: string }) => Promise<CallToolResult>
)
// With URI schema
app.mcpAddResource<TUriSchema extends TSchema>(
definition: { uriPattern: string, name: string, description: string, uriSchema?: TUriSchema },
handler?: (uri: Static<TUriSchema>) => Promise<ReadResourceResult>
)
// Without schema
app.mcpAddResource(
definition: { uriPattern: string, name: string, description: string },
handler?: (uri: string) => Promise<ReadResourceResult>
)
// With argument schema (automatically generates arguments array)
app.mcpAddPrompt<TArgsSchema extends TObject>(
definition: { name: string, description: string, argumentSchema?: TArgsSchema },
handler?: (name: string, args: Static<TArgsSchema>) => Promise<GetPromptResult>
)
// Without schema
app.mcpAddPrompt(
definition: { name: string, description: string, arguments?: PromptArgument[] },
handler?: (name: string, args: any) => Promise<GetPromptResult>
)
app.mcpBroadcastNotification(notification)
: Broadcast a notification to all connected SSE clients (works across Redis instances)app.mcpSendToSession(sessionId, message)
: Send a message/request to a specific SSE session (works across Redis instances)
Handler functions are called when the corresponding MCP methods are invoked:
- Tool handlers receive validated, typed arguments and return
CallToolResult
- Resource handlers receive validated URIs and return
ReadResourceResult
- Prompt handlers receive the prompt name and validated, typed arguments, return
GetPromptResult
The plugin exposes the following endpoints:
POST /mcp
: Handles JSON-RPC 2.0 messages according to the MCP specification- Supports both regular JSON responses and SSE streams based on
Accept
header - Returns
Content-Type: application/json
orContent-Type: text/event-stream
- Supports both regular JSON responses and SSE streams based on
GET /mcp
: Long-lived SSE streams for server-initiated communication (when SSE is enabled)- Returns
Content-Type: text/event-stream
with periodic heartbeats
- Returns
initialize
: Server initializationping
: Health checktools/list
: List available toolstools/call
: Execute a tool (calls registered handler or returns error)resources/list
: List available resourcesresources/read
: Read a resource (calls registered handler or returns error)prompts/list
: List available promptsprompts/get
: Get a prompt (calls registered handler or returns error)
Apache 2.0