agent-notify-chime 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -0
  3. package/index.js +306 -0
  4. package/package.json +42 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # Agent Notify Chime MCP Server
2
+
3
+ A lightweight Model Context Protocol (MCP) server that sends push notifications through [ntfy](https://ntfy.sh). It mirrors the feature set from `simple-ntfy-mcp` and adds clear setup guides for Codex, VS Code, and Windsurf clients.
4
+
5
+ ## Features
6
+
7
+ - Zero-setup usage via `npx` or local install
8
+ - Sends notifications to any ntfy topic
9
+ - Optional title, priority, tags, click actions, and action buttons
10
+ - Works with STDIO MCP clients (Codex CLI/IDE, VS Code extensions, Windsurf)
11
+
12
+ ## Requirements
13
+
14
+ - Node.js 18+
15
+ - An ntfy topic name (and optional ntfy credentials if you self-host)
16
+
17
+ ## Quickstart (local)
18
+
19
+ ```bash
20
+ npm install
21
+ npm run start
22
+ ```
23
+
24
+ This runs the MCP server on stdio. Use the client configuration examples below to connect.
25
+
26
+ ## Environment variables
27
+
28
+ | Variable | Description | Default |
29
+ | --- | --- | --- |
30
+ | `NTFY_DEFAULT_TOPIC` | Default topic to send notifications to | required |
31
+ | `NTFY_BASE_URL` | Base URL of the ntfy server | `https://ntfy.sh` |
32
+ | `NTFY_AUTH_TOKEN` | Bearer token for ntfy auth (optional) | empty |
33
+ | `NTFY_USERNAME` | Username for Basic auth (optional) | empty |
34
+ | `NTFY_PASSWORD` | Password for Basic auth (optional) | empty |
35
+ | `NTFY_REQUEST_TIMEOUT_MS` | Request timeout in ms | `10000` |
36
+
37
+ ## Tool: `send_ntfy`
38
+
39
+ Send a push notification to your configured ntfy topic.
40
+
41
+ ### Parameters
42
+
43
+ | Parameter | Type | Required | Description |
44
+ | --- | --- | --- | --- |
45
+ | `message` | string | yes | The notification message |
46
+ | `topic` | string | no | Topic to send to (uses default if not specified) |
47
+ | `title` | string | no | Notification title |
48
+ | `priority` | number | no | Priority level 1-5 (default 3) |
49
+ | `tags` | string[] | no | Array of tags/emojis |
50
+ | `click` | string | no | URL to open when clicked |
51
+ | `actions` | object[] | no | Action buttons per ntfy docs |
52
+
53
+ ### Example tool call
54
+
55
+ ```json
56
+ {
57
+ "message": "Build finished successfully",
58
+ "title": "CI",
59
+ "priority": 4,
60
+ "tags": ["white_check_mark", "rocket"],
61
+ "click": "https://example.com/builds/123"
62
+ }
63
+ ```
64
+
65
+ ## Client setup
66
+
67
+ Use the published package command below:
68
+
69
+ - **Published package**: `command = "npx"`, `args = ["-y", "agent-notify-chime"]`
70
+
71
+ ### Codex (CLI + IDE extension)
72
+
73
+ Codex reads MCP servers from `~/.codex/config.toml` and shares this config with the IDE extension (VS Code, Cursor, Windsurf).
74
+
75
+ **Option A: CLI command**
76
+
77
+ ```bash
78
+ codex mcp add agent-notify-chime \
79
+ --env NTFY_DEFAULT_TOPIC=your-topic \
80
+ -- npx -y agent-notify-chime
81
+ ```
82
+
83
+ **Option B: Edit `config.toml`**
84
+
85
+ ```toml
86
+ [mcp_servers.agent-notify-chime]
87
+ command = "npx"
88
+ args = ["-y", "agent-notify-chime"]
89
+
90
+ [mcp_servers.agent-notify-chime.env]
91
+ NTFY_DEFAULT_TOPIC = "your-topic"
92
+ ```
93
+
94
+ ### VS Code (Cline extension)
95
+
96
+ Cline is a popular MCP client for VS Code. It stores MCP settings in `cline_mcp_settings.json` (open it via the MCP Servers panel).
97
+
98
+ ```json
99
+ {
100
+ "mcpServers": {
101
+ "agent-notify-chime": {
102
+ "command": "npx",
103
+ "args": ["-y", "agent-notify-chime"],
104
+ "env": {
105
+ "NTFY_DEFAULT_TOPIC": "your-topic"
106
+ }
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ After editing, restart the server from the MCP Servers panel.
113
+
114
+ ### Windsurf (Cascade)
115
+
116
+ Windsurf reads MCP settings from `~/.codeium/windsurf/mcp_config.json`.
117
+
118
+ ```json
119
+ {
120
+ "mcpServers": {
121
+ "agent-notify-chime": {
122
+ "command": "npx",
123
+ "args": ["-y", "agent-notify-chime"],
124
+ "env": {
125
+ "NTFY_DEFAULT_TOPIC": "your-topic"
126
+ }
127
+ }
128
+ }
129
+ }
130
+ ```
131
+
132
+ Restart Cascade or toggle the server to apply changes.
133
+
134
+ ## Implementation notes
135
+
136
+ - This server uses the MCP SDK over stdio and publishes a single tool: `send_ntfy`.
137
+ - Notifications are sent by POSTing to `NTFY_BASE_URL/<topic>` with ntfy headers.
138
+ - Auth is optional (Bearer token or Basic auth) for self-hosted ntfy servers.
139
+
140
+ ## Future improvements (recommended)
141
+
142
+ - Add attachment support (files, images) via ntfy `X-Attach`.
143
+ - Add retry/backoff for transient HTTP failures.
144
+ - Provide a small CLI to validate config/env variables before launching.
145
+ - Support ntfy message templating (predefined titles/tags).
146
+ - Optional metrics or structured logging for observability.
147
+
148
+ ## Publish to npm
149
+
150
+ 1. Ensure you are logged in: `npm login`
151
+ 2. Update the version if needed: `npm version patch` (or `minor`/`major`)
152
+ 3. Publish: `npm publish --access public`
153
+
154
+ If you are publishing for the first time, make sure the package name `agent-notify-chime` is available on npm.
155
+
156
+ ## License
157
+
158
+ MIT
package/index.js ADDED
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } from "@modelcontextprotocol/sdk/types.js";
9
+
10
+ const NTFY_BASE_URL = (process.env.NTFY_BASE_URL || "https://ntfy.sh").replace(
11
+ /\/+$/,
12
+ ""
13
+ );
14
+ const DEFAULT_TOPIC = process.env.NTFY_DEFAULT_TOPIC || "";
15
+ const AUTH_TOKEN = process.env.NTFY_AUTH_TOKEN || "";
16
+ const NTFY_USERNAME = process.env.NTFY_USERNAME || "";
17
+ const NTFY_PASSWORD = process.env.NTFY_PASSWORD || "";
18
+ const REQUEST_TIMEOUT_MS = Number.parseInt(
19
+ process.env.NTFY_REQUEST_TIMEOUT_MS || "10000",
20
+ 10
21
+ );
22
+
23
+ const server = new Server(
24
+ {
25
+ name: "agent-notify-chime",
26
+ version: "0.1.0",
27
+ },
28
+ {
29
+ capabilities: {
30
+ tools: {},
31
+ },
32
+ }
33
+ );
34
+
35
+ function getAuthHeader() {
36
+ if (AUTH_TOKEN) {
37
+ return `Bearer ${AUTH_TOKEN}`;
38
+ }
39
+
40
+ if (NTFY_USERNAME && NTFY_PASSWORD) {
41
+ const encoded = Buffer.from(`${NTFY_USERNAME}:${NTFY_PASSWORD}`).toString(
42
+ "base64"
43
+ );
44
+ return `Basic ${encoded}`;
45
+ }
46
+
47
+ return undefined;
48
+ }
49
+
50
+ function buildActionsHeader(actions) {
51
+ if (!Array.isArray(actions)) {
52
+ return undefined;
53
+ }
54
+
55
+ const serialized = actions
56
+ .map((action) => {
57
+ if (!action || typeof action.action !== "string") {
58
+ return null;
59
+ }
60
+
61
+ const label = typeof action.label === "string" ? action.label : "";
62
+ const url = typeof action.url === "string" ? action.url : "";
63
+ const parts = [action.action, label, url];
64
+
65
+ if (typeof action.method === "string" && action.method) {
66
+ parts.push(`method=${action.method}`);
67
+ }
68
+
69
+ return parts.join(", ");
70
+ })
71
+ .filter(Boolean)
72
+ .join("; ");
73
+
74
+ return serialized || undefined;
75
+ }
76
+
77
+ function normalizeTags(tags) {
78
+ if (!Array.isArray(tags)) {
79
+ return undefined;
80
+ }
81
+
82
+ const filtered = tags.filter((tag) => typeof tag === "string" && tag.trim());
83
+ return filtered.length > 0 ? filtered : undefined;
84
+ }
85
+
86
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
87
+ return {
88
+ tools: [
89
+ {
90
+ name: "send_ntfy",
91
+ description: "Send a notification via ntfy",
92
+ inputSchema: {
93
+ type: "object",
94
+ properties: {
95
+ topic: {
96
+ type: "string",
97
+ description: "The ntfy topic to send to",
98
+ },
99
+ message: {
100
+ type: "string",
101
+ description: "The message to send",
102
+ },
103
+ title: {
104
+ type: "string",
105
+ description: "Optional notification title",
106
+ },
107
+ priority: {
108
+ type: "number",
109
+ description: "Priority level (1-5, default 3)",
110
+ minimum: 1,
111
+ maximum: 5,
112
+ },
113
+ tags: {
114
+ type: "array",
115
+ items: { type: "string" },
116
+ description: "Optional tags/emojis",
117
+ },
118
+ click: {
119
+ type: "string",
120
+ description: "URL to open when notification is clicked",
121
+ },
122
+ actions: {
123
+ type: "array",
124
+ description: "Action buttons",
125
+ items: {
126
+ type: "object",
127
+ properties: {
128
+ action: { type: "string" },
129
+ label: { type: "string" },
130
+ url: { type: "string" },
131
+ method: { type: "string" },
132
+ },
133
+ },
134
+ },
135
+ },
136
+ required: ["message"],
137
+ },
138
+ },
139
+ ],
140
+ };
141
+ });
142
+
143
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
144
+ if (request.params.name !== "send_ntfy") {
145
+ throw new Error(`Unknown tool: ${request.params.name}`);
146
+ }
147
+
148
+ const args = request.params.arguments ?? {};
149
+ const message = typeof args.message === "string" ? args.message : "";
150
+ const topic =
151
+ typeof args.topic === "string" && args.topic.trim()
152
+ ? args.topic.trim()
153
+ : DEFAULT_TOPIC;
154
+
155
+ if (!message.trim()) {
156
+ return {
157
+ content: [
158
+ {
159
+ type: "text",
160
+ text: JSON.stringify(
161
+ {
162
+ success: false,
163
+ error: "Message is required",
164
+ },
165
+ null,
166
+ 2
167
+ ),
168
+ },
169
+ ],
170
+ isError: true,
171
+ };
172
+ }
173
+
174
+ if (!topic) {
175
+ return {
176
+ content: [
177
+ {
178
+ type: "text",
179
+ text: JSON.stringify(
180
+ {
181
+ success: false,
182
+ error: "No topic specified and no default topic configured",
183
+ },
184
+ null,
185
+ 2
186
+ ),
187
+ },
188
+ ],
189
+ isError: true,
190
+ };
191
+ }
192
+
193
+ const url = `${NTFY_BASE_URL}/${encodeURIComponent(topic)}`;
194
+ const headers = {
195
+ "Content-Type": "text/plain",
196
+ };
197
+
198
+ const authHeader = getAuthHeader();
199
+ if (authHeader) {
200
+ headers.Authorization = authHeader;
201
+ }
202
+
203
+ if (typeof args.title === "string" && args.title) {
204
+ headers["X-Title"] = args.title;
205
+ }
206
+
207
+ if (typeof args.priority === "number") {
208
+ headers["X-Priority"] = String(args.priority);
209
+ }
210
+
211
+ const tags = normalizeTags(args.tags);
212
+ if (tags) {
213
+ headers["X-Tags"] = tags.join(",");
214
+ }
215
+
216
+ if (typeof args.click === "string" && args.click) {
217
+ headers["X-Click"] = args.click;
218
+ }
219
+
220
+ const actionsHeader = buildActionsHeader(args.actions);
221
+ if (actionsHeader) {
222
+ headers["X-Actions"] = actionsHeader;
223
+ }
224
+
225
+ const timeoutMs = Number.isFinite(REQUEST_TIMEOUT_MS)
226
+ ? REQUEST_TIMEOUT_MS
227
+ : 10000;
228
+ const controller = new AbortController();
229
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
230
+
231
+ try {
232
+ const response = await fetch(url, {
233
+ method: "POST",
234
+ headers,
235
+ body: message,
236
+ signal: controller.signal,
237
+ });
238
+
239
+ if (!response.ok) {
240
+ const responseText = await response.text().catch(() => "");
241
+ throw new Error(
242
+ `HTTP ${response.status}${responseText ? `: ${responseText}` : ""}`
243
+ );
244
+ }
245
+
246
+ const contentType = response.headers.get("content-type") || "";
247
+ const result = contentType.includes("application/json")
248
+ ? await response.json()
249
+ : { body: await response.text() };
250
+
251
+ const payload = {
252
+ success: true,
253
+ topic,
254
+ response: result,
255
+ };
256
+
257
+ if (result && typeof result === "object") {
258
+ if (result.id) payload.messageId = result.id;
259
+ if (result.time) payload.time = result.time;
260
+ }
261
+
262
+ return {
263
+ content: [
264
+ {
265
+ type: "text",
266
+ text: JSON.stringify(payload, null, 2),
267
+ },
268
+ ],
269
+ };
270
+ } catch (error) {
271
+ const messageText =
272
+ error && error.name === "AbortError"
273
+ ? `Request timed out after ${timeoutMs}ms`
274
+ : error?.message || "Unknown error";
275
+
276
+ return {
277
+ content: [
278
+ {
279
+ type: "text",
280
+ text: JSON.stringify(
281
+ {
282
+ success: false,
283
+ error: messageText,
284
+ },
285
+ null,
286
+ 2
287
+ ),
288
+ },
289
+ ],
290
+ isError: true,
291
+ };
292
+ } finally {
293
+ clearTimeout(timeoutId);
294
+ }
295
+ });
296
+
297
+ async function main() {
298
+ const transport = new StdioServerTransport();
299
+ await server.connect(transport);
300
+ console.error("Agent Notify Chime MCP Server running on stdio");
301
+ }
302
+
303
+ main().catch((error) => {
304
+ console.error("Fatal error:", error);
305
+ process.exit(1);
306
+ });
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "agent-notify-chime",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for sending ntfy notifications",
5
+ "type": "module",
6
+ "bin": {
7
+ "agent-notify-chime": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "start": "node index.js"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "ntfy",
20
+ "notifications",
21
+ "model-context-protocol",
22
+ "codex",
23
+ "vscode",
24
+ "windsurf"
25
+ ],
26
+ "author": "",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/ace3/agent-notify-chime.git"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/ace3/agent-notify-chime/issues"
33
+ },
34
+ "homepage": "https://github.com/ace3/agent-notify-chime#readme",
35
+ "license": "MIT",
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^1.0.4"
38
+ },
39
+ "engines": {
40
+ "node": ">=18"
41
+ }
42
+ }