Skip to main content
Glama
coder-mil

Ask Agent

by coder-mil
README.md
# Ask Agent — MCP ↔ PWA bridge

A local-first system that lets any MCP-compatible AI agent (OpenCode, Claude Code, Hermes Agent, etc.) ask the human questions in real time via a **mobile-friendly PWA** with browser push notifications. No Telegram, no Discord, no third-party middlemen.

```
┌─────────────┐    ask_user()    ┌──────────────┐    Socket.IO    ┌────────────┐
│  Agent /    │ ───────────────► │  MCP server  │ ──────────────► │   PWA in   │
│  OpenCode   │ ◄─────────────── │  (Node.js)   │ ◄────────────── │  your phone│
└─────────────┘   user's reply   └──────────────┘    answer       └────────────┘
                                       │
                                       ▼   Web Push (VAPID)
                                  ┌──────────────┐
                                  │  Browser     │
                                  │  Notification│
                                  └──────────────┘
```

## Why?

When an agent runs for hours, it shouldn't have to **guess** — it should **ask**. This lets you stay in flow on your phone/PC while the agent waits, without being glued to a terminal or a Telegram session.

## Features

- **5 question types**: free text, yes/no, multiple choice, 1–5 scale, confirm
- **Multiple concurrent questions** (each with a UUID, separate timeout)
- **Real-time delivery** via Socket.IO with automatic reconnect
- **Push notifications** even when the PWA tab is in the background (VAPID/web-push)
- **Installable PWA** (manifest + service worker + maskable icons)
- **Stateless MCP** — each agent session gets its own server, scales horizontally
- **One command to run**: `npm start` on port 3010

## Quick start

```bash
npm install
npm start
# open http://localhost:3010
```

That's it. VAPID keys auto-generate on first boot (printed in the console — set `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` / `VAPID_SUBJECT` env vars to persist them).

## MCP client config

### OpenCode / Claude Code / Hermes Agent

Add to your MCP config (`~/.config/opencode/mcp.json` or equivalent):

```json
{
  "mcpServers": {
    "ask-user": {
      "type": "http",
      "url": "http://localhost:3010/mcp"
    }
  }
}
```

The agent will see one tool: **`ask_user(question, type, options?, timeoutMs?)`**.

### GitHub Actions

Expose the server with ngrok/cloudflared and use the same URL:

```yaml
- run: npm install
- run: npm start &
- run: npx ngrok http 3010 --authtoken=$NGROK_TOKEN > ngrok.log &
- run: sleep 5 && cat ngrok.log  # extract https URL
```

Then point your MCP client at the ngrok URL.

## Question types

| `type`          | UI                                   | Response shape           |
|-----------------|--------------------------------------|--------------------------|
| `text`          | Textarea + Enter to submit           | string                   |
| `yesno`         | Two big buttons (Sim / Não)          | `true` or `false`        |
| `multiple_choice` | Grid of buttons (requires `options`) | string (selected option) |
| `scale`         | Slider 1–5                           | integer 1–5              |
| `confirm`       | Single "OK, confirmado" button       | `true`                   |

Default timeout: **5 minutes**. Override per call with `timeoutMs`.

## Endpoints

| Path                       | Purpose                              |
|----------------------------|--------------------------------------|
| `GET /`                    | The PWA                              |
| `GET /health`              | Liveness probe                       |
| `POST /mcp`                | MCP Streamable HTTP                  |
| `GET /api/vapid-public-key`| VAPID public key (for push subscribe)|
| `POST /api/push/subscribe` | Register a push subscription         |
| `POST /api/push/unsubscribe` | Unregister                          |
| `GET /api/push/test`       | Send a test push                     |

## Architecture

```
src/
├── server.js         Entry point: Express + Socket.IO + MCP, all on 3010
├── questionManager.js  In-memory store + EventEmitter; UUID-keyed Promises
├── mcp.js            Streamable HTTP MCP, stateless (new McpServer per session)
├── socket.js         Socket.IO ↔ QuestionManager glue
├── push.js           Web-push init + VAPID key handling
└── pushRoutes.js     REST endpoints for push subscription
public/
├── index.html        PWA shell
├── app.js            Vanilla JS frontend (Socket.IO client, push subscribe)
├── styles.css        Dark-mode mobile-first styles
├── manifest.webmanifest  PWA manifest (installable, maskable icons)
├── sw.js             Service worker (push handler + asset cache)
└── icons/            192/512/maskable SVG icons
```

### Key design choices

- **Stateless MCP sessions**: each agent that calls `/mcp` gets its own `McpServer` instance. The `McpServer` SDK enforces one transport per server — this is the cleanest workaround that still allows multiple concurrent agents.
- **Shared state via QuestionManager singleton**: questions and answers flow through a single in-memory Map. Restart loses pending questions (intentional — these are short-lived conversational exchanges).
- **Push is best-effort**: if no one is subscribed, the question still arrives via Socket.IO. Push is only for when the PWA tab isn't active.

## Running in production

This is a **local-first** tool by design. If you want 24/7 availability:

1. Run on a VPS with HTTPS (Cloudflare Tunnel, Caddy, or nginx + Let's Encrypt)
2. Set `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY`, `VAPID_SUBJECT` env vars to persist VAPID across restarts
3. Persist `subscriptions` somewhere (the current in-memory Map dies on restart) — easy swap to SQLite/Redis
4. Add auth to `/api/push/*` and `/mcp` if exposed beyond localhost

## License

MIT