@rafaelsouza-ai/agent-buzz 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.
- package/README.md +47 -0
- package/bin/agent-buzz.js +4 -0
- package/package.json +19 -0
- package/src/push.js +23 -0
- package/src/server.js +44 -0
- package/src/storage.js +9 -0
- package/src/vapid.js +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# @rafaelsouza-ai/agent-buzz
|
|
2
|
+
|
|
3
|
+
MCP server que envia notificações push em tempo real para o seu celular.
|
|
4
|
+
Quando o agente (Claude Code, Codex, etc.) termina uma task, precisa de
|
|
5
|
+
aprovação ou dá erro — o celular toca.
|
|
6
|
+
|
|
7
|
+
## Como funciona
|
|
8
|
+
- PWA (Docker + Express) roda atrás do seu Cloudflare Tunnel (HTTPS).
|
|
9
|
+
- O celular registra a subscription Web Push num volume compartilhado.
|
|
10
|
+
- O MCP server (stdio) lê a subscription e faz POST na Web Push API
|
|
11
|
+
(`web-push`, VAPID self-managed).
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
1. `docker compose up -d` — sobe a PWA na porta 8080.
|
|
15
|
+
2. Aponte seu Cloudflare Tunnel para `localhost:8080`.
|
|
16
|
+
3. Abra a URL do tunnel no celular → "Adicionar à tela inicial" → "Ativar push".
|
|
17
|
+
4. Configure o MCP no Claude Code:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"agent-buzz": {
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["@rafaelsouza-ai/agent-buzz"],
|
|
25
|
+
"env": { "AGENT_BUZZ_DATA": "./data" }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
> **Importante:** `AGENT_BUZZ_DATA` deve apontar para a pasta `./data` do projeto — a mesma que o `docker-compose.yml` monta em `/data` dentro do container. Se os caminhos divergirem, o MCP server não encontra a subscription e a `notify()` falha silenciosamente.
|
|
32
|
+
|
|
33
|
+
5. O agente chama `notify(title, body, priority)`.
|
|
34
|
+
|
|
35
|
+
## Priority
|
|
36
|
+
- `low` — silencioso + badge
|
|
37
|
+
- `normal` — som + badge
|
|
38
|
+
- `high` — som + badge + vibração + requireInteraction
|
|
39
|
+
|
|
40
|
+
## Limitações (v1)
|
|
41
|
+
- 1 dev / 1 celular (múltiplos dispositivos = v2).
|
|
42
|
+
- A PWA deve permanecer em foreground no celular (Web Push em background no iOS é limitado).
|
|
43
|
+
- Requer secure context (HTTPS via Cloudflare Tunnel).
|
|
44
|
+
- **Segurança do token:** o endpoint `GET /token` não é autenticado. A URL do Cloudflare Tunnel é efêmera (muda a cada `cloudflared` restart), o que mitiga o risco. Para uso em rede pública persistente, considere adicionar um reverse proxy com auth ou injetar o token via env no container.
|
|
45
|
+
|
|
46
|
+
## Publish
|
|
47
|
+
`npm publish --access=public` (pacote scoped, free tier).
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rafaelsouza-ai/agent-buzz",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server that sends real-time Web Push notifications to your phone",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "agent-buzz": "bin/agent-buzz.js" },
|
|
7
|
+
"main": "src/server.js",
|
|
8
|
+
"files": ["bin", "src"],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"engines": { "node": ">=18" },
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node --test tests/",
|
|
13
|
+
"start": "node bin/agent-buzz.js"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
17
|
+
"web-push": "^3.6.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/push.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import webpush from "web-push";
|
|
2
|
+
import { readSubscriptions } from "./storage.js";
|
|
3
|
+
import { loadVapid } from "./vapid.js";
|
|
4
|
+
|
|
5
|
+
export function sendNotification({ title, body, priority, dataPath }) {
|
|
6
|
+
const { subscriptions } = readSubscriptions(dataPath);
|
|
7
|
+
if (!subscriptions.length) {
|
|
8
|
+
return Promise.reject(new Error("No subscription registered"));
|
|
9
|
+
}
|
|
10
|
+
const vapid = loadVapid(dataPath);
|
|
11
|
+
const payload = JSON.stringify({ title, body, priority });
|
|
12
|
+
const urgency = priority === "high" ? "high" : "normal";
|
|
13
|
+
const vapidKeys = {
|
|
14
|
+
publicKey: vapid.publicKey,
|
|
15
|
+
privateKey: vapid.privateKey,
|
|
16
|
+
subject: "mailto:agent-buzz@local",
|
|
17
|
+
};
|
|
18
|
+
return Promise.all(
|
|
19
|
+
subscriptions.map((sub) =>
|
|
20
|
+
webpush.sendNotification(sub, payload, { TTL: 60, headers: { Urgency: urgency }, vapidKeys })
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
}
|
package/src/server.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { sendNotification } from "./push.js";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const DATA_PATH = process.env.AGENT_BUZZ_DATA || join(process.cwd(), "data");
|
|
8
|
+
|
|
9
|
+
export function buildServer(dataPath = DATA_PATH) {
|
|
10
|
+
const server = new McpServer({ name: "agent-buzz", version: "0.1.0" });
|
|
11
|
+
|
|
12
|
+
server.registerTool(
|
|
13
|
+
"notify",
|
|
14
|
+
{
|
|
15
|
+
title: "Notify phone",
|
|
16
|
+
description: "Envia uma notificação push em tempo real para o celular do usuário.",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
title: z.string().describe("Título da notificação"),
|
|
19
|
+
body: z.string().describe("Corpo da notificação"),
|
|
20
|
+
priority: z.enum(["low", "normal", "high"]).default("normal")
|
|
21
|
+
.describe("low=silencioso+badge, normal=som+badge, high=som+badge+vibração+requireInteraction"),
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
async ({ title, body, priority }) => {
|
|
25
|
+
try {
|
|
26
|
+
await sendNotification({ title, body, priority, dataPath });
|
|
27
|
+
return { content: [{ type: "text", text: `Notificação enviada: ${title}` }] };
|
|
28
|
+
} catch (err) {
|
|
29
|
+
return { content: [{ type: "text", text: `Falha ao notificar: ${err.message}` }], isError: true };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return { server, mcp: server };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function startServer() {
|
|
38
|
+
const { mcp } = buildServer();
|
|
39
|
+
const transport = new StdioServerTransport();
|
|
40
|
+
mcp.connect(transport).catch((err) => {
|
|
41
|
+
console.error("agent-buzz: falha ao conectar stdio", err);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|
|
44
|
+
}
|
package/src/storage.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function readSubscriptions(dataPath) {
|
|
5
|
+
const file = join(dataPath, "subscriptions.json");
|
|
6
|
+
if (!existsSync(file)) return { subscriptions: [] };
|
|
7
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
8
|
+
return { subscriptions: raw.subscriptions ?? [] };
|
|
9
|
+
}
|
package/src/vapid.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import webpush from "web-push";
|
|
4
|
+
|
|
5
|
+
export function loadVapid(dataPath) {
|
|
6
|
+
const file = join(dataPath, "vapid.json");
|
|
7
|
+
if (existsSync(file)) {
|
|
8
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
9
|
+
}
|
|
10
|
+
const keys = webpush.generateVAPIDKeys();
|
|
11
|
+
mkdirSync(dataPath, { recursive: true });
|
|
12
|
+
writeFileSync(file, JSON.stringify(keys, null, 2));
|
|
13
|
+
return keys;
|
|
14
|
+
}
|