mcp-http-webhook
v1.0.29
Published
Production-ready MCP server framework with HTTP + webhook-based subscriptions
Maintainers
Readme
MCP HTTP Webhook Server Library
Production-ready TypeScript library for building Model Context Protocol (MCP) servers using the official @modelcontextprotocol/sdk with HTTP transport and webhook-based resource subscriptions.
✨ Key Highlights
- 🎯 Standard MCP Compliant: Built on official
@modelcontextprotocol/sdk- works with all MCP clients - 🔌 Universal Client Support: Compatible with MCP Inspector, Claude Desktop, VS Code, Cursor, and more
- 📡 Webhook Subscriptions: Native support for third-party webhooks (GitHub, Google Drive, Slack, etc.)
- 🚀 Horizontally Scalable: Stateless HTTP design for trivial multi-instance deployment
- 🔒 Production Ready: Built-in retry logic, signature verification, and persistent storage
- 📘 Type Safe: Full TypeScript support with comprehensive type definitions
- 💾 Flexible Storage: Pluggable key-value store (Redis, in-memory, or custom)
🆕 v1.0 Update
This library now uses the standard MCP SDK (@modelcontextprotocol/sdk) while preserving all webhook subscription, authentication, and persistence features.
📖 Migration Guide - Upgrading from custom protocol? Read this first!
📦 Installation
npm install mcp-http-webhook zod
# or
pnpm add mcp-http-webhook zod
# or
yarn add mcp-http-webhook zodPeer Dependencies
npm install @modelcontextprotocol/sdk expressOptional Dependencies
npm install ioredis # For Redis store
npm install prom-client # For Prometheus metrics🎯 Quick Start
import { createMCPServer, InMemoryStore } from 'mcp-http-webhook';
const server = createMCPServer({
name: 'my-mcp-server',
version: '1.0.0',
publicUrl: 'https://mcp.example.com',
store: new InMemoryStore(),
tools: [
{
name: 'echo',
description: 'Echo back the input',
inputSchema: {
type: 'object',
properties: {
message: { type: 'string' }
},
required: ['message']
},
handler: async (input) => {
return { echo: input.message };
}
}
],
resources: [
{
uri: 'example://resource',
name: 'Example Resource',
description: 'An example resource',
read: async (uri) => {
return { contents: [{ uri, text: 'Hello, World!' }] };
}
}
]
});
await server.start();
console.log('Server running on http://localhost:3000');🔌 Client Integration
With MCP Inspector
npx @modelcontextprotocol/inspector http://localhost:3000/mcpWith Claude Desktop / VS Code
Add to your MCP configuration:
{
"mcpServers": {
"my-server": {
"url": "http://localhost:3000/mcp"
}
}
}📚 Documentation
Configuration
interface MCPServerConfig {
// Server Identity
name: string;
version: string;
// HTTP Configuration
port?: number; // Default: 3000
host?: string; // Default: '0.0.0.0'
basePath?: string; // Default: '/mcp'
publicUrl: string; // Required
// Core Components
tools: ToolDefinition[];
resources: ResourceDefinition[];
prompts?: PromptDefinition[];
// Storage (Required)
store: KeyValueStore;
// Authentication
authenticate?: (req: Request) => Promise<AuthContext>;
// Webhook Configuration
webhooks?: WebhookConfig;
// Logging
logger?: Logger;
logLevel?: 'debug' | 'info' | 'warn' | 'error';
}Tools
Tools are functions that can be called by clients:
{
name: 'create_issue',
description: 'Create a GitHub issue',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string' },
body: { type: 'string' }
},
required: ['title']
},
handler: async (input, context) => {
// Your logic here
return { issue_id: 123 };
}
}Resources
Resources represent data that can be read and subscribed to:
{
uri: 'github://repo/{owner}/{repo}/issues',
name: 'GitHub Issues',
description: 'Repository issues',
read: async (uri, context, options) => {
// Access pagination options
const page = options?.pagination?.page || 1;
const limit = options?.pagination?.limit || 10;
return {
contents: [...],
pagination: {
page,
limit,
total: 100,
hasMore: true
}
};
},
list: async (context, options) => {
// Pagination support in list
const page = options?.pagination?.page || 1;
const limit = options?.pagination?.limit || 10;
return {
resources: [
{ uri: '...', name: '...' }
],
pagination: {
page,
limit,
total: 50,
hasMore: true
}
};
},
subscription: {
onSubscribe: async (uri, subscriptionId, webhookUrl, context) => {
// Register webhook with third-party service
return { thirdPartyWebhookId: 'webhook_123' };
},
onUnsubscribe: async (uri, subscriptionId, storedData, context) => {
// Remove webhook from third-party service
},
onWebhook: async (subscriptionId, payload, headers) => {
// Process incoming webhook
return {
resourceUri: uri,
changeType: 'created',
data: { ... }
};
}
}
}Storage
The library requires a key-value store for subscription persistence:
In-Memory (Development Only):
import { InMemoryStore } from 'mcp-http-webhook/stores';
const store = new InMemoryStore();Redis:
import { createRedisStore } from 'mcp-http-webhook/stores';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
const store = createRedisStore(redis);Custom Implementation:
import { KeyValueStore } from 'mcp-http-webhook';
class MyStore implements KeyValueStore {
async get(key: string): Promise<string | null> { ... }
async set(key: string, value: string, ttl?: number): Promise<void> { ... }
async delete(key: string): Promise<void> { ... }
async scan?(pattern: string): Promise<string[]> { ... }
}Pagination
Resources support pagination for both list() and read() operations:
Client Request:
// Send pagination params in _meta
{
"jsonrpc": "2.0",
"method": "resources/list",
"params": {},
"_meta": {
"page": 1,
"limit": 10,
"cursor": "optional-cursor"
}
}Server Implementation:
{
list: async (context, options) => {
const page = options?.pagination?.page || 1;
const limit = options?.pagination?.limit || 10;
// Fetch paginated data
const { items, total } = await fetchItems(page, limit);
return {
resources: items,
pagination: {
page,
limit,
total,
hasMore: page * limit < total,
nextCursor: 'next-page-token', // Optional
}
};
},
read: async (uri, context, options) => {
// Pagination also works for large content in read()
const cursor = options?.pagination?.cursor;
const limit = options?.pagination?.limit || 50;
return {
contents: paginatedData,
pagination: {
page: 1,
limit,
hasMore: true,
nextCursor: 'token-for-next-page',
}
};
}
}Response Format:
{
"resources": [...],
"_meta": {
"pagination": {
"page": 1,
"limit": 10,
"total": 100,
"hasMore": true,
"nextCursor": "...",
"prevCursor": "..."
}
}
}See examples/pagination-example.ts for complete examples including cursor-based pagination.
Completions
Completions provide autocomplete suggestions for prompt arguments and resource URI parameters:
Prompt with Completion:
{
name: 'generate-code',
arguments: [
{ name: 'language', required: true },
{ name: 'framework', required: false }
],
handler: async (args, context) => { /* ... */ },
// Completion handler for arguments
completion: async (ref, argument, context) => {
if (argument === 'language') {
return [
{ value: 'python', label: 'Python' },
{ value: 'javascript', label: 'JavaScript' },
{ value: 'typescript', label: 'TypeScript' }
];
}
// Context-aware: suggest frameworks based on language
if (argument === 'framework') {
const lang = ref.arguments?.language;
if (lang === 'python') {
return [
{ value: 'django', label: 'Django' },
{ value: 'flask', label: 'Flask' }
];
}
}
return [];
}
}Resource with Completion:
{
uri: 'github://repos/{owner}/{repo}',
name: 'github-repos',
/* ... */
// Completion for URI parameters
completion: async (ref, argument, context) => {
if (argument === 'owner') {
return [
{ value: 'facebook', label: 'Facebook' },
{ value: 'microsoft', label: 'Microsoft' }
];
}
if (argument === 'repo') {
// Get owner from current URI
const owner = ref.uri.match(/repos\/([^/]+)/)?.[1];
// Return repos for that owner
return fetchReposForOwner(owner);
}
return [];
}
}Global Completion Handler:
createMCPServer({
// ...
completions: async (ref, argument, context) => {
// Fallback for any prompt/resource without specific handler
return [
{ value: 'default', label: 'Default Value' }
];
}
});See examples/completion-example.ts for comprehensive completion examples.
Authentication
authenticate: async (req) => {
const token = req.headers.authorization?.replace('Bearer ', '');
// Verify token
const payload = await verifyJWT(token);
return {
userId: payload.sub,
// Add any custom context
permissions: payload.permissions
};
}Webhooks
Configure webhook behavior:
webhooks: {
incomingPath: '/webhooks/incoming',
incomingSecret: process.env.WEBHOOK_SECRET,
verifyIncomingSignature: (payload, signature, secret) => {
// Verify third-party webhook signature
return verifyHmacSignature(payload, signature, secret);
},
outgoing: {
timeout: 5000,
retries: 3,
retryDelay: 1000,
signPayload: (payload, secret) => {
// Sign outgoing notifications
return createHmacSignature(payload, secret);
}
}
}🔧 API Endpoints
The server automatically exposes these endpoints:
Health Checks
GET /health- Basic health checkGET /ready- Readiness check (includes store connectivity)
MCP Protocol
POST /mcp/tools/list- List available toolsPOST /mcp/tools/call- Call a toolPOST /mcp/resources/list- List available resourcesPOST /mcp/resources/read- Read a resourcePOST /mcp/resources/subscribe- Subscribe to resource changesPOST /mcp/resources/unsubscribe- Unsubscribe from resourcePOST /mcp/prompts/list- List available promptsPOST /mcp/prompts/get- Get a prompt
Webhooks
POST /webhooks/incoming/{subscriptionId}- Receive third-party webhooks
📖 Examples
See the examples/ directory for complete examples:
- basic-setup.ts - Simple server with tools and resources
- github-server.ts - GitHub integration with webhooks
🔒 Security
HTTPS Only in Production
// Automatically enforced in production
publicUrl: 'https://mcp.example.com' // Must use HTTPSSignature Verification
Always verify webhook signatures:
import crypto from 'crypto';
verifyIncomingSignature: (payload, signature, secret) => {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(JSON.stringify(payload));
const expected = `sha256=${hmac.digest('hex')}`;
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}Rate Limiting
Add rate limiting middleware:
import rateLimit from 'express-rate-limit';
const server = createMCPServer({
// ...
middleware: [
rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
})
]
});🚀 Deployment
Docker
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]Docker Compose
version: '3.8'
services:
mcp-server:
build: .
ports:
- "3000:3000"
environment:
- PUBLIC_URL=https://mcp.example.com
- REDIS_URL=redis://redis:6379
depends_on:
- redis
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
volumes:
redis-data:Environment Variables
# Server
PORT=3000
PUBLIC_URL=https://mcp.example.com
NODE_ENV=production
# Storage
REDIS_URL=redis://localhost:6379
# Webhooks
WEBHOOK_SECRET=your-secret-here
# Auth
JWT_SECRET=your-jwt-secret🧪 Testing
# Run tests
npm test
# Run with coverage
npm test -- --coverage
# Run specific test
npm test -- SubscriptionManager📊 Monitoring
Logging
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.Console()
]
});
const server = createMCPServer({
logger,
logLevel: 'info'
});Metrics
Enable Prometheus metrics:
import promClient from 'prom-client';
const register = new promClient.Registry();
promClient.collectDefaultMetrics({ register });
const server = createMCPServer({
metrics: {
enabled: true,
registry: register
}
});
// Metrics available at GET /metrics🤝 Contributing
Contributions are welcome! Please see CONTRIBUTING.md for details.
📄 License
MIT License - see LICENSE file for details.
🔗 Links
💬 Support
Made with ❤️ by the MCP Community
