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
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues