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,134 @@
1
+ /**
2
+ * Custom error classes for the ntfy service
3
+ */
4
+ import { BaseErrorCode, McpError } from '../../types-global/errors.js';
5
+ import { ErrorHandler } from '../../utils/errorHandler.js';
6
+ /**
7
+ * Get message from an error object
8
+ * @param error The error to extract message from
9
+ * @returns Error message as string
10
+ */
11
+ function getErrorMessage(error) {
12
+ if (error instanceof Error) {
13
+ return error.message;
14
+ }
15
+ if (error === null) {
16
+ return 'Null error occurred';
17
+ }
18
+ if (error === undefined) {
19
+ return 'Undefined error occurred';
20
+ }
21
+ return typeof error === 'string'
22
+ ? error
23
+ : String(error);
24
+ }
25
+ /**
26
+ * Base error class for ntfy service errors
27
+ */
28
+ export class NtfyError extends McpError {
29
+ constructor(message, details) {
30
+ const errorCode = details?.errorCode || BaseErrorCode.SERVICE_UNAVAILABLE;
31
+ super(errorCode, message, details);
32
+ this.name = 'NtfyError';
33
+ }
34
+ }
35
+ /**
36
+ * Error thrown when connection to ntfy server fails
37
+ */
38
+ export class NtfyConnectionError extends NtfyError {
39
+ constructor(message, url) {
40
+ super(message, { url });
41
+ this.url = url;
42
+ this.name = 'NtfyConnectionError';
43
+ }
44
+ }
45
+ /**
46
+ * Error thrown when authentication fails
47
+ */
48
+ export class NtfyAuthenticationError extends NtfyError {
49
+ constructor(message) {
50
+ super(message, { errorCode: BaseErrorCode.UNAUTHORIZED });
51
+ this.name = 'NtfyAuthenticationError';
52
+ this.code = BaseErrorCode.UNAUTHORIZED; // Ensure code is set correctly
53
+ }
54
+ }
55
+ /**
56
+ * Error thrown when a message cannot be parsed
57
+ */
58
+ export class NtfyParseError extends NtfyError {
59
+ constructor(message, rawData) {
60
+ super(message, {
61
+ rawData: rawData?.substring(0, 100), // Truncate large data for logging
62
+ errorCode: BaseErrorCode.VALIDATION_ERROR
63
+ });
64
+ this.rawData = rawData;
65
+ this.name = 'NtfyParseError';
66
+ this.code = BaseErrorCode.VALIDATION_ERROR; // Ensure code is set correctly
67
+ }
68
+ }
69
+ /**
70
+ * Error thrown when a subscription is closed unexpectedly
71
+ */
72
+ export class NtfySubscriptionClosedError extends NtfyError {
73
+ constructor(message, reason) {
74
+ super(message, { reason });
75
+ this.reason = reason;
76
+ this.name = 'NtfySubscriptionClosedError';
77
+ }
78
+ }
79
+ /**
80
+ * Error thrown when an invalid topic name is provided
81
+ */
82
+ export class NtfyInvalidTopicError extends NtfyError {
83
+ constructor(message, topic) {
84
+ super(message, { topic, errorCode: BaseErrorCode.VALIDATION_ERROR });
85
+ this.topic = topic;
86
+ this.name = 'NtfyInvalidTopicError';
87
+ this.code = BaseErrorCode.VALIDATION_ERROR; // Ensure code is set correctly
88
+ }
89
+ }
90
+ /**
91
+ * Error thrown when a timeout occurs
92
+ */
93
+ export class NtfyTimeoutError extends NtfyError {
94
+ constructor(message, timeoutMs) {
95
+ super(message, { timeoutMs, errorCode: BaseErrorCode.TIMEOUT });
96
+ this.timeoutMs = timeoutMs;
97
+ this.name = 'NtfyTimeoutError';
98
+ this.code = BaseErrorCode.TIMEOUT; // Ensure code is set correctly
99
+ }
100
+ }
101
+ /**
102
+ * Error mapping for ntfy errors
103
+ */
104
+ export const NTFY_ERROR_MAPPINGS = [
105
+ {
106
+ pattern: /authentication|unauthorized|auth.*failed/i,
107
+ errorCode: BaseErrorCode.UNAUTHORIZED,
108
+ factory: (error) => new NtfyAuthenticationError(getErrorMessage(error))
109
+ },
110
+ {
111
+ pattern: /parse|invalid.*json|invalid.*format/i,
112
+ errorCode: BaseErrorCode.VALIDATION_ERROR,
113
+ factory: (error, context) => new NtfyParseError(getErrorMessage(error), context?.rawData)
114
+ },
115
+ {
116
+ pattern: /invalid.*topic/i,
117
+ errorCode: BaseErrorCode.VALIDATION_ERROR,
118
+ factory: (error, context) => new NtfyInvalidTopicError(getErrorMessage(error), context?.topic)
119
+ },
120
+ {
121
+ pattern: /timed out|timeout|deadline exceeded/i,
122
+ errorCode: BaseErrorCode.TIMEOUT,
123
+ factory: (error, context) => new NtfyTimeoutError(getErrorMessage(error), context?.timeoutMs)
124
+ },
125
+ {
126
+ pattern: /connection|network|failed to connect|refused/i,
127
+ errorCode: BaseErrorCode.SERVICE_UNAVAILABLE,
128
+ factory: (error, context) => new NtfyConnectionError(getErrorMessage(error), context?.url)
129
+ }
130
+ ];
131
+ /**
132
+ * Create an error mapper function for ntfy errors
133
+ */
134
+ export const ntfyErrorMapper = ErrorHandler.createErrorMapper(NTFY_ERROR_MAPPINGS, BaseErrorCode.SERVICE_UNAVAILABLE);
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Ntfy service for subscribing to and publishing messages to ntfy topics
3
+ *
4
+ * This module provides functionality to:
5
+ * 1. Subscribe to ntfy topics to receive notifications
6
+ * 2. Publish messages to ntfy topics
7
+ * 3. Utility functions for working with ntfy
8
+ */
9
+ import { NtfySubscriber } from './subscriber.js';
10
+ import { NtfySubscriptionHandlers, NtfySubscriptionOptions } from './types.js';
11
+ import { publish, NtfyPublishOptions, NtfyPublishResponse } from './publisher.js';
12
+ import { buildSubscriptionUrl, buildSubscriptionUrlSync, createBasicAuthHeader, createBasicAuthHeaderSync, isValidTopic, validateTopicSync } from './utils.js';
13
+ import { DEFAULT_NTFY_BASE_URL, DEFAULT_SUBSCRIPTION_OPTIONS, DEFAULT_REQUEST_TIMEOUT } from './constants.js';
14
+ export * from './types.js';
15
+ export * from './errors.js';
16
+ export { NtfySubscriber };
17
+ export { publish, NtfyPublishOptions, NtfyPublishResponse };
18
+ export { buildSubscriptionUrl, buildSubscriptionUrlSync, createBasicAuthHeader, createBasicAuthHeaderSync, isValidTopic, validateTopicSync };
19
+ export { DEFAULT_NTFY_BASE_URL, DEFAULT_SUBSCRIPTION_OPTIONS, DEFAULT_REQUEST_TIMEOUT };
20
+ /**
21
+ * Create a new ntfy subscriber with the given handlers
22
+ * @param handlers Event handlers for the subscription
23
+ * @returns A new NtfySubscriber instance
24
+ */
25
+ export declare function createSubscriber(handlers?: NtfySubscriptionHandlers): NtfySubscriber;
26
+ /**
27
+ * Subscribe to a ntfy topic
28
+ * @param topic Topic to subscribe to
29
+ * @param handlers Event handlers for the subscription
30
+ * @param options Subscription options
31
+ * @returns A function to unsubscribe
32
+ */
33
+ export declare function subscribe(topic: string, handlers: NtfySubscriptionHandlers, options?: NtfySubscriptionOptions): Promise<() => void>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Ntfy service for subscribing to and publishing messages to ntfy topics
3
+ *
4
+ * This module provides functionality to:
5
+ * 1. Subscribe to ntfy topics to receive notifications
6
+ * 2. Publish messages to ntfy topics
7
+ * 3. Utility functions for working with ntfy
8
+ */
9
+ import { NtfySubscriber } from './subscriber.js';
10
+ import { publish } from './publisher.js';
11
+ import { buildSubscriptionUrl, buildSubscriptionUrlSync, createBasicAuthHeader, createBasicAuthHeaderSync, isValidTopic, validateTopicSync } from './utils.js';
12
+ import { createRequestContext } from '../../utils/requestContext.js';
13
+ import { idGenerator } from '../../utils/idGenerator.js';
14
+ import { DEFAULT_NTFY_BASE_URL, DEFAULT_SUBSCRIPTION_OPTIONS, DEFAULT_REQUEST_TIMEOUT } from './constants.js';
15
+ // Export types
16
+ export * from './types.js';
17
+ export * from './errors.js';
18
+ // Export main classes
19
+ export { NtfySubscriber };
20
+ // Export publisher functions
21
+ export { publish };
22
+ // Export utility functions
23
+ export {
24
+ // Export both sync and async versions of utilities
25
+ buildSubscriptionUrl, buildSubscriptionUrlSync, createBasicAuthHeader, createBasicAuthHeaderSync, isValidTopic, validateTopicSync };
26
+ // Export constants
27
+ export { DEFAULT_NTFY_BASE_URL, DEFAULT_SUBSCRIPTION_OPTIONS, DEFAULT_REQUEST_TIMEOUT };
28
+ /**
29
+ * Create a new ntfy subscriber with the given handlers
30
+ * @param handlers Event handlers for the subscription
31
+ * @returns A new NtfySubscriber instance
32
+ */
33
+ export function createSubscriber(handlers = {}) {
34
+ const requestCtx = createRequestContext({
35
+ operation: 'createSubscriber',
36
+ subscriberId: idGenerator.generateRandomString(8)
37
+ });
38
+ return new NtfySubscriber(handlers);
39
+ }
40
+ /**
41
+ * Subscribe to a ntfy topic
42
+ * @param topic Topic to subscribe to
43
+ * @param handlers Event handlers for the subscription
44
+ * @param options Subscription options
45
+ * @returns A function to unsubscribe
46
+ */
47
+ export async function subscribe(topic, handlers, options = {}) {
48
+ const requestCtx = createRequestContext({
49
+ operation: 'subscribe',
50
+ topic,
51
+ subscriberId: idGenerator.generateRandomString(8)
52
+ });
53
+ const subscriber = new NtfySubscriber(handlers);
54
+ await subscriber.subscribe(topic, options);
55
+ return () => subscriber.unsubscribe();
56
+ }
@@ -0,0 +1,66 @@
1
+ import { NtfyAction, NtfyAttachment, NtfyPriority } from './types.js';
2
+ /**
3
+ * Options for publishing to ntfy topics
4
+ */
5
+ export interface NtfyPublishOptions {
6
+ /** Base URL for the ntfy server */
7
+ baseUrl?: string;
8
+ /** Authentication token */
9
+ auth?: string;
10
+ /** Basic auth username */
11
+ username?: string;
12
+ /** Basic auth password */
13
+ password?: string;
14
+ /** Additional headers to include in requests */
15
+ headers?: Record<string, string>;
16
+ /** Message title */
17
+ title?: string;
18
+ /** Message tags (emojis) */
19
+ tags?: string[];
20
+ /** Message priority (1-5) */
21
+ priority?: NtfyPriority;
22
+ /** URL to open when notification is clicked */
23
+ click?: string;
24
+ /** Message actions (buttons, etc.) */
25
+ actions?: NtfyAction[];
26
+ /** Message attachment */
27
+ attachment?: NtfyAttachment;
28
+ /** Email addresses to send the notification to */
29
+ email?: string;
30
+ /** Delay the message for a specific time (e.g., 30m, 1h, tomorrow) */
31
+ delay?: string;
32
+ /** Cache the message for a specific duration (e.g., 10m, 1h, 1d) */
33
+ cache?: string;
34
+ /** Firebase Cloud Messaging (FCM) topic to forward to */
35
+ firebase?: string;
36
+ /** Unique ID for the message */
37
+ id?: string;
38
+ /** Message expiration (e.g., 10m, 1h, 1d) */
39
+ expires?: string;
40
+ /** Whether the message should be X-Forwarded */
41
+ markdown?: boolean;
42
+ }
43
+ /**
44
+ * Response from publishing to ntfy
45
+ */
46
+ export interface NtfyPublishResponse {
47
+ /** Server-assigned message ID */
48
+ id: string;
49
+ /** Time the message was received */
50
+ time: number;
51
+ /** Message expiration timestamp (if set) */
52
+ expires?: number;
53
+ /** Topic the message was published to */
54
+ topic: string;
55
+ }
56
+ /**
57
+ * Publish a message to a ntfy topic
58
+ *
59
+ * @param topic - Topic to publish to
60
+ * @param message - Message to publish
61
+ * @param options - Publishing options
62
+ * @returns Promise resolving to the publish response
63
+ * @throws NtfyInvalidTopicError if the topic name is invalid
64
+ * @throws NtfyConnectionError if the connection fails
65
+ */
66
+ export declare function publish(topic: string, message: string, options?: NtfyPublishOptions): Promise<NtfyPublishResponse>;
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Ntfy publisher implementation for sending notifications
3
+ */
4
+ import { DEFAULT_NTFY_BASE_URL, DEFAULT_REQUEST_TIMEOUT, ERROR_MESSAGES } from './constants.js';
5
+ import { NtfyAuthenticationError, NtfyConnectionError, NtfyInvalidTopicError, ntfyErrorMapper } from './errors.js';
6
+ import { createTimeout, validateTopicSync, createRequestHeadersSync } from './utils.js';
7
+ import { BaseErrorCode, McpError } from '../../types-global/errors.js';
8
+ import { ErrorHandler } from '../../utils/errorHandler.js';
9
+ import { logger } from '../../utils/logger.js';
10
+ import { sanitizeInput, sanitizeInputForLogging } from '../../utils/sanitization.js';
11
+ import { createRequestContext } from '../../utils/requestContext.js';
12
+ import { idGenerator } from '../../utils/idGenerator.js';
13
+ // Create a module-specific logger
14
+ const publisherLogger = logger.createChildLogger({
15
+ module: 'NtfyPublisher',
16
+ serviceId: idGenerator.generateRandomString(8)
17
+ });
18
+ /**
19
+ * Publish a message to a ntfy topic
20
+ *
21
+ * @param topic - Topic to publish to
22
+ * @param message - Message to publish
23
+ * @param options - Publishing options
24
+ * @returns Promise resolving to the publish response
25
+ * @throws NtfyInvalidTopicError if the topic name is invalid
26
+ * @throws NtfyConnectionError if the connection fails
27
+ */
28
+ export async function publish(topic, message, options = {}) {
29
+ return ErrorHandler.tryCatch(async () => {
30
+ // Create request context for tracking
31
+ const requestCtx = createRequestContext({
32
+ operation: 'publishNtfyMessage',
33
+ topic,
34
+ messageLength: message?.length,
35
+ hasTitle: !!options.title,
36
+ hasTags: Array.isArray(options.tags) && options.tags.length > 0,
37
+ priority: options.priority,
38
+ publishId: idGenerator.generateRandomString(8)
39
+ });
40
+ publisherLogger.info('Publishing message', {
41
+ topic,
42
+ messageLength: message?.length,
43
+ hasTitle: !!options.title,
44
+ hasTags: Array.isArray(options.tags) && options.tags.length > 0,
45
+ priority: options.priority,
46
+ requestId: requestCtx.requestId
47
+ });
48
+ // Validate topic synchronously for better performance
49
+ if (!validateTopicSync(topic)) {
50
+ publisherLogger.error('Invalid topic name', {
51
+ topic,
52
+ requestId: requestCtx.requestId
53
+ });
54
+ throw new NtfyInvalidTopicError(ERROR_MESSAGES.INVALID_TOPIC, topic);
55
+ }
56
+ // Build URL
57
+ const baseUrl = sanitizeInput.url(options.baseUrl || DEFAULT_NTFY_BASE_URL);
58
+ const url = `${baseUrl}/${sanitizeInput.string(topic)}`;
59
+ publisherLogger.debug('Publishing to URL', {
60
+ url,
61
+ requestId: requestCtx.requestId
62
+ });
63
+ // Prepare headers - using sync version for performance
64
+ const initialHeaders = createRequestHeadersSync({
65
+ auth: options.auth,
66
+ username: options.username,
67
+ password: options.password,
68
+ headers: options.headers,
69
+ });
70
+ // Convert HeadersInit to a Record for easier manipulation
71
+ const headers = {};
72
+ // Copy initial headers to our record object
73
+ if (initialHeaders instanceof Headers) {
74
+ initialHeaders.forEach((value, key) => {
75
+ headers[key] = value;
76
+ });
77
+ }
78
+ else if (Array.isArray(initialHeaders)) {
79
+ for (const [key, value] of initialHeaders) {
80
+ headers[key] = value;
81
+ }
82
+ }
83
+ else if (initialHeaders) {
84
+ Object.assign(headers, initialHeaders);
85
+ }
86
+ // Set content type
87
+ headers['Content-Type'] = 'text/plain';
88
+ // Add special headers for ntfy features
89
+ if (options.title) {
90
+ headers['X-Title'] = sanitizeInput.string(options.title);
91
+ }
92
+ if (options.tags && options.tags.length > 0) {
93
+ // Sanitize each tag
94
+ const sanitizedTags = options.tags.map(tag => sanitizeInput.string(tag));
95
+ headers['X-Tags'] = sanitizedTags.join(',');
96
+ }
97
+ if (options.priority) {
98
+ headers['X-Priority'] = options.priority.toString();
99
+ }
100
+ if (options.click) {
101
+ headers['X-Click'] = sanitizeInput.url(options.click);
102
+ }
103
+ if (options.actions && options.actions.length > 0) {
104
+ // We need to sanitize the actions
105
+ const sanitizedActions = options.actions.map(action => ({
106
+ id: sanitizeInput.string(action.id),
107
+ label: sanitizeInput.string(action.label),
108
+ action: sanitizeInput.string(action.action),
109
+ url: action.url ? sanitizeInput.url(action.url) : undefined,
110
+ method: action.method ? sanitizeInput.string(action.method) : undefined,
111
+ headers: action.headers,
112
+ body: action.body ? sanitizeInput.string(action.body) : undefined,
113
+ clear: action.clear
114
+ }));
115
+ headers['X-Actions'] = JSON.stringify(sanitizedActions);
116
+ }
117
+ if (options.attachment) {
118
+ headers['X-Attach'] = sanitizeInput.url(options.attachment.url);
119
+ if (options.attachment.name) {
120
+ headers['X-Filename'] = sanitizeInput.string(options.attachment.name);
121
+ }
122
+ }
123
+ if (options.email) {
124
+ headers['X-Email'] = sanitizeInput.string(options.email);
125
+ }
126
+ if (options.delay) {
127
+ headers['X-Delay'] = sanitizeInput.string(options.delay);
128
+ }
129
+ if (options.cache) {
130
+ headers['X-Cache'] = sanitizeInput.string(options.cache);
131
+ }
132
+ if (options.firebase) {
133
+ headers['X-Firebase'] = sanitizeInput.string(options.firebase);
134
+ }
135
+ if (options.id) {
136
+ headers['X-ID'] = sanitizeInput.string(options.id);
137
+ }
138
+ if (options.expires) {
139
+ headers['X-Expires'] = sanitizeInput.string(options.expires);
140
+ }
141
+ if (options.markdown) {
142
+ headers['X-Markdown'] = 'true';
143
+ }
144
+ // Send request with timeout
145
+ const controller = new AbortController();
146
+ const timeoutId = setTimeout(() => controller.abort(), DEFAULT_REQUEST_TIMEOUT);
147
+ try {
148
+ publisherLogger.debug('Sending HTTP request', {
149
+ url,
150
+ method: 'POST',
151
+ requestId: requestCtx.requestId
152
+ });
153
+ const response = await Promise.race([
154
+ fetch(url, {
155
+ method: 'POST',
156
+ headers,
157
+ body: message,
158
+ signal: controller.signal,
159
+ }),
160
+ createTimeout(DEFAULT_REQUEST_TIMEOUT),
161
+ ]);
162
+ clearTimeout(timeoutId);
163
+ // Check response status
164
+ if (!response.ok) {
165
+ publisherLogger.error('HTTP error from ntfy server', {
166
+ status: response.status,
167
+ statusText: response.statusText,
168
+ url,
169
+ requestId: requestCtx.requestId
170
+ });
171
+ // Provide more specific error messages based on status code
172
+ let errorMessage = `HTTP Error: ${response.status} ${response.statusText}`;
173
+ switch (response.status) {
174
+ case 401:
175
+ errorMessage = 'Authentication failed: invalid credentials';
176
+ throw new NtfyAuthenticationError(errorMessage);
177
+ case 403:
178
+ errorMessage = 'Access forbidden: insufficient permissions';
179
+ throw new McpError(BaseErrorCode.FORBIDDEN, errorMessage, { url, statusCode: response.status });
180
+ case 404:
181
+ errorMessage = 'Topic or resource not found';
182
+ throw new McpError(BaseErrorCode.NOT_FOUND, errorMessage, { url, statusCode: response.status, topic });
183
+ case 429:
184
+ errorMessage = 'Too many requests: rate limit exceeded';
185
+ throw new McpError(BaseErrorCode.RATE_LIMITED, errorMessage, { url, statusCode: response.status });
186
+ case 500:
187
+ case 502:
188
+ case 503:
189
+ case 504:
190
+ errorMessage = `Server error: ${response.statusText}`;
191
+ // Fall through to default error handling
192
+ default:
193
+ throw new NtfyConnectionError(errorMessage, url);
194
+ }
195
+ }
196
+ // Parse response
197
+ const result = await response.json();
198
+ publisherLogger.info('Message published successfully', {
199
+ messageId: result.id,
200
+ topic: result.topic,
201
+ requestId: requestCtx.requestId
202
+ });
203
+ return result;
204
+ }
205
+ catch (error) {
206
+ clearTimeout(timeoutId);
207
+ if (error instanceof NtfyInvalidTopicError) {
208
+ throw error;
209
+ }
210
+ publisherLogger.error('Failed to publish message', {
211
+ error: error instanceof Error ? error.message : String(error),
212
+ topic,
213
+ url,
214
+ requestId: requestCtx.requestId
215
+ });
216
+ throw new NtfyConnectionError(`Error publishing to topic: ${error instanceof Error ? error.message : String(error)}`, url);
217
+ }
218
+ }, {
219
+ operation: 'publishNtfyMessage',
220
+ context: { topic },
221
+ input: {
222
+ message: message?.length > 100 ? `${message.substring(0, 100)}...` : message,
223
+ options: sanitizeInputForLogging(options)
224
+ },
225
+ errorCode: BaseErrorCode.SERVICE_UNAVAILABLE,
226
+ errorMapper: ntfyErrorMapper,
227
+ rethrow: true
228
+ });
229
+ }
@@ -0,0 +1,81 @@
1
+ import { NtfySubscriptionHandlers, NtfySubscriptionOptions } from './types.js';
2
+ /**
3
+ * NtfySubscriber class for subscribing to ntfy topics
4
+ */
5
+ export declare class NtfySubscriber {
6
+ private handlers;
7
+ private abortController?;
8
+ private cleanupFn?;
9
+ private connectionActive;
10
+ private lastKeepaliveTime;
11
+ private reconnectAttempts;
12
+ private keepaliveCheckInterval?;
13
+ private logger;
14
+ private subscriberId;
15
+ private currentTopic?;
16
+ /**
17
+ * Creates a new NtfySubscriber instance
18
+ * @param handlers Event handlers for the subscription
19
+ */
20
+ constructor(handlers?: NtfySubscriptionHandlers);
21
+ /**
22
+ * Subscribe to a ntfy topic
23
+ * @param topic Topic to subscribe to (can be comma-separated for multiple topics)
24
+ * @param options Subscription options
25
+ * @returns Promise that resolves when the subscription is established
26
+ * @throws NtfyInvalidTopicError if the topic name is invalid
27
+ * @throws NtfyConnectionError if the connection fails
28
+ */
29
+ subscribe(topic: string, options?: NtfySubscriptionOptions): Promise<void>;
30
+ /**
31
+ * Unsubscribe from the current topic
32
+ */
33
+ unsubscribe(): void;
34
+ /**
35
+ * Start a subscription to a topic
36
+ * @param topic Topic to subscribe to
37
+ * @param format Format to subscribe in (json, sse, raw, ws)
38
+ * @param options Subscription options
39
+ */
40
+ private startSubscription;
41
+ /**
42
+ * Process a JSON stream from ntfy
43
+ * @param reader ReadableStreamDefaultReader to read from
44
+ * @param requestId Request ID for logging
45
+ */
46
+ private processJsonStream;
47
+ /**
48
+ * Handle a message from ntfy
49
+ * @param message Message from ntfy
50
+ * @param requestId Request ID for logging
51
+ */
52
+ private handleMessage;
53
+ /**
54
+ * Handle a parse error
55
+ * @param error Error that occurred
56
+ * @param rawData Raw data that caused the error
57
+ * @param requestId Request ID for logging
58
+ */
59
+ private handleParseError;
60
+ /**
61
+ * Handle a subscription error
62
+ * @param error Error that occurred
63
+ * @param requestId Request ID for logging
64
+ */
65
+ private handleSubscriptionError;
66
+ /**
67
+ * Start the keepalive check interval
68
+ */
69
+ private startKeepaliveCheck;
70
+ /**
71
+ * Stop the keepalive check interval
72
+ */
73
+ private stopKeepaliveCheck;
74
+ /**
75
+ * Schedule a reconnection attempt
76
+ * @param topic Topic to reconnect to
77
+ * @param format Format to reconnect with
78
+ * @param options Subscription options
79
+ */
80
+ private scheduleReconnect;
81
+ }