ntfy-me-mcp
Version:
An ntfy MCP server for sending ntfy notifications to your self-hosted ntfy server from AI Agents 📤 (supports secure token auth & more - use with npx or docker!)
127 lines (126 loc) • 5.29 kB
JavaScript
import fetch from 'node-fetch';
import { Logger } from './logger.js';
import { messageDataSchema, } from '../schemas/messageData.schema.js';
import { ntfyFetchOptionsSchema, } from '../schemas/ntfyFetchOptions.schema.js';
import { validateNtfyTopic, validateNtfyUrl } from './validation.js';
const logger = Logger.getInstance();
/**
* Fetches cached messages from an ntfy server
*
* @param options Configuration options for the fetch operation
* @returns A record of message arrays organized by topic
*/
export async function fetchMessages(options) {
try {
// Validate the URL to prevent prompt injection via malicious URL values
validateNtfyUrl(options.url, "url");
const topic = validateNtfyTopic(options.topic, 'topic');
const parsedOptions = ntfyFetchOptionsSchema.parse({
...options,
topic,
});
// Prepare the URL with proper handling of trailing slashes
const baseUrl = parsedOptions.url.endsWith("/")
? parsedOptions.url.slice(0, -1)
: parsedOptions.url;
// Start with the basic endpoint
let endpoint = `${baseUrl}/${topic}/json?poll=1`;
// Add the since parameter if provided
if (parsedOptions.since !== undefined && parsedOptions.since !== null) {
endpoint += `&since=${parsedOptions.since}`;
}
// Prepare headers
const headers = {};
// Add authorization if token is provided
if (parsedOptions.token) {
headers.Authorization = `Bearer ${parsedOptions.token}`;
}
// Add filter headers if provided
if (parsedOptions.messageId) {
headers['X-ID'] = parsedOptions.messageId;
}
if (parsedOptions.messageText) {
headers['X-Message'] = parsedOptions.messageText;
}
if (parsedOptions.messageTitle) {
headers['X-Title'] = parsedOptions.messageTitle;
}
if (parsedOptions.priorities) {
// Handle both string and string[] formats
const priorityValue = Array.isArray(parsedOptions.priorities)
? parsedOptions.priorities.join(',')
: parsedOptions.priorities;
headers['X-Priority'] = priorityValue;
}
if (parsedOptions.tags) {
// Handle both string and string[] formats
const tagsValue = Array.isArray(parsedOptions.tags)
? parsedOptions.tags.join(',')
: parsedOptions.tags;
headers['X-Tags'] = tagsValue;
}
// Log helpful message with filter information
const appliedFilters = [];
if (parsedOptions.messageId)
appliedFilters.push('messageId');
if (parsedOptions.messageTitle)
appliedFilters.push('messageTitle');
if (parsedOptions.messageText)
appliedFilters.push('messageText');
if (parsedOptions.priorities)
appliedFilters.push('priorities');
if (parsedOptions.tags)
appliedFilters.push('tags');
logger.info(`Fetching messages for topic ${topic}` +
`${appliedFilters.length > 0 ? ` with filters: ${appliedFilters.join(', ')}` : ''}`);
// Make the API call
const response = await fetch(endpoint, { headers });
if (!response.ok) {
// Handle authentication errors
if (response.status === 401 || response.status === 403) {
throw new Error('Authentication failed when fetching messages. ' +
`This ntfy topic requires an access token.`);
}
// Handle other errors
throw new Error(`Failed to fetch ntfy messages. Status code: ${response.status}`);
}
// Get the raw response data
const rawResponse = await response.text();
if (!rawResponse)
return null;
// Process the response as line-delimited JSON
const messageData = rawResponse
.split('\n') // Split by newlines
.filter((line) => line.trim().length > 0) // Remove empty lines
.map((line) => {
let parsedLine;
try {
parsedLine = JSON.parse(line); // Parse each line as JSON
}
catch {
logger.warn('Skipping invalid JSON line returned by ntfy server.');
return null; // Skip invalid JSON lines
}
const parsedMessage = messageDataSchema.safeParse(parsedLine);
if (!parsedMessage.success) {
logger.warn('Skipping invalid message payload returned by ntfy server.');
return null;
}
return parsedMessage.data;
})
.filter((msg) => msg !== null); // Filter out invalid messages
// Organize messages by topic
const messageRecords = {};
messageData.forEach((data) => {
const topic = data.topic;
if (!messageRecords[topic])
messageRecords[topic] = [];
messageRecords[topic].push(data);
});
return messageRecords;
}
catch (error) {
logger.error('Failed to fetch messages from ntfy server.');
throw error;
}
}