ntfy-mcp-server
Version:
An MCP (Model Context Protocol) server designed to interact with the ntfy push notification service. It enables LLMs and AI agents to send notifications to your devices with extensive customization options.
125 lines (124 loc) • 6.22 kB
JavaScript
import { config } from "../../../config/index.js";
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
import { ErrorHandler } from "../../../utils/errorHandler.js";
import { logger } from "../../../utils/logger.js";
import { createRequestContext } from "../../../utils/requestContext.js";
import { sanitizeInputForLogging } from "../../../utils/sanitization.js";
import { processNtfyMessage } from "./ntfyMessage.js";
import { SendNtfyToolInputSchema } from "./types.js";
// Create module logger
const moduleLogger = logger.createChildLogger({
module: 'NtfyToolRegistration'
});
/**
* Register the send_ntfy tool with the MCP server
*
* This function registers a tool for sending notifications via ntfy.sh with
* comprehensive parameter support for all ntfy features.
*
* @param server - The MCP server instance to register the tool with
* @returns Promise resolving when registration is complete
*/
export const registerNtfyTool = async (server) => {
// Create a request context for tracking this registration operation
const requestCtx = createRequestContext({
operation: 'registerNtfyTool',
component: 'NtfyTool'
});
// Create a tool-specific logger
const toolLogger = logger.createChildLogger({
module: 'NtfyTool',
operation: 'registration'
});
moduleLogger.info('Starting ntfy tool registration');
// Create a fresh schema with the latest config values
// This ensures we have the most up-to-date environment variables
const schemaWithLatestConfig = SendNtfyToolInputSchema();
// Log default topic info at registration time for verification
const ntfyConfig = config.ntfy;
toolLogger.info('Registering ntfy tool handler with config', {
defaultTopic: ntfyConfig.defaultTopic || '(not set)',
baseUrl: ntfyConfig.baseUrl,
apiKeyPresent: !!ntfyConfig.apiKey
});
try {
// Prepare the description with the default topic information
const defaultTopicInfo = ntfyConfig.defaultTopic
? `Default topic: "${ntfyConfig.defaultTopic}"`
: "No default topic configured";
// Register the tool directly using the SDK pattern
server.tool("send_ntfy", `Send notifications to the user's devices using ntfy.sh service with support for titles, priorities, tags, attachments, and actions. Use this tool to externally notify the user of something important. ${defaultTopicInfo}.`, schemaWithLatestConfig.shape, async (params) => {
// Create request context for tracking this invocation
const toolRequestCtx = createRequestContext({
operation: 'handleNtfyTool',
topic: params?.topic
});
toolLogger.debug('Received tool invocation', {
requestId: toolRequestCtx.requestId,
topic: params?.topic
});
// Use ErrorHandler for consistent error handling
return await ErrorHandler.tryCatch(async () => {
// Process the notification
const response = await processNtfyMessage(params);
toolLogger.info('Successfully processed ntfy message', {
messageId: response.id,
topic: response.topic,
retries: response.retries
});
// Return in the standard MCP format
return {
content: [{
type: "text",
text: JSON.stringify(response, null, 2)
}]
};
}, {
operation: 'sending ntfy notification',
context: {
requestId: toolRequestCtx.requestId,
topic: params?.topic
},
input: sanitizeInputForLogging(params),
// Map errors appropriately
errorMapper: (error) => {
// Log the error
toolLogger.error('Error processing ntfy tool request', {
error: error instanceof Error ? error.message : 'Unknown error',
errorType: error instanceof Error ? error.name : 'Unknown',
topic: params?.topic
});
// Pass through McpErrors, map others properly
if (error instanceof McpError) {
return error;
}
// Try to classify unknown errors
if (error instanceof Error) {
const errorMsg = error.message.toLowerCase();
if (errorMsg.includes('validation') || errorMsg.includes('invalid')) {
return new McpError(BaseErrorCode.VALIDATION_ERROR, `Validation error: ${error.message}`);
}
else if (errorMsg.includes('not found') || errorMsg.includes('missing')) {
return new McpError(BaseErrorCode.NOT_FOUND, `Resource not found: ${error.message}`);
}
else if (errorMsg.includes('timeout')) {
return new McpError(BaseErrorCode.TIMEOUT, `Request timed out: ${error.message}`);
}
else if (errorMsg.includes('rate limit')) {
return new McpError(BaseErrorCode.RATE_LIMITED, `Rate limit exceeded: ${error.message}`);
}
}
// Default to service unavailable for network/connection issues
return new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, `Failed to send notification: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
});
});
toolLogger.info("Ntfy tool handler registered successfully");
}
catch (error) {
toolLogger.error("Failed to register ntfy tool", {
error: error instanceof Error ? error.message : String(error)
});
throw error; // Re-throw to propagate the error
}
};