UNPKG

ntfy-mcp-server

Version:

An MCP (Model Context Protocol) server designed to interact with the ntfy push notification service. It enables LLMs and AI agents to send notifications to your devices with extensive customization options.

150 lines (149 loc) 5.97 kB
import { randomBytes } from 'crypto'; import { BaseErrorCode, McpError } from '../types-global/errors.js'; import { logger } from './logger.js'; /** * Generic ID Generator class for creating and managing unique identifiers */ export class IdGenerator { /** * Constructor that accepts entity prefix configuration * @param entityPrefixes Map of entity types to their prefixes */ constructor(entityPrefixes = {}) { // Entity prefixes this.entityPrefixes = {}; // Reverse mapping for prefix to entity type lookup this.prefixToEntityType = {}; this.setEntityPrefixes(entityPrefixes); } /** * Set or update entity prefixes and rebuild the reverse lookup * @param entityPrefixes Map of entity types to their prefixes */ setEntityPrefixes(entityPrefixes) { this.entityPrefixes = { ...entityPrefixes }; // Rebuild reverse mapping this.prefixToEntityType = Object.entries(this.entityPrefixes).reduce((acc, [type, prefix]) => { acc[prefix] = type; acc[prefix.toLowerCase()] = type; return acc; }, {}); logger.debug('Entity prefixes updated', { entityPrefixes: this.entityPrefixes }); } /** * Get all registered entity prefixes * @returns The entity prefix configuration */ getEntityPrefixes() { return { ...this.entityPrefixes }; } /** * Generates a cryptographically secure random alphanumeric string * @param length The length of the random string to generate * @param charset Optional custom character set * @returns Random alphanumeric string */ generateRandomString(length = IdGenerator.DEFAULT_LENGTH, charset = IdGenerator.DEFAULT_CHARSET) { const bytes = randomBytes(length); let result = ''; for (let i = 0; i < length; i++) { result += charset[bytes[i] % charset.length]; } return result; } /** * Generates a unique ID with an optional prefix * @param prefix Optional prefix to add to the ID * @param options Optional generation options * @returns A unique identifier string */ generate(prefix, options = {}) { const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR, charset = IdGenerator.DEFAULT_CHARSET } = options; const randomPart = this.generateRandomString(length, charset); return prefix ? `${prefix}${separator}${randomPart}` : randomPart; } /** * Generates a custom ID for an entity with format PREFIX_XXXXXX * @param entityType The type of entity to generate an ID for * @param options Optional generation options * @returns A unique identifier string (e.g., "PROJ_A6B3J0") * @throws {McpError} If the entity type is not registered */ generateForEntity(entityType, options = {}) { const prefix = this.entityPrefixes[entityType]; if (!prefix) { throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Unknown entity type: ${entityType}`); } return this.generate(prefix, options); } /** * Validates if a given ID matches the expected format for an entity type * @param id The ID to validate * @param entityType The expected entity type * @param options Optional validation options * @returns boolean indicating if the ID is valid */ isValid(id, entityType, options = {}) { const prefix = this.entityPrefixes[entityType]; const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR } = options; if (!prefix) { return false; } const pattern = new RegExp(`^${prefix}${separator}[A-Z0-9]{${length}}$`); return pattern.test(id); } /** * Strips the prefix from an ID * @param id The ID to strip * @param separator Optional custom separator * @returns The ID without the prefix */ stripPrefix(id, separator = IdGenerator.DEFAULT_SEPARATOR) { return id.split(separator)[1] || id; } /** * Determines the entity type from an ID * @param id The ID to get the entity type for * @param separator Optional custom separator * @returns The entity type * @throws {McpError} If the ID format is invalid or entity type is unknown */ getEntityType(id, separator = IdGenerator.DEFAULT_SEPARATOR) { const parts = id.split(separator); if (parts.length !== 2 || !parts[0]) { throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid ID format: ${id}. Expected format: PREFIX${separator}XXXXXX`); } const prefix = parts[0]; const entityType = this.prefixToEntityType[prefix]; if (!entityType) { throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Unknown entity type prefix: ${prefix}`); } return entityType; } /** * Normalizes an entity ID to ensure consistent uppercase format * @param id The ID to normalize * @param separator Optional custom separator * @returns The normalized ID in uppercase format */ normalize(id, separator = IdGenerator.DEFAULT_SEPARATOR) { const entityType = this.getEntityType(id, separator); const idParts = id.split(separator); return `${this.entityPrefixes[entityType]}${separator}${idParts[1].toUpperCase()}`; } } // Default charset IdGenerator.DEFAULT_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; // Default separator IdGenerator.DEFAULT_SEPARATOR = '_'; // Default random part length IdGenerator.DEFAULT_LENGTH = 6; // Create and export a default instance with an empty entity prefix configuration export const idGenerator = new IdGenerator(); // For standalone use as a UUID generator export const generateUUID = () => { return crypto.randomUUID(); }; export default idGenerator;