ntfy-mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +423 -0
  3. package/dist/config/index.d.ts +23 -0
  4. package/dist/config/index.js +111 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +108 -0
  7. package/dist/mcp-server/resources/ntfyResource/getNtfyTopic.d.ts +2 -0
  8. package/dist/mcp-server/resources/ntfyResource/getNtfyTopic.js +111 -0
  9. package/dist/mcp-server/resources/ntfyResource/index.d.ts +12 -0
  10. package/dist/mcp-server/resources/ntfyResource/index.js +72 -0
  11. package/dist/mcp-server/resources/ntfyResource/types.d.ts +27 -0
  12. package/dist/mcp-server/resources/ntfyResource/types.js +8 -0
  13. package/dist/mcp-server/server.d.ts +40 -0
  14. package/dist/mcp-server/server.js +245 -0
  15. package/dist/mcp-server/tools/ntfyTool/index.d.ts +11 -0
  16. package/dist/mcp-server/tools/ntfyTool/index.js +110 -0
  17. package/dist/mcp-server/tools/ntfyTool/ntfyMessage.d.ts +9 -0
  18. package/dist/mcp-server/tools/ntfyTool/ntfyMessage.js +289 -0
  19. package/dist/mcp-server/tools/ntfyTool/types.d.ts +252 -0
  20. package/dist/mcp-server/tools/ntfyTool/types.js +144 -0
  21. package/dist/mcp-server/utils/registrationHelper.d.ts +48 -0
  22. package/dist/mcp-server/utils/registrationHelper.js +63 -0
  23. package/dist/services/ntfy/constants.d.ts +37 -0
  24. package/dist/services/ntfy/constants.js +37 -0
  25. package/dist/services/ntfy/errors.d.ts +79 -0
  26. package/dist/services/ntfy/errors.js +134 -0
  27. package/dist/services/ntfy/index.d.ts +33 -0
  28. package/dist/services/ntfy/index.js +56 -0
  29. package/dist/services/ntfy/publisher.d.ts +66 -0
  30. package/dist/services/ntfy/publisher.js +229 -0
  31. package/dist/services/ntfy/subscriber.d.ts +81 -0
  32. package/dist/services/ntfy/subscriber.js +502 -0
  33. package/dist/services/ntfy/types.d.ts +161 -0
  34. package/dist/services/ntfy/types.js +4 -0
  35. package/dist/services/ntfy/utils.d.ts +85 -0
  36. package/dist/services/ntfy/utils.js +410 -0
  37. package/dist/types-global/errors.d.ts +35 -0
  38. package/dist/types-global/errors.js +39 -0
  39. package/dist/types-global/mcp.d.ts +30 -0
  40. package/dist/types-global/mcp.js +25 -0
  41. package/dist/types-global/tool.d.ts +61 -0
  42. package/dist/types-global/tool.js +99 -0
  43. package/dist/utils/errorHandler.d.ts +98 -0
  44. package/dist/utils/errorHandler.js +271 -0
  45. package/dist/utils/idGenerator.d.ts +94 -0
  46. package/dist/utils/idGenerator.js +149 -0
  47. package/dist/utils/index.d.ts +13 -0
  48. package/dist/utils/index.js +16 -0
  49. package/dist/utils/logger.d.ts +36 -0
  50. package/dist/utils/logger.js +92 -0
  51. package/dist/utils/rateLimiter.d.ts +115 -0
  52. package/dist/utils/rateLimiter.js +180 -0
  53. package/dist/utils/requestContext.d.ts +68 -0
  54. package/dist/utils/requestContext.js +91 -0
  55. package/dist/utils/sanitization.d.ts +224 -0
  56. package/dist/utils/sanitization.js +367 -0
  57. package/dist/utils/security.d.ts +26 -0
  58. package/dist/utils/security.js +27 -0
  59. package/package.json +47 -0
@@ -0,0 +1,111 @@
1
+ import dotenv from 'dotenv';
2
+ import { createRequestContext } from '../utils/requestContext.js';
3
+ import { logger } from '../utils/logger.js';
4
+ // Initialize environment variables from .env file
5
+ dotenv.config();
6
+ // Create a request context for logging
7
+ const configContext = createRequestContext({
8
+ operation: 'ConfigInit',
9
+ component: 'Config',
10
+ });
11
+ // Create a logger specific to config
12
+ const configLogger = logger.createChildLogger({
13
+ module: 'Config',
14
+ service: 'Config',
15
+ requestId: configContext.requestId,
16
+ });
17
+ /**
18
+ * Environment validation and parsing utilities
19
+ */
20
+ const parsers = {
21
+ /**
22
+ * Parse environment string to number with validation
23
+ *
24
+ * @param value - String value from environment
25
+ * @param defaultValue - Default value to use if parsing fails
26
+ * @returns Parsed number value
27
+ */
28
+ number: (value, defaultValue) => {
29
+ if (!value)
30
+ return defaultValue;
31
+ const parsed = parseInt(value, 10);
32
+ if (isNaN(parsed)) {
33
+ configLogger.warn(`Invalid number for environment variable, using default: ${defaultValue}`, {
34
+ value,
35
+ defaultValue
36
+ });
37
+ return defaultValue;
38
+ }
39
+ return parsed;
40
+ },
41
+ /**
42
+ * Parse environment string to boolean
43
+ *
44
+ * @param value - String value from environment
45
+ * @param defaultValue - Default value to use if parsing fails
46
+ * @returns Parsed boolean value
47
+ */
48
+ boolean: (value, defaultValue) => {
49
+ if (!value)
50
+ return defaultValue;
51
+ const normalized = value.toLowerCase().trim();
52
+ if (['true', '1', 'yes', 'y'].includes(normalized))
53
+ return true;
54
+ if (['false', '0', 'no', 'n'].includes(normalized))
55
+ return false;
56
+ configLogger.warn(`Invalid boolean for environment variable, using default: ${defaultValue}`, {
57
+ value,
58
+ defaultValue
59
+ });
60
+ return defaultValue;
61
+ },
62
+ /**
63
+ * Parse environment string to an array of strings
64
+ *
65
+ * @param value - Comma-separated string value from environment
66
+ * @param defaultValue - Default value to use if parsing fails
67
+ * @returns Array of parsed string values
68
+ */
69
+ array: (value, defaultValue = []) => {
70
+ if (!value)
71
+ return defaultValue;
72
+ return value.split(',').map(item => item.trim()).filter(Boolean);
73
+ }
74
+ };
75
+ /**
76
+ * Environment variable configuration
77
+ */
78
+ export const config = {
79
+ environment: process.env.NODE_ENV || 'development',
80
+ logLevel: process.env.LOG_LEVEL || 'info',
81
+ // HTTP server configuration
82
+ server: {
83
+ port: parsers.number(process.env.PORT, 3000),
84
+ host: process.env.HOST || 'localhost',
85
+ },
86
+ // Rate limiting settings
87
+ rateLimit: {
88
+ windowMs: parsers.number(process.env.RATE_LIMIT_WINDOW_MS, 60000),
89
+ maxRequests: parsers.number(process.env.RATE_LIMIT_MAX_REQUESTS, 100),
90
+ },
91
+ // Ntfy notification service configuration
92
+ ntfy: {
93
+ baseUrl: process.env.NTFY_BASE_URL || 'https://ntfy.sh',
94
+ defaultTopic: process.env.NTFY_DEFAULT_TOPIC || '',
95
+ apiKey: process.env.NTFY_API_KEY || '',
96
+ maxMessageSize: parsers.number(process.env.NTFY_MAX_MESSAGE_SIZE, 4096),
97
+ maxRetries: parsers.number(process.env.NTFY_MAX_RETRIES, 3),
98
+ },
99
+ };
100
+ // Log the loaded configuration (excluding sensitive values)
101
+ configLogger.info('Configuration loaded', {
102
+ environment: config.environment,
103
+ logLevel: config.logLevel,
104
+ server: config.server,
105
+ ntfy: {
106
+ baseUrl: config.ntfy.baseUrl,
107
+ defaultTopic: config.ntfy.defaultTopic || '(not set)',
108
+ hasApiKey: !!config.ntfy.apiKey,
109
+ },
110
+ });
111
+ export default config;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Ntfy MCP Server - Main Entry Point
4
+ *
5
+ * This is the main entry point for the Ntfy MCP server. It initializes the
6
+ * server, sets up signal handlers for graceful shutdown, and manages the
7
+ * application lifecycle.
8
+ */
9
+ import { config } from "./config/index.js";
10
+ import { createMcpServer } from "./mcp-server/server.js";
11
+ import { logger } from "./utils/logger.js";
12
+ import { createRequestContext } from "./utils/requestContext.js";
13
+ // Create main application logger
14
+ const appLogger = logger.createChildLogger({
15
+ module: 'NtfyMcpServer',
16
+ service: 'NtfyMcpServer',
17
+ component: 'Main',
18
+ environment: config.environment
19
+ });
20
+ /**
21
+ * Graceful shutdown handler
22
+ * @param signal The signal that triggered the shutdown
23
+ */
24
+ const shutdown = async (signal) => {
25
+ appLogger.info(`Shutting down due to ${signal} signal...`);
26
+ try {
27
+ if (mcpServer) {
28
+ appLogger.info('Closing MCP server...');
29
+ await mcpServer.close();
30
+ appLogger.info('MCP server closed successfully');
31
+ }
32
+ appLogger.info('Shutdown complete. Exiting process.');
33
+ process.exit(0);
34
+ }
35
+ catch (error) {
36
+ appLogger.error('Error during shutdown', {
37
+ error: error instanceof Error ? error.message : String(error),
38
+ signal
39
+ });
40
+ process.exit(1);
41
+ }
42
+ };
43
+ // Variable to hold server instance
44
+ let mcpServer;
45
+ /**
46
+ * Main startup function
47
+ */
48
+ const start = async () => {
49
+ // Create startup context
50
+ const startupContext = createRequestContext({
51
+ operation: "ServerStartup",
52
+ appName: "ntfy-mcp-server",
53
+ environment: config.environment,
54
+ });
55
+ appLogger.info("Starting ntfy-mcp-server...", {
56
+ environment: config.environment,
57
+ logLevel: config.logLevel,
58
+ requestId: startupContext.requestId
59
+ });
60
+ try {
61
+ // Validate ntfy configuration
62
+ const ntfyConfig = config.ntfy;
63
+ if (!ntfyConfig.baseUrl) {
64
+ appLogger.warn("Ntfy base URL not configured. Using default https://ntfy.sh");
65
+ }
66
+ if (!ntfyConfig.defaultTopic) {
67
+ appLogger.warn("No default ntfy topic configured. Some functionality may be limited.");
68
+ }
69
+ // Create main MCP server
70
+ appLogger.info("Creating MCP server...");
71
+ mcpServer = await createMcpServer();
72
+ appLogger.info("MCP server created and connected successfully");
73
+ // Register signal handlers for graceful shutdown
74
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
75
+ process.on("SIGINT", () => shutdown("SIGINT"));
76
+ // Handle uncaught exceptions
77
+ process.on("uncaughtException", (error) => {
78
+ appLogger.error("Uncaught exception", {
79
+ error: error instanceof Error ? error.message : String(error),
80
+ stack: error instanceof Error ? error.stack : undefined
81
+ });
82
+ });
83
+ // Handle unhandled promise rejections
84
+ process.on("unhandledRejection", (reason) => {
85
+ appLogger.error("Unhandled promise rejection", {
86
+ reason: reason instanceof Error ? reason.message : String(reason),
87
+ stack: reason instanceof Error ? reason.stack : undefined
88
+ });
89
+ });
90
+ appLogger.info("Server startup complete. Ready to handle requests.");
91
+ }
92
+ catch (error) {
93
+ appLogger.error("Failed to start server", {
94
+ error: error instanceof Error ? error.message : String(error),
95
+ stack: error instanceof Error ? error.stack : undefined
96
+ });
97
+ // Exit with non-zero code to indicate error
98
+ process.exit(1);
99
+ }
100
+ };
101
+ // Start the application
102
+ start().catch((error) => {
103
+ appLogger.error("Fatal error during startup", {
104
+ error: error instanceof Error ? error.message : String(error),
105
+ stack: error instanceof Error ? error.stack : undefined
106
+ });
107
+ process.exit(1);
108
+ });
@@ -0,0 +1,2 @@
1
+ import { NtfyResourceResponse } from './types.js';
2
+ export declare const getNtfyTopic: (uri: URL) => Promise<NtfyResourceResponse>;
@@ -0,0 +1,111 @@
1
+ import { config } from '../../../config/index.js';
2
+ import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
3
+ import { ErrorHandler } from '../../../utils/errorHandler.js';
4
+ import { logger } from '../../../utils/logger.js';
5
+ import { createRequestContext } from '../../../utils/security.js';
6
+ // Create resource-specific logger
7
+ const resourceLogger = logger.createChildLogger({
8
+ module: 'NtfyResource',
9
+ service: 'NtfyResource'
10
+ });
11
+ export const getNtfyTopic = async (uri) => {
12
+ // Create a request context with unique ID
13
+ const requestContext = createRequestContext({
14
+ operation: 'getNtfyTopic',
15
+ uri: uri.toString()
16
+ });
17
+ const requestId = requestContext.requestId;
18
+ // Extract the topic from the URI pathname
19
+ const topic = uri.hostname || "";
20
+ resourceLogger.info("Ntfy resource request received", {
21
+ requestId,
22
+ uri: uri.href,
23
+ topic
24
+ });
25
+ return ErrorHandler.tryCatch(async () => {
26
+ // Get the default topic from configuration
27
+ const ntfyConfig = config.ntfy;
28
+ let defaultTopic = ntfyConfig.defaultTopic;
29
+ if (!defaultTopic) {
30
+ resourceLogger.warn("Default ntfy topic is not configured, using fallback value", {
31
+ requestId,
32
+ uri: uri.href
33
+ });
34
+ // Provide a fallback value instead of failing
35
+ defaultTopic = "ATLAS";
36
+ }
37
+ // Get recent messages asynchronously for this topic
38
+ let recentMessages = [];
39
+ try {
40
+ // Use a different topic for actual fetching based on whether this is default or not
41
+ const topicToFetch = topic === "default" ? defaultTopic : topic;
42
+ // Attempt to fetch the 10 most recent messages
43
+ const response = await fetch(`${config.ntfy.baseUrl || 'https://ntfy.sh'}/${topicToFetch}/json?poll=1&since=30d`, {
44
+ method: 'GET',
45
+ headers: {
46
+ 'Accept': 'application/json'
47
+ }
48
+ });
49
+ if (response.ok) {
50
+ // Parse response - each line is a separate JSON object
51
+ const text = await response.text();
52
+ const lines = text.split('\n').filter(line => line.trim());
53
+ // Parse each line as a JSON object and add to recent messages
54
+ recentMessages = lines.map(line => JSON.parse(line))
55
+ .filter(msg => msg.event === 'message')
56
+ .slice(0, 10); // Keep only the 10 most recent
57
+ resourceLogger.info(`Retrieved ${recentMessages.length} recent messages`, {
58
+ topic: topicToFetch,
59
+ requestId
60
+ });
61
+ }
62
+ }
63
+ catch (error) {
64
+ // Just log the error but don't fail the request
65
+ resourceLogger.warn(`Failed to fetch recent messages for topic`, {
66
+ topic,
67
+ error: error instanceof Error ? error.message : String(error),
68
+ requestId
69
+ });
70
+ }
71
+ // Handle the "default" topic case specially
72
+ const responseData = topic === "default" ?
73
+ {
74
+ defaultTopic,
75
+ timestamp: new Date().toISOString(),
76
+ requestUri: uri.href,
77
+ requestId,
78
+ recentMessages: recentMessages.length > 0 ? recentMessages : undefined
79
+ } :
80
+ {
81
+ topic,
82
+ timestamp: new Date().toISOString(),
83
+ requestUri: uri.href,
84
+ requestId,
85
+ recentMessages: recentMessages.length > 0 ? recentMessages : undefined
86
+ };
87
+ resourceLogger.info("Ntfy resource response data prepared", {
88
+ requestId,
89
+ responseData
90
+ });
91
+ // Return in the standard MCP format
92
+ const response = {
93
+ contents: [{
94
+ uri: uri.href,
95
+ text: JSON.stringify(responseData, null, 2),
96
+ mimeType: "application/json"
97
+ }]
98
+ };
99
+ return response;
100
+ }, {
101
+ context: {
102
+ requestId,
103
+ uri: uri.toString()
104
+ },
105
+ operation: 'processing ntfy resource request',
106
+ errorMapper: (error) => {
107
+ return new McpError(BaseErrorCode.INTERNAL_ERROR, `Error processing ntfy resource request: ${error instanceof Error ? error.message : 'Unknown error'}`, { requestId, uri: uri.toString() });
108
+ },
109
+ rethrow: true
110
+ });
111
+ };
@@ -0,0 +1,12 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * Register the ntfy resource with the MCP server
4
+ *
5
+ * This function creates and registers the ntfy resource which returns the default
6
+ * ntfy topic configured in the environment variables. It provides access to this
7
+ * configuration through a resource URI.
8
+ *
9
+ * @param server - The MCP server instance to register the resource with
10
+ * @returns Promise resolving when registration is complete
11
+ */
12
+ export declare const registerNtfyResource: (server: McpServer) => Promise<void>;
@@ -0,0 +1,72 @@
1
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
3
+ import { registerResource } from '../../utils/registrationHelper.js';
4
+ import { getNtfyTopic } from './getNtfyTopic.js';
5
+ /**
6
+ * Register the ntfy resource with the MCP server
7
+ *
8
+ * This function creates and registers the ntfy resource which returns the default
9
+ * ntfy topic configured in the environment variables. It provides access to this
10
+ * configuration through a resource URI.
11
+ *
12
+ * @param server - The MCP server instance to register the resource with
13
+ * @returns Promise resolving when registration is complete
14
+ */
15
+ export const registerNtfyResource = async (server) => {
16
+ return registerResource(server, { name: "ntfy-resource" }, async (server, resourceLogger) => {
17
+ // Create resource template
18
+ const template = new ResourceTemplate("ntfy://{topic}", {
19
+ // Simple list implementation
20
+ list: async () => ({
21
+ resources: [{
22
+ uri: "ntfy://default",
23
+ name: "Default Ntfy Topic",
24
+ description: "Returns the default ntfy topic configured in environment variables"
25
+ }]
26
+ }),
27
+ // No completion needed for this resource
28
+ complete: {}
29
+ });
30
+ // Register the resource
31
+ server.resource(
32
+ // Resource name
33
+ "ntfy-resource",
34
+ // Resource template
35
+ template,
36
+ // Resource metadata
37
+ {
38
+ name: "Ntfy Default Topic",
39
+ description: "Returns the default ntfy topic configured in environment variables",
40
+ mimeType: "application/json",
41
+ // No query parameters needed for this resource
42
+ // Examples
43
+ examples: [
44
+ {
45
+ name: "Default topic",
46
+ uri: "ntfy://default",
47
+ description: "Get the default ntfy topic"
48
+ }
49
+ ],
50
+ },
51
+ // Resource handler
52
+ async (uri, params) => {
53
+ // Extract the topic from the URI
54
+ const topic = params.topic;
55
+ resourceLogger.info(`Processing ntfy resource request for topic: ${topic}`, {
56
+ topic,
57
+ href: uri.href
58
+ });
59
+ // Check if the topic is valid
60
+ if (!topic) {
61
+ resourceLogger.error(`Missing topic in ntfy resource uri: ${uri.href}`, {
62
+ href: uri.href,
63
+ protocol: uri.protocol
64
+ });
65
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Resource not found: ${uri.href}`, { uri: uri.href });
66
+ }
67
+ // Process the request using our dedicated handler
68
+ return await getNtfyTopic(uri);
69
+ });
70
+ resourceLogger.info("Ntfy resource handler registered");
71
+ });
72
+ };
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Schema for validating ntfy resource query parameters
4
+ */
5
+ export declare const NtfyResourceQuerySchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
6
+ export type NtfyResourceQuery = z.infer<typeof NtfyResourceQuerySchema>;
7
+ /**
8
+ * Response type for the ntfy resource, matching MCP SDK expectations
9
+ */
10
+ export interface NtfyResourceResponse {
11
+ [key: string]: unknown;
12
+ contents: [
13
+ {
14
+ uri: string;
15
+ text: string;
16
+ mimeType: "application/json";
17
+ }
18
+ ];
19
+ }
20
+ /**
21
+ * Data structure for the ntfy response
22
+ */
23
+ export interface NtfyData {
24
+ defaultTopic: string;
25
+ timestamp: string;
26
+ requestUri: string;
27
+ }
@@ -0,0 +1,8 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Schema for validating ntfy resource query parameters
4
+ */
5
+ export const NtfyResourceQuerySchema = z.object({
6
+ // No parameters needed for default topic
7
+ }).describe('Query parameters for the ntfy resource.\n' +
8
+ 'URI Format: ntfy://default');
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ /**
4
+ * Server state management interface
5
+ */
6
+ export interface ServerState {
7
+ status: 'initializing' | 'running' | 'error' | 'degraded' | 'shutting_down' | 'shutdown';
8
+ startTime: Date;
9
+ lastHealthCheck: Date;
10
+ activeOperations: Map<string, {
11
+ operation: string;
12
+ startTime: Date;
13
+ }>;
14
+ errors: Array<{
15
+ timestamp: Date;
16
+ message: string;
17
+ code?: string;
18
+ }>;
19
+ registeredTools: Set<string>;
20
+ registeredResources: Set<string>;
21
+ failedRegistrations: Array<{
22
+ type: 'tool' | 'resource';
23
+ name: string;
24
+ error: any;
25
+ attempts: number;
26
+ }>;
27
+ requiredTools: Set<string>;
28
+ requiredResources: Set<string>;
29
+ }
30
+ /**
31
+ * Create and initialize an MCP server instance with all tools and resources
32
+ *
33
+ * This function configures the MCP server with security settings, tools, and resources.
34
+ * It connects the server to a transport (currently stdio) and returns the initialized
35
+ * server instance.
36
+ *
37
+ * @returns A promise that resolves to the initialized McpServer instance
38
+ * @throws {McpError} If the server fails to initialize
39
+ */
40
+ export declare const createMcpServer: () => Promise<McpServer>;