@applogico/blipr-mcp 0.1.0 → 0.1.3
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/README.md +8 -2
- package/dist/index.js +11 -94
- package/dist/publish.js +40 -0
- package/dist/server.js +57 -0
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# @applogico/blipr-mcp
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@applogico/blipr-mcp)
|
|
4
|
+
[](https://github.com/applogico/blipr-mcp/actions/workflows/ci.yml)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
|
|
3
8
|
An [MCP](https://modelcontextprotocol.io) server that lets AI agents send
|
|
4
9
|
**[Blipr](https://blipr.dev)** push alerts to your phone. Your agent finishes a
|
|
5
10
|
long task, breaks a build, needs approval, or gets stuck — and it pages you.
|
|
@@ -78,14 +83,15 @@ it's delivered as time-sensitive.
|
|
|
78
83
|
> "Run the migration, and `send_alert` me when it's done — priority 4 if it
|
|
79
84
|
> fails."
|
|
80
85
|
|
|
81
|
-
> "
|
|
82
|
-
>
|
|
86
|
+
> "If the nightly backup fails, `send_critical` me with the error — that one
|
|
87
|
+
> can't wait."
|
|
83
88
|
|
|
84
89
|
## Develop
|
|
85
90
|
|
|
86
91
|
```bash
|
|
87
92
|
npm install
|
|
88
93
|
npm run build # → dist/index.js
|
|
94
|
+
npm test # vitest: unit (publish) + in-memory MCP integration
|
|
89
95
|
BLIPR_URL=https://blipr.dev BLIPR_TOPIC=demo node dist/index.js # stdio
|
|
90
96
|
```
|
|
91
97
|
|
package/dist/index.js
CHANGED
|
@@ -1,104 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Blipr MCP server (stdio).
|
|
3
|
+
* Blipr MCP server (stdio entrypoint).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* this process POSTs to `${BLIPR_URL}/api/notify/<topic>`. No inbound socket —
|
|
8
|
-
* stdio in, outbound HTTPS out.
|
|
5
|
+
* Lets an MCP-capable AI agent (Claude Code, Cursor, …) send push notifications
|
|
6
|
+
* to a phone via a Blipr instance. stdio in, outbound HTTPS out — no socket.
|
|
9
7
|
*
|
|
10
8
|
* Config (env):
|
|
11
9
|
* BLIPR_URL Base URL of the Blipr server. Default: https://blipr.dev
|
|
12
10
|
* BLIPR_TOPIC Default topic when a tool call omits one. Optional.
|
|
13
11
|
*/
|
|
14
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
-
import {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
throw new Error("No topic given and BLIPR_TOPIC is not set. Pass `topic`, or set the BLIPR_TOPIC env var.");
|
|
24
|
-
}
|
|
25
|
-
const headers = { "Content-Type": "text/plain" };
|
|
26
|
-
if (opts.title)
|
|
27
|
-
headers["X-Title"] = opts.title;
|
|
28
|
-
if (opts.priority)
|
|
29
|
-
headers["X-Priority"] = String(opts.priority);
|
|
30
|
-
if (opts.tags?.length)
|
|
31
|
-
headers["X-Tags"] = opts.tags.join(",");
|
|
32
|
-
if (opts.click)
|
|
33
|
-
headers["X-Click"] = opts.click;
|
|
34
|
-
let res;
|
|
35
|
-
try {
|
|
36
|
-
res = await fetch(`${BLIPR_URL}/api/notify/${encodeURIComponent(topic)}`, {
|
|
37
|
-
method: "POST",
|
|
38
|
-
headers,
|
|
39
|
-
body: opts.message,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
catch (e) {
|
|
43
|
-
throw new Error(`Could not reach Blipr at ${BLIPR_URL}: ${e.message}`);
|
|
44
|
-
}
|
|
45
|
-
if (!res.ok) {
|
|
46
|
-
const body = await res.text().catch(() => "");
|
|
47
|
-
throw new Error(`Blipr returned ${res.status} ${res.statusText}${body ? ` — ${body}` : ""}`);
|
|
48
|
-
}
|
|
49
|
-
return topic;
|
|
50
|
-
}
|
|
51
|
-
const server = new McpServer({ name: "blipr", version: "0.1.0" });
|
|
52
|
-
server.registerTool("send_alert", {
|
|
53
|
-
title: "Send a Blipr alert",
|
|
54
|
-
description: "Send a push notification to the user's phone via Blipr. Use this to reach the human: a long task finished, a build broke, you need approval, or you're blocked and need input. Priority 1 (silent) to 5 (critical); defaults to 3.",
|
|
55
|
-
inputSchema: {
|
|
56
|
-
message: z.string().describe("The alert body — what happened or what you need."),
|
|
57
|
-
title: z.string().optional().describe("Short title, shown bold above the message."),
|
|
58
|
-
topic: z
|
|
59
|
-
.string()
|
|
60
|
-
.optional()
|
|
61
|
-
.describe("Topic to publish to. Defaults to the BLIPR_TOPIC env var."),
|
|
62
|
-
priority: z
|
|
63
|
-
.number()
|
|
64
|
-
.int()
|
|
65
|
-
.min(1)
|
|
66
|
-
.max(5)
|
|
67
|
-
.optional()
|
|
68
|
-
.describe("1=min/silent, 2=low, 3=default, 4=time-sensitive (breaks Focus), 5=critical."),
|
|
69
|
-
tags: z
|
|
70
|
-
.array(z.string())
|
|
71
|
-
.optional()
|
|
72
|
-
.describe('Tags / emoji shortcodes, e.g. ["warning", "rocket"].'),
|
|
73
|
-
click: z.string().url().optional().describe("URL opened when the notification is tapped."),
|
|
74
|
-
},
|
|
75
|
-
}, async ({ message, title, topic, priority, tags, click }) => {
|
|
76
|
-
try {
|
|
77
|
-
const sent = await publish({ message, title, topic, priority, tags, click });
|
|
78
|
-
return { content: [{ type: "text", text: `Sent to "${sent}" (priority ${priority ?? 3}).` }] };
|
|
79
|
-
}
|
|
80
|
-
catch (e) {
|
|
81
|
-
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
server.registerTool("send_critical", {
|
|
85
|
-
title: "Page the user (critical)",
|
|
86
|
-
description: "Send a priority-5 critical page. Use ONLY for things that genuinely cannot wait (production down, urgent approval, safety). Bypasses silent/Focus when the Blipr app has Apple's Critical Alerts entitlement enabled; otherwise it is delivered as time-sensitive.",
|
|
87
|
-
inputSchema: {
|
|
88
|
-
message: z.string().describe("What is wrong or what you need, urgently."),
|
|
89
|
-
title: z.string().optional().describe("Short title."),
|
|
90
|
-
topic: z.string().optional().describe("Topic. Defaults to BLIPR_TOPIC."),
|
|
91
|
-
},
|
|
92
|
-
}, async ({ message, title, topic }) => {
|
|
93
|
-
try {
|
|
94
|
-
const sent = await publish({ message, title, topic, priority: 5, tags: ["rotating_light"] });
|
|
95
|
-
return { content: [{ type: "text", text: `Paged "${sent}" (priority 5 / critical).` }] };
|
|
96
|
-
}
|
|
97
|
-
catch (e) {
|
|
98
|
-
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
const transport = new StdioServerTransport();
|
|
102
|
-
await server.connect(transport);
|
|
13
|
+
import { createServer } from "./server.js";
|
|
14
|
+
const cfg = {
|
|
15
|
+
bliprUrl: (process.env.BLIPR_URL ?? "https://blipr.dev").replace(/\/+$/, ""),
|
|
16
|
+
defaultTopic: process.env.BLIPR_TOPIC?.trim() || undefined,
|
|
17
|
+
};
|
|
18
|
+
const server = createServer(cfg);
|
|
19
|
+
await server.connect(new StdioServerTransport());
|
|
103
20
|
// stderr is safe for logs; stdout is the MCP channel and must stay clean.
|
|
104
|
-
console.error(`blipr-mcp ready → ${
|
|
21
|
+
console.error(`blipr-mcp ready → ${cfg.bliprUrl}${cfg.defaultTopic ? ` (default topic: ${cfg.defaultTopic})` : ""}`);
|
package/dist/publish.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Blipr publish client — the one piece of real logic, kept pure for testing. */
|
|
2
|
+
/**
|
|
3
|
+
* Publish a message to a Blipr topic. Returns the resolved topic on success.
|
|
4
|
+
*
|
|
5
|
+
* Uses the JSON publish endpoint (`POST /api/notify`) rather than the
|
|
6
|
+
* header-based one. The JSON body is UTF-8, so titles/messages with emoji or
|
|
7
|
+
* accents work — HTTP headers are Latin-1 only and would corrupt or reject them.
|
|
8
|
+
*/
|
|
9
|
+
export async function publish(opts, cfg) {
|
|
10
|
+
const topic = (opts.topic ?? cfg.defaultTopic ?? "").trim();
|
|
11
|
+
if (!topic) {
|
|
12
|
+
throw new Error("No topic given and BLIPR_TOPIC is not set. Pass `topic`, or set the BLIPR_TOPIC env var.");
|
|
13
|
+
}
|
|
14
|
+
const payload = { topic, message: opts.message };
|
|
15
|
+
if (opts.title)
|
|
16
|
+
payload.title = opts.title;
|
|
17
|
+
if (opts.priority)
|
|
18
|
+
payload.priority = opts.priority;
|
|
19
|
+
if (opts.tags?.length)
|
|
20
|
+
payload.tags = opts.tags;
|
|
21
|
+
if (opts.click)
|
|
22
|
+
payload.click = opts.click;
|
|
23
|
+
const base = cfg.bliprUrl.replace(/\/+$/, "");
|
|
24
|
+
let res;
|
|
25
|
+
try {
|
|
26
|
+
res = await fetch(`${base}/api/notify`, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
body: JSON.stringify(payload),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
throw new Error(`Could not reach Blipr at ${base}: ${e.message}`);
|
|
34
|
+
}
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const body = await res.text().catch(() => "");
|
|
37
|
+
throw new Error(`Blipr returned ${res.status} ${res.statusText}${body ? ` — ${body}` : ""}`);
|
|
38
|
+
}
|
|
39
|
+
return topic;
|
|
40
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { publish } from "./publish.js";
|
|
4
|
+
/** Build a configured Blipr MCP server with its tools registered. */
|
|
5
|
+
export function createServer(cfg) {
|
|
6
|
+
const server = new McpServer({ name: "blipr", version: "0.1.0" });
|
|
7
|
+
server.registerTool("send_alert", {
|
|
8
|
+
title: "Send a Blipr alert",
|
|
9
|
+
description: "Send a push notification to the user's phone via Blipr. Use this to reach the human: a long task finished, a build broke, you need approval, or you're blocked and need input. Priority 1 (silent) to 5 (critical); defaults to 3.",
|
|
10
|
+
inputSchema: {
|
|
11
|
+
message: z.string().describe("The alert body — what happened or what you need."),
|
|
12
|
+
title: z.string().optional().describe("Short title, shown bold above the message."),
|
|
13
|
+
topic: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("Topic to publish to. Defaults to the BLIPR_TOPIC env var."),
|
|
17
|
+
priority: z
|
|
18
|
+
.number()
|
|
19
|
+
.int()
|
|
20
|
+
.min(1)
|
|
21
|
+
.max(5)
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("1=min/silent, 2=low, 3=default, 4=time-sensitive (breaks Focus), 5=critical."),
|
|
24
|
+
tags: z
|
|
25
|
+
.array(z.string())
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('Tags / emoji shortcodes, e.g. ["warning", "rocket"].'),
|
|
28
|
+
click: z.string().url().optional().describe("URL opened when the notification is tapped."),
|
|
29
|
+
},
|
|
30
|
+
}, async ({ message, title, topic, priority, tags, click }) => {
|
|
31
|
+
try {
|
|
32
|
+
const sent = await publish({ message, title, topic, priority, tags, click }, cfg);
|
|
33
|
+
return { content: [{ type: "text", text: `Sent to "${sent}" (priority ${priority ?? 3}).` }] };
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
server.registerTool("send_critical", {
|
|
40
|
+
title: "Page the user (critical)",
|
|
41
|
+
description: "Send a priority-5 critical page. Use ONLY for things that genuinely cannot wait (production down, urgent approval, safety). Bypasses silent/Focus when the Blipr app has Apple's Critical Alerts entitlement enabled; otherwise it is delivered as time-sensitive.",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
message: z.string().describe("What is wrong or what you need, urgently."),
|
|
44
|
+
title: z.string().optional().describe("Short title."),
|
|
45
|
+
topic: z.string().optional().describe("Topic. Defaults to BLIPR_TOPIC."),
|
|
46
|
+
},
|
|
47
|
+
}, async ({ message, title, topic }) => {
|
|
48
|
+
try {
|
|
49
|
+
const sent = await publish({ message, title, topic, priority: 5, tags: ["rotating_light"] }, cfg);
|
|
50
|
+
return { content: [{ type: "text", text: `Paged "${sent}" (priority 5 / critical).` }] };
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return server;
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applogico/blipr-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "MCP server that lets AI agents send Blipr push alerts to your phone — your agent can page you.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
17
19
|
"prepare": "npm run build",
|
|
18
20
|
"start": "node dist/index.js"
|
|
19
21
|
},
|
|
@@ -30,6 +32,13 @@
|
|
|
30
32
|
],
|
|
31
33
|
"author": "Applogico LLC",
|
|
32
34
|
"homepage": "https://blipr.dev",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/applogico/blipr-mcp.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/applogico/blipr-mcp/issues"
|
|
41
|
+
},
|
|
33
42
|
"license": "MIT",
|
|
34
43
|
"publishConfig": {
|
|
35
44
|
"access": "public"
|
|
@@ -40,6 +49,7 @@
|
|
|
40
49
|
},
|
|
41
50
|
"devDependencies": {
|
|
42
51
|
"@types/node": "^22.0.0",
|
|
43
|
-
"typescript": "^5.6.0"
|
|
52
|
+
"typescript": "^5.6.0",
|
|
53
|
+
"vitest": "^4.1.9"
|
|
44
54
|
}
|
|
45
55
|
}
|