MCP Status Callback
A utility for handling API callbacks via Ngrok tunnels. Especially useful for MCP (Model Context Protocol) status callbacks.
This module creates a local Express server and establishes an Ngrok tunnel to it, allowing external services to send callbacks to your local development environment.
New in Version 0.5.0: Official ngrok SDK
As of version 0.5.0, this package now uses the official ngrok JavaScript SDK (@ngrok/ngrok
) instead of the community-maintained package. This provides several benefits:
- Official support from ngrok
- More robust and feature-rich API
- Better TypeScript integration
- Improved error handling
- Native support for TLS backends
The API remains backward compatible, so existing code should continue to work without changes.
Installation
npm install @deshartman/mcp-status-callback
Requirements
- Node.js 18 or higher
- An Ngrok account and auth token (get one at ngrok.com)
Usage
Basic Usage
import { CallbackHandler, CallbackHandlerEventNames } from '@deshartman/mcp-status-callback';
const callbackHandler = new CallbackHandler({
ngrokAuthToken: 'your-ngrok-auth-token',
customDomain: 'your-custom-domain.ngrok.dev'
});
callbackHandler.on(CallbackHandlerEventNames.LOG, (data) => {
console.log(`${data.level}: ${data.message}`);
});
callbackHandler.on(CallbackHandlerEventNames.CALLBACK, (data) => {
console.log('Received callback query parameters:', data.queryParameters);
console.log('Received callback body:', data.body);
});
callbackHandler.on(CallbackHandlerEventNames.TUNNEL_STATUS, (data) => {
if (data.level === 'error') {
console.error('Tunnel error:', data.message);
} else {
console.log('Callback URL:', data.message);
}
});
(async () => {
try {
const publicUrl = await callbackHandler.start();
console.log(`Server started! Use this URL for callbacks: ${publicUrl}`);
} catch (error) {
console.error('Failed to start callback handler:', error);
}
})();
TypeScript Usage
import {
CallbackHandler,
CallbackEventData,
CallbackHandlerOptions,
CallbackHandlerEventNames,
LogEventData,
TunnelStatusEventData
} from '@deshartman/mcp-status-callback';
const options: CallbackHandlerOptions = {
ngrokAuthToken: 'your-ngrok-auth-token',
customDomain: 'your-custom-domain.ngrok.dev'
};
const callbackHandler = new CallbackHandler(options);
callbackHandler.on(CallbackHandlerEventNames.LOG, (data: LogEventData) => {
console.log(`[${data.level.toUpperCase()}] ${data.message}`);
});
callbackHandler.on(CallbackHandlerEventNames.CALLBACK, (data: CallbackEventData) => {
const queryParams = data.queryParameters;
const payload = data.body;
console.log('TS Callback received:', payload);
});
callbackHandler.on(CallbackHandlerEventNames.TUNNEL_STATUS, (data: TunnelStatusEventData) => {
if (data.level === 'info') {
console.log('TS Tunnel Ready:', data.message);
} else {
console.error('TS Tunnel Error:', data.message);
}
});
const startServer = async () => {
try {
const publicUrl = await callbackHandler.start();
console.log(`Server started with callback URL: ${publicUrl}`);
return publicUrl;
} catch (error) {
console.error('Failed to start server:', error);
throw error;
}
};
startServer();
API Reference
CallbackHandler
The main class for handling callbacks.
Constructor
new CallbackHandler(options: CallbackHandlerOptions)
options.ngrokAuthToken
(required): Your Ngrok authentication token
options.customDomain
(optional): Custom domain for Ngrok tunnel (requires paid Ngrok plan)
Methods
start(): Promise<string>
- Starts the callback server and establishes an Ngrok tunnel. Returns a Promise that resolves to the public callback URL, which you can use directly in your API requests.
getPublicUrl(): string | null
- Returns the public Ngrok URL if available
stop(): Promise<void>
- Stops the callback server and closes the Ngrok tunnel
Events
Use the exported CallbackHandlerEventNames
constants for type-safe event handling.
CallbackHandlerEventNames.LOG
('log'
) - Emitted for general log messages.
data
: LogEventData
({ level: 'info' | 'warn' | 'error', message: string | Error }
)
CallbackHandlerEventNames.CALLBACK
('callback'
) - Emitted when a callback is received on the /callback
endpoint.
data
: CallbackEventData
({ level: 'info', queryParameters: any, body: any }
)
CallbackHandlerEventNames.TUNNEL_STATUS
('tunnelStatus'
) - Emitted when the tunnel status changes (e.g., connection established, error) or provides the initial URL.
data
: TunnelStatusEventData
({ level: 'info' | 'error', message: string | Error }
)
Automatic Port Finding
The CallbackHandler automatically finds an available port if the specified port is in use. This means you don't have to worry about port conflicts when starting the server. If the default port (4000) or your specified port is already in use, the server will increment the port number and try again until it finds an available port.
Custom Domains
Ngrok allows you to use custom domains with paid plans. This gives you a consistent URL for your callbacks, which is useful for:
- Configuring webhooks in third-party services without updating them each time you restart
- Sharing a stable URL with team members
- Testing with consistent URLs across development sessions
- Creating a more professional appearance for demos
To use a custom domain:
async function startWithCustomDomain() {
try {
const callbackHandler = new CallbackHandler({
ngrokAuthToken: 'your-ngrok-auth-token',
customDomain: 'your-domain.ngrok.dev'
});
const publicUrl = await callbackHandler.start();
console.log(`Custom domain callback URL: ${publicUrl}`);
} catch (error) {
console.error('Failed to start with custom domain:', error);
}
}
async function startWithOptionalCustomDomain() {
const ngrokAuthToken = process.env.NGROK_AUTH_TOKEN;
const customDomain = process.env.NGROK_CUSTOM_DOMAIN;
if (!ngrokAuthToken) {
console.error('NGROK_AUTH_TOKEN environment variable is required');
return;
}
try {
const callbackHandler = new CallbackHandler({
ngrokAuthToken,
customDomain: customDomain || undefined
});
const publicUrl = await callbackHandler.start();
console.log(`Callback URL: ${publicUrl}`);
} catch (error) {
console.error('Failed to start callback handler:', error);
}
}
Note: Custom domains require a paid Ngrok plan. See ngrok.com/pricing for details.
Automatic Content Type Conversion
The CallbackHandler automatically converts application/x-www-form-urlencoded
request bodies to JSON objects. This is particularly useful when working with services like Twilio that send callbacks in URL-encoded format by default.
When a request with Content-Type: application/x-www-form-urlencoded
is received:
- The Express middleware parses the URL-encoded body
- The CallbackHandler converts it to a proper JSON object
- The converted JSON object is passed to your callback event handler
- A log event is emitted indicating the conversion occurred
This means you can work with a consistent JSON format in your callback handlers regardless of how the data was originally sent, simplifying your code.
callbackHandler.on('callback', (data) => {
console.log('Received callback data:', data.body);
if (data.body.status === 'completed') {
}
});
Example: Using with MCP Servers
This utility is particularly useful for MCP (Model Context Protocol) servers that need to receive callbacks from external services.
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { CallbackHandler } from '@deshartman/mcp-status-callback';
const callbackHandler = new CallbackHandler({
ngrokAuthToken: 'your-ngrok-auth-token',
customDomain: 'your-custom-domain.ngrok.dev'
});
async function setupCallbackHandler() {
try {
const callbackUrl = await callbackHandler.start();
console.log(`Callback URL ready: ${callbackUrl}`);
return callbackUrl;
} catch (error) {
console.error('Failed to start callback handler:', error);
throw error;
}
}
callbackHandler.on('tunnelStatus', (data) => {
if (data.level === 'info') {
console.log(`Tunnel status update: ${data.message}`);
}
});
setupCallbackHandler().then(url => {
});
Publishing
This package is published with a scope. To publish updates:
npm run build
npm publish --access=public
npm publish
License
MIT