Fetch ntfy messages
ntfy_me_fetchFetch cached messages from an ntfy server topic. Use it to view recent notifications, search by content, title, tags, or priority, and check message history.
Instructions
Fetch cached messages from an ntfy server topic. Use this tool when the user asks to 'show notifications', 'get my messages', 'show my alerts', 'find notifications', 'search notifications', or any similar request. Great for finding recent notifications, checking message history, or searching for specific notifications by content, title, tags, or priority.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| url | No | Optional custom ntfy server URL (defaults to NTFY_URL env var or https://ntfy.sh) | |
| topic | No | Optional custom ntfy topic/channel to get messages from (defaults to NTFY_TOPIC env var) | |
| accessToken | No | Optional access token for authentication (defaults to NTFY_TOKEN env var) | |
| since | No | How far back to retrieve messages: timespan (e.g., '10m', '1h', '1d'), timestamp, message ID, or 'all' for all messages. Default: 10 minutes | |
| messageId | No | Find a specific message by its ID | |
| messageText | No | Find messages containing this exact text content | |
| messageTitle | No | Find messages with this exact title/subject | |
| priorities | No | Find messages with specific priority levels (min, low, default, high, max) | |
| tags | No | Find messages with specific tags (e.g., 'error', 'warning', 'success') |
Implementation Reference
- src/utils/toolHandlers.ts:150-244 (handler)The main handler function for the 'ntfy_me_fetch' tool. It takes filtering parameters (url, topic, accessToken, since, messageId, messageText, messageTitle, priorities, tags), calls fetchMessages, and returns formatted results with structured content.
async function handleFetchTool({ url: customUrl, topic: customTopic, accessToken, since, messageId, messageText, messageTitle, priorities, tags, }: FetchToolInput) { try { const url = customUrl || getDefaultUrl(); const topic = resolveTopic(customTopic); const token = accessToken || getDefaultToken(); const sinceSetting = since === null ? undefined : since || "10m"; validateNtfyUrl(url, "url"); const messageRecords = await fetchMessages({ url, topic, token, since: sinceSetting, messageId, messageText, messageTitle, priorities, tags, }); if (!messageRecords) { return { content: [ { type: "text" as const, text: `No messages found in topic ${topic}`, }, ], structuredContent: { success: true, topic, messages: [], }, }; } const messagesCount = Object.values(messageRecords).reduce( (sum, messages) => sum + messages.length, 0 ); const formattedMessages = Object.entries(messageRecords).map( ([recordTopic, messages]) => ({ type: "text" as const, text: `Topic: ${recordTopic}\nMessages: ${messages.length}\n${JSON.stringify( messages, null, 2 )}`, }) ); return { content: [ { type: "text" as const, text: `Successfully fetched ${messagesCount} message(s) from ${Object.keys( messageRecords ).length} topic(s)`, }, ...formattedMessages, ], structuredContent: { success: true, messageCount: messagesCount, topics: messageRecords, }, }; } catch (error: unknown) { const message = sanitizeErrorMessage(error, "Failed to fetch ntfy messages"); return { content: [ { type: "text" as const, text: message, }, ], structuredContent: { success: false, error: message, }, isError: true, }; } } - src/utils/messages.ts:21-153 (helper)The fetchMessages function that performs the actual HTTP request to the ntfy server. Builds the endpoint with poll=1 and since parameter, adds filter headers (X-ID, X-Message, X-Title, X-Priority, X-Tags), and processes the line-delimited JSON response into MessageData records.
export async function fetchMessages(options: NtfyFetchOptions): Promise<Record<string, MessageData[]> | null> { 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: Record<string, string> = {}; // 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: string[] = []; 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: MessageData[] = rawResponse .split('\n') // Split by newlines .filter((line: string) => line.trim().length > 0) // Remove empty lines .map((line: string) => { let parsedLine: unknown; 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: MessageData | null) => msg !== null) as MessageData[]; // Filter out invalid messages // Organize messages by topic const messageRecords: Record<string, MessageData[]> = {}; messageData.forEach((data) => { const topic = data.topic; if (!messageRecords[topic]) messageRecords[topic] = []; messageRecords[topic].push(data); }); return messageRecords; } catch (error: unknown) { logger.error('Failed to fetch messages from ntfy server.'); throw error; } } - src/schemas/fetchTool.schema.ts:5-47 (schema)Zod schema defining all input parameters for ntfy_me_fetch: url, topic, accessToken, since, messageId, messageText, messageTitle, priorities, and tags.
export const fetchToolInputSchema = z.object({ url: z .string() .optional() .describe( "Optional custom ntfy server URL (defaults to NTFY_URL env var or https://ntfy.sh)" ), topic: createOptionalNtfyTopicSchema( "Optional custom ntfy topic/channel to get messages from (defaults to NTFY_TOPIC env var)" ), accessToken: z .string() .optional() .describe( "Optional access token for authentication (defaults to NTFY_TOKEN env var)" ), since: z .union([z.string(), z.number()]) .optional() .describe( "How far back to retrieve messages: timespan (e.g., '10m', '1h', '1d'), timestamp, message ID, or 'all' for all messages. Default: 10 minutes" ), messageId: z.string().optional().describe("Find a specific message by its ID"), messageText: z .string() .optional() .describe("Find messages containing this exact text content"), messageTitle: z .string() .optional() .describe("Find messages with this exact title/subject"), priorities: createOptionalNtfyPrioritiesSchema( "Find messages with specific priority levels (min, low, default, high, max)" ), tags: z .union([z.string(), z.array(z.string())]) .optional() .describe( "Find messages with specific tags (e.g., 'error', 'warning', 'success')" ), }); export type FetchToolInput = z.infer<typeof fetchToolInputSchema>; - src/index.ts:105-110 (registration)Registration of the 'ntfy_me_fetch' tool on the MCP server with title 'Fetch ntfy messages', description, input schema reference, and the handleFetchTool handler.
server.registerTool("ntfy_me_fetch", { title: "Fetch ntfy messages", description: "Fetch cached messages from an ntfy server topic. Use this tool when the user asks to 'show notifications', 'get my messages', 'show my alerts', 'find notifications', 'search notifications', or any similar request. Great for finding recent notifications, checking message history, or searching for specific notifications by content, title, tags, or priority.", inputSchema: fetchToolInputSchema, }, handleFetchTool);