Skip to main content
Glama

Fetch ntfy messages

ntfy_me_fetch

Fetch 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

TableJSON Schema
NameRequiredDescriptionDefault
urlNoOptional custom ntfy server URL (defaults to NTFY_URL env var or https://ntfy.sh)
topicNoOptional custom ntfy topic/channel to get messages from (defaults to NTFY_TOPIC env var)
accessTokenNoOptional access token for authentication (defaults to NTFY_TOKEN env var)
sinceNoHow far back to retrieve messages: timespan (e.g., '10m', '1h', '1d'), timestamp, message ID, or 'all' for all messages. Default: 10 minutes
messageIdNoFind a specific message by its ID
messageTextNoFind messages containing this exact text content
messageTitleNoFind messages with this exact title/subject
prioritiesNoFind messages with specific priority levels (min, low, default, high, max)
tagsNoFind messages with specific tags (e.g., 'error', 'warning', 'success')

Implementation Reference

  • 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,
            };
        }
    }
  • 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;
      }
    }
  • 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);
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided. Description indicates a read-only fetch operation and mentions default parameter sources. Lacks details on rate limits, authentication requirements beyond token, or error handling, but basic transparency is present.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Description is four sentences with front-loaded main purpose and clear structure. Could be slightly more concise but is efficient overall.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Covers purpose, usage scenarios, and parameter defaults. No output schema, so return values are implicit. Additional context like output format would improve completeness, but it's adequate for the tool's simplicity.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Input schema has 9 parameters with 100% description coverage. The description adds context about defaults and usage but does not significantly go beyond the schema's own parameter descriptions, so baseline score of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly states the verb 'Fetch' and the resource 'cached messages from an ntfy server topic'. Provides specific example user requests, distinguishing from sibling tool (ntfy_me).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly lists when to use this tool with example phrases like 'show notifications' and 'get my messages'. Does not state when not to use or alternatives, but given only one sibling, the guidance is sufficient.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/gitmotion/ntfy-me-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server