MCP Middleware Adapter for Express Servers
About
Run multiple MCP clients on a NodeJS Express server (adapter/middleware)
Details
- License
- MIT
Explore
- Express middleware integration with SSE transport
- TypeScript‑friendly tool definition using Zod schemas
- Header‑based authorization support
- Multiple MCP clients on different endpoints
- Lightweight design for deploying and scaling MCP servers
- Simplifies hosting many MCP tools in one process
Setting up with Highlight
This MCP is not yet compatible with Highlight’s one-click setup. However, you can still use it with Highlight by following these steps:
- Download and install Highlight from highlightai.com/download
- Navigate to the plugins tab and select "Add Custom Plugin"
-
Configure the plugin with the settings below
Plugin Name
MCP Middleware Adapter for Express ServersCommand (node, npx, python, etc.)Please refer to the README for specific instructions on how to obtain API keys or other required environment variables.
- Enable "Start Automatically" if you want the plugin to start when Highlight launches
From the repository
npm install mcp-express-adapter@latest
bashMCP 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
- With the help of @langchain/mcp-adapters https://github.com/langchain-ai/langchainjs-mcp-adapters
typescript// 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()
```
npx mcp-express-adapter --host http://localhost:3000/mcp/sse
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:
Here's how to create a simple tool with the mcpTool helper:
typescript// 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 }
typescriptinterface 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:
bashClaude Desktop / Cursor
Paste into your MCP client config file to install this server.
{
"mcpServers": {
"mcp middleware adapter for express servers": {
"mcp-express-adapter": {
"command": "npx",
"args": [
"mcp-express-adapter@latest",
"--host",
"http://localhost:3000/mcp/sse",
"--header",
"Authorization: Bearer 000000"
]
}
}
}
}
McpServers
{
"mcp-express-adapter": {
"command": "npx",
"args": [
"mcp-express-adapter@latest",
"--host",
"http://localhost:3000/mcp/sse",
"--header",
"Authorization: Bearer 000000"
]
}
}
- 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:
- [x] Express middleware integration SSE support
- [ ] Websocket integration support (Soon but SSE is working great)
- [x] Tool implementation with TypeScript support
- [x] Header-based authorization support
- [x] Multiple MCP clients on different endpoints
- [ ] Prompts support (Soon as it's kinda needless)
Why
- 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.
Installation
```bash
npm install mcp-express-adapter@latest
Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.



