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.
- package/LICENSE +201 -0
- package/README.md +423 -0
- package/dist/config/index.d.ts +23 -0
- package/dist/config/index.js +111 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +108 -0
- package/dist/mcp-server/resources/ntfyResource/getNtfyTopic.d.ts +2 -0
- package/dist/mcp-server/resources/ntfyResource/getNtfyTopic.js +111 -0
- package/dist/mcp-server/resources/ntfyResource/index.d.ts +12 -0
- package/dist/mcp-server/resources/ntfyResource/index.js +72 -0
- package/dist/mcp-server/resources/ntfyResource/types.d.ts +27 -0
- package/dist/mcp-server/resources/ntfyResource/types.js +8 -0
- package/dist/mcp-server/server.d.ts +40 -0
- package/dist/mcp-server/server.js +245 -0
- package/dist/mcp-server/tools/ntfyTool/index.d.ts +11 -0
- package/dist/mcp-server/tools/ntfyTool/index.js +110 -0
- package/dist/mcp-server/tools/ntfyTool/ntfyMessage.d.ts +9 -0
- package/dist/mcp-server/tools/ntfyTool/ntfyMessage.js +289 -0
- package/dist/mcp-server/tools/ntfyTool/types.d.ts +252 -0
- package/dist/mcp-server/tools/ntfyTool/types.js +144 -0
- package/dist/mcp-server/utils/registrationHelper.d.ts +48 -0
- package/dist/mcp-server/utils/registrationHelper.js +63 -0
- package/dist/services/ntfy/constants.d.ts +37 -0
- package/dist/services/ntfy/constants.js +37 -0
- package/dist/services/ntfy/errors.d.ts +79 -0
- package/dist/services/ntfy/errors.js +134 -0
- package/dist/services/ntfy/index.d.ts +33 -0
- package/dist/services/ntfy/index.js +56 -0
- package/dist/services/ntfy/publisher.d.ts +66 -0
- package/dist/services/ntfy/publisher.js +229 -0
- package/dist/services/ntfy/subscriber.d.ts +81 -0
- package/dist/services/ntfy/subscriber.js +502 -0
- package/dist/services/ntfy/types.d.ts +161 -0
- package/dist/services/ntfy/types.js +4 -0
- package/dist/services/ntfy/utils.d.ts +85 -0
- package/dist/services/ntfy/utils.js +410 -0
- package/dist/types-global/errors.d.ts +35 -0
- package/dist/types-global/errors.js +39 -0
- package/dist/types-global/mcp.d.ts +30 -0
- package/dist/types-global/mcp.js +25 -0
- package/dist/types-global/tool.d.ts +61 -0
- package/dist/types-global/tool.js +99 -0
- package/dist/utils/errorHandler.d.ts +98 -0
- package/dist/utils/errorHandler.js +271 -0
- package/dist/utils/idGenerator.d.ts +94 -0
- package/dist/utils/idGenerator.js +149 -0
- package/dist/utils/index.d.ts +13 -0
- package/dist/utils/index.js +16 -0
- package/dist/utils/logger.d.ts +36 -0
- package/dist/utils/logger.js +92 -0
- package/dist/utils/rateLimiter.d.ts +115 -0
- package/dist/utils/rateLimiter.js +180 -0
- package/dist/utils/requestContext.d.ts +68 -0
- package/dist/utils/requestContext.js +91 -0
- package/dist/utils/sanitization.d.ts +224 -0
- package/dist/utils/sanitization.js +367 -0
- package/dist/utils/security.d.ts +26 -0
- package/dist/utils/security.js +27 -0
- package/package.json +47 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
import { createRequestContext } from '../utils/requestContext.js';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
// Initialize environment variables from .env file
|
|
5
|
+
dotenv.config();
|
|
6
|
+
// Create a request context for logging
|
|
7
|
+
const configContext = createRequestContext({
|
|
8
|
+
operation: 'ConfigInit',
|
|
9
|
+
component: 'Config',
|
|
10
|
+
});
|
|
11
|
+
// Create a logger specific to config
|
|
12
|
+
const configLogger = logger.createChildLogger({
|
|
13
|
+
module: 'Config',
|
|
14
|
+
service: 'Config',
|
|
15
|
+
requestId: configContext.requestId,
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Environment validation and parsing utilities
|
|
19
|
+
*/
|
|
20
|
+
const parsers = {
|
|
21
|
+
/**
|
|
22
|
+
* Parse environment string to number with validation
|
|
23
|
+
*
|
|
24
|
+
* @param value - String value from environment
|
|
25
|
+
* @param defaultValue - Default value to use if parsing fails
|
|
26
|
+
* @returns Parsed number value
|
|
27
|
+
*/
|
|
28
|
+
number: (value, defaultValue) => {
|
|
29
|
+
if (!value)
|
|
30
|
+
return defaultValue;
|
|
31
|
+
const parsed = parseInt(value, 10);
|
|
32
|
+
if (isNaN(parsed)) {
|
|
33
|
+
configLogger.warn(`Invalid number for environment variable, using default: ${defaultValue}`, {
|
|
34
|
+
value,
|
|
35
|
+
defaultValue
|
|
36
|
+
});
|
|
37
|
+
return defaultValue;
|
|
38
|
+
}
|
|
39
|
+
return parsed;
|
|
40
|
+
},
|
|
41
|
+
/**
|
|
42
|
+
* Parse environment string to boolean
|
|
43
|
+
*
|
|
44
|
+
* @param value - String value from environment
|
|
45
|
+
* @param defaultValue - Default value to use if parsing fails
|
|
46
|
+
* @returns Parsed boolean value
|
|
47
|
+
*/
|
|
48
|
+
boolean: (value, defaultValue) => {
|
|
49
|
+
if (!value)
|
|
50
|
+
return defaultValue;
|
|
51
|
+
const normalized = value.toLowerCase().trim();
|
|
52
|
+
if (['true', '1', 'yes', 'y'].includes(normalized))
|
|
53
|
+
return true;
|
|
54
|
+
if (['false', '0', 'no', 'n'].includes(normalized))
|
|
55
|
+
return false;
|
|
56
|
+
configLogger.warn(`Invalid boolean for environment variable, using default: ${defaultValue}`, {
|
|
57
|
+
value,
|
|
58
|
+
defaultValue
|
|
59
|
+
});
|
|
60
|
+
return defaultValue;
|
|
61
|
+
},
|
|
62
|
+
/**
|
|
63
|
+
* Parse environment string to an array of strings
|
|
64
|
+
*
|
|
65
|
+
* @param value - Comma-separated string value from environment
|
|
66
|
+
* @param defaultValue - Default value to use if parsing fails
|
|
67
|
+
* @returns Array of parsed string values
|
|
68
|
+
*/
|
|
69
|
+
array: (value, defaultValue = []) => {
|
|
70
|
+
if (!value)
|
|
71
|
+
return defaultValue;
|
|
72
|
+
return value.split(',').map(item => item.trim()).filter(Boolean);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Environment variable configuration
|
|
77
|
+
*/
|
|
78
|
+
export const config = {
|
|
79
|
+
environment: process.env.NODE_ENV || 'development',
|
|
80
|
+
logLevel: process.env.LOG_LEVEL || 'info',
|
|
81
|
+
// HTTP server configuration
|
|
82
|
+
server: {
|
|
83
|
+
port: parsers.number(process.env.PORT, 3000),
|
|
84
|
+
host: process.env.HOST || 'localhost',
|
|
85
|
+
},
|
|
86
|
+
// Rate limiting settings
|
|
87
|
+
rateLimit: {
|
|
88
|
+
windowMs: parsers.number(process.env.RATE_LIMIT_WINDOW_MS, 60000),
|
|
89
|
+
maxRequests: parsers.number(process.env.RATE_LIMIT_MAX_REQUESTS, 100),
|
|
90
|
+
},
|
|
91
|
+
// Ntfy notification service configuration
|
|
92
|
+
ntfy: {
|
|
93
|
+
baseUrl: process.env.NTFY_BASE_URL || 'https://ntfy.sh',
|
|
94
|
+
defaultTopic: process.env.NTFY_DEFAULT_TOPIC || '',
|
|
95
|
+
apiKey: process.env.NTFY_API_KEY || '',
|
|
96
|
+
maxMessageSize: parsers.number(process.env.NTFY_MAX_MESSAGE_SIZE, 4096),
|
|
97
|
+
maxRetries: parsers.number(process.env.NTFY_MAX_RETRIES, 3),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
// Log the loaded configuration (excluding sensitive values)
|
|
101
|
+
configLogger.info('Configuration loaded', {
|
|
102
|
+
environment: config.environment,
|
|
103
|
+
logLevel: config.logLevel,
|
|
104
|
+
server: config.server,
|
|
105
|
+
ntfy: {
|
|
106
|
+
baseUrl: config.ntfy.baseUrl,
|
|
107
|
+
defaultTopic: config.ntfy.defaultTopic || '(not set)',
|
|
108
|
+
hasApiKey: !!config.ntfy.apiKey,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
export default config;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Ntfy MCP Server - Main Entry Point
|
|
4
|
+
*
|
|
5
|
+
* This is the main entry point for the Ntfy MCP server. It initializes the
|
|
6
|
+
* server, sets up signal handlers for graceful shutdown, and manages the
|
|
7
|
+
* application lifecycle.
|
|
8
|
+
*/
|
|
9
|
+
import { config } from "./config/index.js";
|
|
10
|
+
import { createMcpServer } from "./mcp-server/server.js";
|
|
11
|
+
import { logger } from "./utils/logger.js";
|
|
12
|
+
import { createRequestContext } from "./utils/requestContext.js";
|
|
13
|
+
// Create main application logger
|
|
14
|
+
const appLogger = logger.createChildLogger({
|
|
15
|
+
module: 'NtfyMcpServer',
|
|
16
|
+
service: 'NtfyMcpServer',
|
|
17
|
+
component: 'Main',
|
|
18
|
+
environment: config.environment
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Graceful shutdown handler
|
|
22
|
+
* @param signal The signal that triggered the shutdown
|
|
23
|
+
*/
|
|
24
|
+
const shutdown = async (signal) => {
|
|
25
|
+
appLogger.info(`Shutting down due to ${signal} signal...`);
|
|
26
|
+
try {
|
|
27
|
+
if (mcpServer) {
|
|
28
|
+
appLogger.info('Closing MCP server...');
|
|
29
|
+
await mcpServer.close();
|
|
30
|
+
appLogger.info('MCP server closed successfully');
|
|
31
|
+
}
|
|
32
|
+
appLogger.info('Shutdown complete. Exiting process.');
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
appLogger.error('Error during shutdown', {
|
|
37
|
+
error: error instanceof Error ? error.message : String(error),
|
|
38
|
+
signal
|
|
39
|
+
});
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
// Variable to hold server instance
|
|
44
|
+
let mcpServer;
|
|
45
|
+
/**
|
|
46
|
+
* Main startup function
|
|
47
|
+
*/
|
|
48
|
+
const start = async () => {
|
|
49
|
+
// Create startup context
|
|
50
|
+
const startupContext = createRequestContext({
|
|
51
|
+
operation: "ServerStartup",
|
|
52
|
+
appName: "ntfy-mcp-server",
|
|
53
|
+
environment: config.environment,
|
|
54
|
+
});
|
|
55
|
+
appLogger.info("Starting ntfy-mcp-server...", {
|
|
56
|
+
environment: config.environment,
|
|
57
|
+
logLevel: config.logLevel,
|
|
58
|
+
requestId: startupContext.requestId
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
// Validate ntfy configuration
|
|
62
|
+
const ntfyConfig = config.ntfy;
|
|
63
|
+
if (!ntfyConfig.baseUrl) {
|
|
64
|
+
appLogger.warn("Ntfy base URL not configured. Using default https://ntfy.sh");
|
|
65
|
+
}
|
|
66
|
+
if (!ntfyConfig.defaultTopic) {
|
|
67
|
+
appLogger.warn("No default ntfy topic configured. Some functionality may be limited.");
|
|
68
|
+
}
|
|
69
|
+
// Create main MCP server
|
|
70
|
+
appLogger.info("Creating MCP server...");
|
|
71
|
+
mcpServer = await createMcpServer();
|
|
72
|
+
appLogger.info("MCP server created and connected successfully");
|
|
73
|
+
// Register signal handlers for graceful shutdown
|
|
74
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
75
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
76
|
+
// Handle uncaught exceptions
|
|
77
|
+
process.on("uncaughtException", (error) => {
|
|
78
|
+
appLogger.error("Uncaught exception", {
|
|
79
|
+
error: error instanceof Error ? error.message : String(error),
|
|
80
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
// Handle unhandled promise rejections
|
|
84
|
+
process.on("unhandledRejection", (reason) => {
|
|
85
|
+
appLogger.error("Unhandled promise rejection", {
|
|
86
|
+
reason: reason instanceof Error ? reason.message : String(reason),
|
|
87
|
+
stack: reason instanceof Error ? reason.stack : undefined
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
appLogger.info("Server startup complete. Ready to handle requests.");
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
appLogger.error("Failed to start server", {
|
|
94
|
+
error: error instanceof Error ? error.message : String(error),
|
|
95
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
96
|
+
});
|
|
97
|
+
// Exit with non-zero code to indicate error
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
// Start the application
|
|
102
|
+
start().catch((error) => {
|
|
103
|
+
appLogger.error("Fatal error during startup", {
|
|
104
|
+
error: error instanceof Error ? error.message : String(error),
|
|
105
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
106
|
+
});
|
|
107
|
+
process.exit(1);
|
|
108
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { config } from '../../../config/index.js';
|
|
2
|
+
import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
|
|
3
|
+
import { ErrorHandler } from '../../../utils/errorHandler.js';
|
|
4
|
+
import { logger } from '../../../utils/logger.js';
|
|
5
|
+
import { createRequestContext } from '../../../utils/security.js';
|
|
6
|
+
// Create resource-specific logger
|
|
7
|
+
const resourceLogger = logger.createChildLogger({
|
|
8
|
+
module: 'NtfyResource',
|
|
9
|
+
service: 'NtfyResource'
|
|
10
|
+
});
|
|
11
|
+
export const getNtfyTopic = async (uri) => {
|
|
12
|
+
// Create a request context with unique ID
|
|
13
|
+
const requestContext = createRequestContext({
|
|
14
|
+
operation: 'getNtfyTopic',
|
|
15
|
+
uri: uri.toString()
|
|
16
|
+
});
|
|
17
|
+
const requestId = requestContext.requestId;
|
|
18
|
+
// Extract the topic from the URI pathname
|
|
19
|
+
const topic = uri.hostname || "";
|
|
20
|
+
resourceLogger.info("Ntfy resource request received", {
|
|
21
|
+
requestId,
|
|
22
|
+
uri: uri.href,
|
|
23
|
+
topic
|
|
24
|
+
});
|
|
25
|
+
return ErrorHandler.tryCatch(async () => {
|
|
26
|
+
// Get the default topic from configuration
|
|
27
|
+
const ntfyConfig = config.ntfy;
|
|
28
|
+
let defaultTopic = ntfyConfig.defaultTopic;
|
|
29
|
+
if (!defaultTopic) {
|
|
30
|
+
resourceLogger.warn("Default ntfy topic is not configured, using fallback value", {
|
|
31
|
+
requestId,
|
|
32
|
+
uri: uri.href
|
|
33
|
+
});
|
|
34
|
+
// Provide a fallback value instead of failing
|
|
35
|
+
defaultTopic = "ATLAS";
|
|
36
|
+
}
|
|
37
|
+
// Get recent messages asynchronously for this topic
|
|
38
|
+
let recentMessages = [];
|
|
39
|
+
try {
|
|
40
|
+
// Use a different topic for actual fetching based on whether this is default or not
|
|
41
|
+
const topicToFetch = topic === "default" ? defaultTopic : topic;
|
|
42
|
+
// Attempt to fetch the 10 most recent messages
|
|
43
|
+
const response = await fetch(`${config.ntfy.baseUrl || 'https://ntfy.sh'}/${topicToFetch}/json?poll=1&since=30d`, {
|
|
44
|
+
method: 'GET',
|
|
45
|
+
headers: {
|
|
46
|
+
'Accept': 'application/json'
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
if (response.ok) {
|
|
50
|
+
// Parse response - each line is a separate JSON object
|
|
51
|
+
const text = await response.text();
|
|
52
|
+
const lines = text.split('\n').filter(line => line.trim());
|
|
53
|
+
// Parse each line as a JSON object and add to recent messages
|
|
54
|
+
recentMessages = lines.map(line => JSON.parse(line))
|
|
55
|
+
.filter(msg => msg.event === 'message')
|
|
56
|
+
.slice(0, 10); // Keep only the 10 most recent
|
|
57
|
+
resourceLogger.info(`Retrieved ${recentMessages.length} recent messages`, {
|
|
58
|
+
topic: topicToFetch,
|
|
59
|
+
requestId
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
// Just log the error but don't fail the request
|
|
65
|
+
resourceLogger.warn(`Failed to fetch recent messages for topic`, {
|
|
66
|
+
topic,
|
|
67
|
+
error: error instanceof Error ? error.message : String(error),
|
|
68
|
+
requestId
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
// Handle the "default" topic case specially
|
|
72
|
+
const responseData = topic === "default" ?
|
|
73
|
+
{
|
|
74
|
+
defaultTopic,
|
|
75
|
+
timestamp: new Date().toISOString(),
|
|
76
|
+
requestUri: uri.href,
|
|
77
|
+
requestId,
|
|
78
|
+
recentMessages: recentMessages.length > 0 ? recentMessages : undefined
|
|
79
|
+
} :
|
|
80
|
+
{
|
|
81
|
+
topic,
|
|
82
|
+
timestamp: new Date().toISOString(),
|
|
83
|
+
requestUri: uri.href,
|
|
84
|
+
requestId,
|
|
85
|
+
recentMessages: recentMessages.length > 0 ? recentMessages : undefined
|
|
86
|
+
};
|
|
87
|
+
resourceLogger.info("Ntfy resource response data prepared", {
|
|
88
|
+
requestId,
|
|
89
|
+
responseData
|
|
90
|
+
});
|
|
91
|
+
// Return in the standard MCP format
|
|
92
|
+
const response = {
|
|
93
|
+
contents: [{
|
|
94
|
+
uri: uri.href,
|
|
95
|
+
text: JSON.stringify(responseData, null, 2),
|
|
96
|
+
mimeType: "application/json"
|
|
97
|
+
}]
|
|
98
|
+
};
|
|
99
|
+
return response;
|
|
100
|
+
}, {
|
|
101
|
+
context: {
|
|
102
|
+
requestId,
|
|
103
|
+
uri: uri.toString()
|
|
104
|
+
},
|
|
105
|
+
operation: 'processing ntfy resource request',
|
|
106
|
+
errorMapper: (error) => {
|
|
107
|
+
return new McpError(BaseErrorCode.INTERNAL_ERROR, `Error processing ntfy resource request: ${error instanceof Error ? error.message : 'Unknown error'}`, { requestId, uri: uri.toString() });
|
|
108
|
+
},
|
|
109
|
+
rethrow: true
|
|
110
|
+
});
|
|
111
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Register the ntfy resource with the MCP server
|
|
4
|
+
*
|
|
5
|
+
* This function creates and registers the ntfy resource which returns the default
|
|
6
|
+
* ntfy topic configured in the environment variables. It provides access to this
|
|
7
|
+
* configuration through a resource URI.
|
|
8
|
+
*
|
|
9
|
+
* @param server - The MCP server instance to register the resource with
|
|
10
|
+
* @returns Promise resolving when registration is complete
|
|
11
|
+
*/
|
|
12
|
+
export declare const registerNtfyResource: (server: McpServer) => Promise<void>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
|
|
3
|
+
import { registerResource } from '../../utils/registrationHelper.js';
|
|
4
|
+
import { getNtfyTopic } from './getNtfyTopic.js';
|
|
5
|
+
/**
|
|
6
|
+
* Register the ntfy resource with the MCP server
|
|
7
|
+
*
|
|
8
|
+
* This function creates and registers the ntfy resource which returns the default
|
|
9
|
+
* ntfy topic configured in the environment variables. It provides access to this
|
|
10
|
+
* configuration through a resource URI.
|
|
11
|
+
*
|
|
12
|
+
* @param server - The MCP server instance to register the resource with
|
|
13
|
+
* @returns Promise resolving when registration is complete
|
|
14
|
+
*/
|
|
15
|
+
export const registerNtfyResource = async (server) => {
|
|
16
|
+
return registerResource(server, { name: "ntfy-resource" }, async (server, resourceLogger) => {
|
|
17
|
+
// Create resource template
|
|
18
|
+
const template = new ResourceTemplate("ntfy://{topic}", {
|
|
19
|
+
// Simple list implementation
|
|
20
|
+
list: async () => ({
|
|
21
|
+
resources: [{
|
|
22
|
+
uri: "ntfy://default",
|
|
23
|
+
name: "Default Ntfy Topic",
|
|
24
|
+
description: "Returns the default ntfy topic configured in environment variables"
|
|
25
|
+
}]
|
|
26
|
+
}),
|
|
27
|
+
// No completion needed for this resource
|
|
28
|
+
complete: {}
|
|
29
|
+
});
|
|
30
|
+
// Register the resource
|
|
31
|
+
server.resource(
|
|
32
|
+
// Resource name
|
|
33
|
+
"ntfy-resource",
|
|
34
|
+
// Resource template
|
|
35
|
+
template,
|
|
36
|
+
// Resource metadata
|
|
37
|
+
{
|
|
38
|
+
name: "Ntfy Default Topic",
|
|
39
|
+
description: "Returns the default ntfy topic configured in environment variables",
|
|
40
|
+
mimeType: "application/json",
|
|
41
|
+
// No query parameters needed for this resource
|
|
42
|
+
// Examples
|
|
43
|
+
examples: [
|
|
44
|
+
{
|
|
45
|
+
name: "Default topic",
|
|
46
|
+
uri: "ntfy://default",
|
|
47
|
+
description: "Get the default ntfy topic"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
// Resource handler
|
|
52
|
+
async (uri, params) => {
|
|
53
|
+
// Extract the topic from the URI
|
|
54
|
+
const topic = params.topic;
|
|
55
|
+
resourceLogger.info(`Processing ntfy resource request for topic: ${topic}`, {
|
|
56
|
+
topic,
|
|
57
|
+
href: uri.href
|
|
58
|
+
});
|
|
59
|
+
// Check if the topic is valid
|
|
60
|
+
if (!topic) {
|
|
61
|
+
resourceLogger.error(`Missing topic in ntfy resource uri: ${uri.href}`, {
|
|
62
|
+
href: uri.href,
|
|
63
|
+
protocol: uri.protocol
|
|
64
|
+
});
|
|
65
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Resource not found: ${uri.href}`, { uri: uri.href });
|
|
66
|
+
}
|
|
67
|
+
// Process the request using our dedicated handler
|
|
68
|
+
return await getNtfyTopic(uri);
|
|
69
|
+
});
|
|
70
|
+
resourceLogger.info("Ntfy resource handler registered");
|
|
71
|
+
});
|
|
72
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Schema for validating ntfy resource query parameters
|
|
4
|
+
*/
|
|
5
|
+
export declare const NtfyResourceQuerySchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
6
|
+
export type NtfyResourceQuery = z.infer<typeof NtfyResourceQuerySchema>;
|
|
7
|
+
/**
|
|
8
|
+
* Response type for the ntfy resource, matching MCP SDK expectations
|
|
9
|
+
*/
|
|
10
|
+
export interface NtfyResourceResponse {
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
contents: [
|
|
13
|
+
{
|
|
14
|
+
uri: string;
|
|
15
|
+
text: string;
|
|
16
|
+
mimeType: "application/json";
|
|
17
|
+
}
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Data structure for the ntfy response
|
|
22
|
+
*/
|
|
23
|
+
export interface NtfyData {
|
|
24
|
+
defaultTopic: string;
|
|
25
|
+
timestamp: string;
|
|
26
|
+
requestUri: string;
|
|
27
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Schema for validating ntfy resource query parameters
|
|
4
|
+
*/
|
|
5
|
+
export const NtfyResourceQuerySchema = z.object({
|
|
6
|
+
// No parameters needed for default topic
|
|
7
|
+
}).describe('Query parameters for the ntfy resource.\n' +
|
|
8
|
+
'URI Format: ntfy://default');
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
/**
|
|
4
|
+
* Server state management interface
|
|
5
|
+
*/
|
|
6
|
+
export interface ServerState {
|
|
7
|
+
status: 'initializing' | 'running' | 'error' | 'degraded' | 'shutting_down' | 'shutdown';
|
|
8
|
+
startTime: Date;
|
|
9
|
+
lastHealthCheck: Date;
|
|
10
|
+
activeOperations: Map<string, {
|
|
11
|
+
operation: string;
|
|
12
|
+
startTime: Date;
|
|
13
|
+
}>;
|
|
14
|
+
errors: Array<{
|
|
15
|
+
timestamp: Date;
|
|
16
|
+
message: string;
|
|
17
|
+
code?: string;
|
|
18
|
+
}>;
|
|
19
|
+
registeredTools: Set<string>;
|
|
20
|
+
registeredResources: Set<string>;
|
|
21
|
+
failedRegistrations: Array<{
|
|
22
|
+
type: 'tool' | 'resource';
|
|
23
|
+
name: string;
|
|
24
|
+
error: any;
|
|
25
|
+
attempts: number;
|
|
26
|
+
}>;
|
|
27
|
+
requiredTools: Set<string>;
|
|
28
|
+
requiredResources: Set<string>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create and initialize an MCP server instance with all tools and resources
|
|
32
|
+
*
|
|
33
|
+
* This function configures the MCP server with security settings, tools, and resources.
|
|
34
|
+
* It connects the server to a transport (currently stdio) and returns the initialized
|
|
35
|
+
* server instance.
|
|
36
|
+
*
|
|
37
|
+
* @returns A promise that resolves to the initialized McpServer instance
|
|
38
|
+
* @throws {McpError} If the server fails to initialize
|
|
39
|
+
*/
|
|
40
|
+
export declare const createMcpServer: () => Promise<McpServer>;
|