-
A lightweight adapter for creating MCP (Model Context Protocol) servers using Express.js.
-
Sponsored by https://tixaeagents.ai create Text/Voice AI agents in seconds, compatible with MCP servers.
Checklist:
- Express middleware integration SSE support
- Websocket integration support (Soon but SSE is working great)
- Tool implementation with TypeScript support
- Header-based authorization support
- Multiple MCP clients on different endpoints
- Prompts support (Soon as it's kinda needless)
- You can't seperately scale up or down your MCP clients (and group some together if they are all light weight) from the main LLM service hosting the chat, if you have 100 people opening playwright, brave, etc MCPs directly with npx on a single server that could easily eat up a lot of memory & bottleneck performance.
- Default way of deploying, updating & maintaining many MCP servers is annoying, this is trying to simplify it.
npm install mcp-express-adapter@latest
# or
yarn add mcp-express-adapter@latest
# or
pnpm add mcp-express-adapter@latest
Note: You'll need to authenticate to GitHub Packages if installing from a private repository. See GitHub Packages documentation for more details.
Create an MCP client on an express server (example)
// examples/with-express/src/super-simple.ts
import express from 'express'
import cors from 'cors'
import { MCPClient, mcpTool } from 'mcp-express-adapter'
import { z } from 'zod'
// Create Express app
const app = express()
app.use(cors())
// Define a super simple weather tool
const weatherTool = mcpTool({
name: 'get_weather',
description: 'Get weather for a location',
// Define input schema
schema: z.object({
location: z.string().describe('The city to get weather for'),
}),
// No output schema needed for simple string responses
handler: async (args) => {
// Just return a string - mcpTool handles the formatting
return `Weather for ${args.location}: ☀️ Sunny and 72°F`
},
})
// Create MCP client
const mcpClient = new MCPClient({
endpoint: '/mcp',
tools: [weatherTool],
serverName: 'demo-server',
serverVersion: '1.0.0',
})
// Mount MCP router
app.use('/mcp', mcpClient.middleware())
// Apply JSON parser for other routes
app.use(express.json())
// Start the server
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`✨ Super Simple MCP Server running!`)
console.log(`🔗 Connect at: http://localhost:${PORT}/mcp/sse`)
})
MCP Client created with the following configuration:
- Endpoint: /mcp
- Server: my-mcp-server v1.0.0
- Tools: get_weather, calculator, generate_list, greeting
MCP Server running on port 3000
Connect at: http://localhost:3000/mcp/sse
Debug mode: enabled will show debug logs, to disable set NODE_ENV=production
- settings > developer > edit config file to this:
{
"mcpServers": {
"localMcpServer": {
"command": "npx",
"args": [
"-y",
"mcp-express-adapter",
"--host",
"http://localhost:3000/mcp/sse"
]
}
}
}
Then Restart Claude desktop and you'll be able to see the MCP tools within few seconds
// examples/with-express/src/multiple-mcp-clients.ts
import express from 'express'
import cors from 'cors'
import { MCPClient, mcpTool } from 'mcp-express-adapter'
import { z } from 'zod'
// Create Express app
const app = express()
app.use(cors())
// Define weather tool using mcpTool helper
const weatherTool = mcpTool({
name: 'get_weather',
description: 'Get the current weather for a location',
schema: z.object({
location: z.string().describe('The location to get weather for'),
}),
// you can define typesafe output schema..
outputSchema: z.object({
farenheight: z.number().describe('The temperature in farenheight'),
celsius: z.number().describe('The temperature in celsius'),
}),
handler: async (args) => {
return {
farenheight: 72,
celsius: 22,
}
},
})
// Define calculator tool using mcpTool helper
const calculatorTool = mcpTool({
name: 'calculate',
description: 'Calculate the result of a mathematical expression',
schema: z.object({
expression: z.string().describe('The mathematical expression to evaluate'),
}),
handler: async (args) => {
return `Result: ${eval(args.expression)}`
},
})
// Define time tool using mcpTool helper
const timeTool = mcpTool({
name: 'get_time',
description: 'Get the current time, optionally for a specific timezone',
schema: z.object({
timezone: z
.string()
.optional()
.describe('The timezone to get time for (optional)'),
}),
handler: async (args) => {
return `Current time${args.timezone ? ` in ${args.timezone}` : ''}: ${new Date().toLocaleString()}`
},
})
// Create first MCP client with weather tool
const weatherClient = new MCPClient({
endpoint: '/weather-mcp',
tools: [weatherTool],
serverName: 'weather-mcp-server',
serverVersion: '1.0.0',
})
// Create second MCP client with calculator tool
const calculatorClient = new MCPClient({
endpoint: '/calculator-mcp',
tools: [calculatorTool],
serverName: 'calculator-mcp-server',
serverVersion: '1.0.0',
})
// Create third MCP client with time tool
const timeClient = new MCPClient({
endpoint: '/time-mcp',
tools: [timeTool],
serverName: 'time-mcp-server',
serverVersion: '1.0.0',
})
// Mount MCP routers BEFORE global JSON parser
app.use('/weather-mcp', weatherClient.middleware())
app.use('/calculator-mcp', calculatorClient.middleware())
app.use('/time-mcp', timeClient.middleware())
// Apply global JSON parser AFTER agent routes
app.use(express.json())
// Start the server
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000
app.listen(PORT, () => {
console.log(`Multiple MCP Servers running on port ${PORT}`)
console.log(`Weather MCP: http://localhost:${PORT}/weather-mcp/sse`)
console.log(`Calculator MCP: http://localhost:${PORT}/calculator-mcp/sse`)
console.log(`Time MCP: http://localhost:${PORT}/time-mcp/sse`)
})
- With the help of @langchain/mcp-adapters https://github.com/langchain-ai/langchainjs-mcp-adapters
// examples/with-langchain/src/index.ts
import { MultiServerMCPClient } from '@langchain/mcp-adapters'
import { ChatAnthropic } from '@langchain/anthropic'
import { createReactAgent } from '@langchain/langgraph/prebuilt' // Incorrect
import dotenv from 'dotenv'
dotenv.config()
async function runLangchainMcpExample() {
console.log('Initializing LangChain with MCP Adapters...')
const model = new ChatAnthropic({
model: 'claude-3-5-sonnet-20240620',
temperature: 0,
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
})
// Keep constructor with only mcpServers map
const mcpClient = new MultiServerMCPClient({
googleMapsServer: {
// The server map directly
transport: 'sse',
url: 'http://localhost:3000/mcp/sse',
useNodeEventSource: true,
reconnect: {
enabled: true,
maxAttempts: 3,
delayMs: 1000,
},
},
})
console.log('Loading tools from MCP server via express adapter...')
// Keep getTools call with options
const tools = (await Promise.race([
mcpClient.getTools(),
new Promise((_, reject) =>
setTimeout(
() =>
reject(new Error('Timeout: Failed to load tools within 15 seconds')),
15000,
),
),
])) as Awaited<ReturnType<typeof mcpClient.getTools>>
if (tools.length === 0) {
console.error('No tools were loaded...')
await mcpClient.close()
return
}
console.log(
`Loaded ${tools.length} tools:`,
tools.map((t) => t.name).join(', '),
)
const agent = await createReactAgent({
llm: model,
tools,
})
const messages = [
{
role: 'system',
content:
'You are a helpful assistant. Use tools to answer user questions.',
},
{
role: 'user',
content: `What is the current weather in San Francisco?`,
},
]
let inputs = { messages }
// console.log(`ALL GOOD NOW TART EVEN STREAMM>>!: `, inputs);
// await new Promise((resolve) => setImmediate(resolve));
const eventStream = await agent.streamEvents(inputs, {
version: 'v2',
// signal: localController.signal, // <--- critical to pass localController!
})
// --- Invocation remains the same ---
for await (const event of eventStream) {
if (event.event === 'on_chat_model_stream') {
console.log('Chat model stream')
console.log(event.data.chunk.content[0]?.text)
} else if (event.event === 'on_tool_start') {
console.log('Tool start')
console.log(JSON.stringify(event, null, 2))
} else if (event.event === 'on_tool_end') {
console.log('Tool end')
console.log(JSON.stringify(event, null, 2))
}
}
// console.log("\nClosing MCP client connections...");
// await mcpClient.close();
// console.log("MCP client closed.");
// throw new Error("Test error");
}
runLangchainMcpExample()
Here's a complete example using the mcpTool
helper for creating type-safe MCP tools with Zod schemas:
// examples/with-express/src/index.ts
import express from 'express'
import cors from 'cors'
import { MCPClient, mcpTool } from 'mcp-express-adapter'
import { z } from 'zod'
import dotenv from 'dotenv'
// Load environment variables
dotenv.config()
// Create Express app
const app = express()
app.use(cors())
// Define weather tool using the enhanced mcpTool helper
const weatherTool = mcpTool({
name: 'get_weather',
description: 'Get the current weather for a location',
schema: z.object({
location: z.string().describe('The location to get weather for'),
}),
// Define the output schema
outputSchema: z
.object({
temperature: z.number().describe('Current temperature in °F'),
condition: z.string().describe('Weather condition (e.g., Sunny, Rainy)'),
humidity: z.number().describe('Humidity percentage'),
location: z.string().describe('The location this weather is for'),
})
.describe('Weather information for the requested location'),
// Simply return the data - mcpTool handles the MCP formatting
handler: async (args) => {
console.log(`[WeatherTool] Called with location: ${args.location}`)
// Return an object matching our output schema
return {
temperature: 72,
condition: 'Sunny',
humidity: 45,
location: args.location,
}
},
})
// Add a calculator tool with a simple numeric output
const calculatorTool = mcpTool({
name: 'calculator',
description: 'Calculate the sum of two numbers',
schema: z.object({
a: z.number().describe('First number'),
b: z.number().describe('Second number'),
}),
// Output is just a number
outputSchema: z.number().describe('The sum of the two input numbers'),
// Simply return the sum - no need to format for MCP
handler: async (args) => {
console.log(`[CalculatorTool] Called with: ${args.a}, ${args.b}`)
return args.a + args.b
},
})
// Add a tool that returns an array
const listTool = mcpTool({
name: 'generate_list',
description: 'Generate a list of items based on a category',
schema: z.object({
category: z
.string()
.describe('Category to generate items for (e.g., fruits, colors)'),
count: z
.number()
.optional()
.describe('Number of items to generate (default: 3)'),
}),
// Output is an array of strings
outputSchema: z
.array(z.string())
.describe('List of generated items in the category'),
handler: async (args) => {
const count = args.count || 3
console.log(
`[ListTool] Generating ${count} items for category: ${args.category}`,
)
// Sample data based on category
const items: Record<string, string[]> = {
fruits: ['apple', 'banana', 'orange', 'grape', 'strawberry'],
colors: ['red', 'blue', 'green', 'yellow', 'purple'],
animals: ['dog', 'cat', 'elephant', 'tiger', 'penguin'],
}
const categoryItems = items[args.category.toLowerCase()] || [
'item1',
'item2',
'item3',
'item4',
'item5',
]
return categoryItems.slice(0, count)
},
})
// Add a tool that doesn't specify an outputSchema (will expect string return)
const greetingTool = mcpTool({
name: 'greeting',
description: 'Get a personalized greeting',
schema: z.object({
name: z.string().describe('The name to greet'),
formal: z.boolean().optional().describe('Whether to use formal language'),
}),
// No outputSchema needed, just return a string
handler: async (args) => {
const greeting = args.formal
? `Good day, ${args.name}. How may I be of service?`
: `Hey ${args.name}! How's it going?`
console.log(
`[GreetingTool] Generated greeting for ${args.name} (formal: ${args.formal || false})`,
)
return greeting
},
})
// Add a protected tool that checks for authentication
const protectedTool = mcpTool({
name: 'get_passcode',
description: 'Get the passcode for the user',
schema: z.object({
name: z.string().describe('The name of the user'),
}),
// Implement authentication check in the handler
handler: async (args, context) => {
console.log(`[ProtectedTool] Called with name: ${args.name}`)
// Check for authorization header
const authHeader = context?.headers?.authorization || ''
console.log(context)
console.log(`[ProtectedTool] Auth header: ${authHeader}`)
// Check for bearer token that matches "000000"
const validToken = 'Bearer 000000'
if (!authHeader || authHeader !== validToken) {
// Return error for unauthorized access
throw new Error('Unauthorized: Invalid or missing authentication token')
}
// If authorized, return the protected data
return `Protected data for ID: ${args.name}`
},
})
// if true will show debug logs, to disable set NODE_ENV=production
const debugMode = process.env.NODE_ENV === 'development'
// Create MCP client
const mcpClient = new MCPClient({
endpoint: '/mcp',
tools: [weatherTool, calculatorTool, listTool, greetingTool, protectedTool],
serverName: 'my-mcp-server',
serverVersion: '1.0.0',
debug: debugMode, // Enable debug logs only when --debug flag is passed
})
// Show metadata about the client
const metadata = mcpClient.getMetadata()
console.log('MCP Client created with the following configuration:')
console.log(`- Endpoint: ${metadata.endpoint}`)
console.log(`- Server: ${metadata.serverName} v${metadata.serverVersion}`)
console.log(`- Tools: ${metadata.tools.map((tool) => tool.name).join(', ')}`)
// Mount MCP router
app.use('/mcp', mcpClient.middleware())
// Apply JSON parser for other routes
app.use(express.json())
app.get('/', (req, res) => {
res.send(`Hello World MCP Express Adapter.`)
})
// Start the server
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000
app.listen(PORT, () => {
const baseUrl = `http://localhost:${PORT}`
// Get the SSE endpoint URL using the helper method
const sseEndpoint = mcpClient.getSSEEndpoint(baseUrl)
console.log(`MCP Client SSE Endpoint: ${sseEndpoint}`)
console.log(
`Debug mode: ${debugMode ? 'enabled will show debug logs, to disable set NODE_ENV=production' : 'disabled will not log anything.'}`,
)
})
You can run this example with:
# From the root of the repo
pnpm install
pnpm test-express
Here's how to create a simple tool with the mcpTool
helper:
// examples/with-express/src/tool-example.ts
import { mcpTool } from 'mcp-express-adapter'
import { z } from 'zod'
/**
* Example 1: Tool with an output schema for complex data
*
* Use this approach when your tool returns structured data that
* needs strong type checking.
*/
const weatherTool = mcpTool({
name: 'get_weather',
description: 'Get the current weather for a location',
schema: z.object({
location: z.string().describe('The location to get weather for'),
}),
// Define the output schema for structured data
outputSchema: z
.object({
temperature: z.number().describe('Current temperature in °F'),
condition: z.string().describe('Weather condition (e.g., Sunny, Rainy)'),
humidity: z.number().describe('Humidity percentage'),
location: z.string().describe('The location this weather is for'),
})
.describe('Weather information for the requested location'),
handler: async (args) => {
// args.location is fully typed as string
return {
temperature: 72,
condition: 'Sunny',
humidity: 45,
location: args.location,
}
},
})
/**
* Example 2: Tool without an output schema for simple string responses
*
* Use this approach when your tool returns simple text responses
* that don't need complex structure or validation.
*/
const greetingTool = mcpTool({
name: 'greeting',
description: 'Get a personalized greeting',
schema: z.object({
name: z.string().describe('The name to greet'),
formal: z.boolean().optional().describe('Whether to use formal language'),
}),
// No outputSchema needed for simple string responses
handler: async (args) => {
// When no outputSchema is provided, you must return a string
return args.formal
? `Good day, ${args.name}. How may I be of service?`
: `Hey ${args.name}! How's it going?`
},
})
// non typesafe tool:
// javascript ready
const nonTypesafeTool = {
name: 'search_web',
description: 'Search the web for information',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'The search query' },
limit: {
type: 'number',
description: 'Maximum number of results to return',
},
},
required: ['query'],
},
handler: async (args) => ({
content: [
{
type: 'text',
text: `Search results for "${args.query}": Results here...`,
},
],
isError: false,
}),
}
export { weatherTool, greetingTool, nonTypesafeTool }
The main class for creating an MCP endpoint on your Express server.
interface MCPClientOptions {
endpoint: string // The base path for the MCP endpoints
tools: ToolImpl[] // Array of tool implementations
serverName?: string // Optional server name (default: 'mcp-server')
serverVersion?: string // Optional server version (default: '1.0.0')
}
interface ToolImpl<T = any> {
name: string // Tool name
description: string // Tool description
inputSchema: {
// JSON Schema for the tool's input
type: 'object'
properties: Record<string, any>
required?: string[]
}
handler: (
args: T,
context?: {
headers?: Record<string, string> // Request headers accessible here
[key: string]: any
},
) => Promise<{
content: Array<
| { type: string; text?: string }
| { type: string; data?: string; mimeType?: string }
>
isError?: boolean
}>
}
You can access request headers within your tool's handler function via the context.headers
object. This is useful for implementing authentication, passing custom metadata, or other header-based logic.
Headers sent by the client (e.g., using the mcp-express-adapter
CLI with --header
or --headers
flags) are made available in the context
.
Example: Passing Authorization Header via CLI
To call a protected tool that expects an Authorization: Bearer <token>
header, you can use the CLI adapter like this:
# Using --header
npx mcp-express-adapter@latest --host http://localhost:3000/mcp/sse --header "Authorization: Bearer 000000"
# Using --headers (if passing multiple)
npx mcp-express-adapter@latest --host http://localhost:3000/mcp/sse --headers "Authorization: Bearer 000000, X-Custom: my-value"
Example: Tool Reading the Authorization Header
Here's the protectedTool
example (from examples/with-express/src/index.ts
) demonstrating how to read the Authorization
header from the context
:
// Protected tool example with authentication
const protectedTool = mcpTool({
name: 'get_passcode', // or 'protected_data' depending on your example version
description: 'Get protected data (requires authentication)',
schema: z.object({
// ... input schema properties
name: z.string().describe('The name of the user'), // Example property
}),
handler: async (args, context) => {
// Access the headers from the context object
// Node.js automatically lowercases header names
const authHeader = context?.headers?.authorization || ''
console.log(`[ProtectedTool] Auth header received: ${authHeader}`)
// Validate the token (e.g., check for a specific Bearer token)
const validToken = 'Bearer 000000'
if (authHeader !== validToken) {
throw new Error('Unauthorized: Invalid or missing authentication token')
}
// If authorized, proceed with tool logic
console.log(`[ProtectedTool] Authorized access for user: ${args.name}`)
return `Protected passcode for ${args.name}: 123456` // Return the protected data
},
})
Here's a more advanced example with multiple tools and endpoints:
import express from 'express'
import cors from 'cors'
import { MCPClient } from 'mcp-express-adapter'
const app = express()
app.use(cors())
// Define multiple tools
const weatherTool = {
name: 'get_weather',
description: 'Get the current weather for a location',
inputSchema: {
type: 'object',
properties: {
location: { type: 'string', description: 'The location' },
},
required: ['location'],
},
handler: async (args) => ({
content: [
{ type: 'text', text: `Weather for ${args.location}: Sunny, 72°F` },
],
isError: false,
}),
}
const searchTool = {
name: 'search_web',
description: 'Search the web for information',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'The search query' },
},
required: ['query'],
},
handler: async (args) => ({
content: [
{
type: 'text',
text: `Search results for "${args.query}": Results here...`,
},
],
isError: false,
}),
}
// Set up multiple MCP Clients on different endpoints
const agent1 = new MCPClient({
endpoint: '/agent-1',
tools: [weatherTool, searchTool],
serverName: 'mcp-server-agent-1',
serverVersion: '1.0.0',
})
const agent2 = new MCPClient({
endpoint: '/agent-2',
tools: [weatherTool], // This agent only has weather tool
serverName: 'mcp-server-agent-2',
serverVersion: '1.0.0',
})
// Mount agent routers BEFORE global JSON parser
app.use('/agent-1', agent1.middleware())
app.use('/agent-2', agent2.middleware())
// Apply global JSON parser AFTER agent routes
app.use(express.json())
// Start the server
const PORT = 3000
app.listen(PORT, () => {
console.log(`MCP Server running on port ${PORT}`)
console.log(`Agent 1: http://localhost:${PORT}/agent-1/sse`)
console.log(`Agent 2: http://localhost:${PORT}/agent-2/sse`)
})
You can test your MCP server using curl
to connect to the SSE endpoint:
curl -N http://localhost:3000/mcp/sse
Or use the MCP command-line client:
# Basic usage
npx mcp-express-adapter --host http://localhost:3000/mcp/sse
# With a single header
npx mcp-express-adapter --host http://localhost:3000/mcp/sse --header "Authorization: Bearer token123"
# With multiple headers (option 1: repeating --header)
npx mcp-express-adapter --host http://localhost:3000/mcp/sse --header "Authorization: Bearer token123" --header "X-Custom: Value"
# With multiple headers (option 2: comma-separated list)
npx mcp-express-adapter --host http://localhost:3000/mcp/sse --headers "Authorization: Bearer token123, X-Custom: Value"
- Make sure to apply exress.json() AFTER the app.use MCP middleware
- Webscokets are not yet tested enough.
- Moe03 - Main contributor and maintainer
Interested in contributing? Please open an issue on the GitHub repository with your feature request or bug report.
MIT