Public
Remixed
Townie: a Claude Managed Agent wired to your Val Town MCP
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.

Townie — a Claude Managed Agent for your Val Town account

An interactive chat UI for Townie, a Claude Managed Agent wired to the logged-in user's Val Town account over MCP. Each thread is one stateful agent session; the agent can read and edit your vals, run your SQLite, ship HTTP endpoints, and produce files — all acting as you, with your permissions. Hono (backend) + React (frontend), Val Town SQLite for storage.

This is a remix of templates/claude-managed-agents-ui that adds per-user Val Town MCP access.

How Townie gets access to your account

The Managed Agents API keeps MCP auth out of the (shared, versioned) agent config and supplies it per-session via a vault:

  1. The agent (backend/agent.ts) declares the Val Town MCP server by URL only — mcp_servers: [{ type: "url", name: "val_town", url: "https://api.val.town/v3/mcp" }] — plus an mcp_toolset set to always_allow so Townie's tools run without approval prompts. The system prompt is Val Town's official Townie prompt (fetched from val.town/townie/system-prompt) plus an addendum explaining that this Townie runs as a Managed Agent and the user's vals live behind the MCP.
  2. The login (index.ts) uses oauthMiddleware with the scopes Townie needs — val_rw, project_rw, sqlite_rw, blob_rw, user_r, telemetry_rw, offline_access — so the user's access token can actually do the work.
  3. Per user (backend/vault.ts) we keep one Anthropic vault holding a static_bearer credential whose token is that user's Val Town access token, bound to the Val Town MCP URL. It's created on first use and the token is rotated to the current one on every turn (std/oauth keeps the cookie token fresh), so the credential never goes stale.
  4. Per session (backend/agent.ts) we attach the user's vault via vault_ids when creating the thread's session (vaults can only be set at session-create time). Anthropic injects the bearer onto every outbound MCP call — the token never touches the agent's sandbox.

The result: when you tell Townie "add a dark-mode toggle to my homepage val", it calls list_vals / read_file / update_file / fetch_val_endpoint on the Val Town MCP as you, and edits land in your account.

How the chat loop works

  • A thread = one session. The agent + environment are created once (lazily, cached in a config row); sessions pin to an agent version. Each thread makes one session (bound to its owner's vault) and reuses it.
  • On a new user message the server refreshes the user's vault token, ensures the session, then opens the event stream, sends the message, and writes each event to SQLiteagent.message → text, agent.tool_use / agent.mcp_tool_use → tool call, session.error → error — returning when the session goes idle.
  • The frontend polls GET /api/threads/:id/messages?after=<lastId> ~1×/s and renders new rows. No SSE to the browser.
  • Files Townie creates under /mnt/session/outputs/ are surfaced after each turn: backend/files.ts lists them (files.list({ scope_id })), downloads them, stashes the bytes in std/blob, and writes an image/file row served at GET /api/files/:id.

Setup

Set ANTHROPIC_API_KEY (from https://platform.claude.com/settings/keys). Everything else — agent, environment, per-user vaults — is created on first use. Tweak the model or system prompt in backend/agent.ts (the default is claude-sonnet-4-6; swap to claude-opus-4-8 for the strongest Townie). To re-create the agent after changing its prompt/tools, clear the agent_id row in the config table.

Files

  • index.ts — Hono server + API routes + OAuth scopes
  • backend/agent.ts — Managed Agent: create-once config (Val Town MCP + Townie prompt), per-thread vault-bound session, per-turn streaming
  • backend/vault.ts — per-user Anthropic vault holding the Val Town MCP bearer credential (created once, token rotated each turn)
  • backend/auth.ts — resolve the Val Town user + access token from the session
  • backend/db.ts — SQLite schema + helpers (threads, messages, config, user_vaults)
  • backend/files.ts — surface session output files: Files API → std/blob → image/file rows
  • backend/og.ts — Open Graph card at GET /og.png
  • frontend/ — React chat UI

Notes & backlog

  • MCP tools run as the user, with always_allow. Townie edits your real vals without a confirmation step — same trust model as the official Townie. To add an approval gate, set the mcp_toolset permission policy to always_ask and handle agent.tool_use (evaluated_permission === "ask") with a user.tool_confirmation round-trip.
  • Token lifetime. The vaulted token is the user's Val Town OAuth access token; it's refreshed every turn from the live session cookie. A session left idle past token expiry recovers on the next turn.
  • Stream reconnection / long turns. The turn request stays open for the whole agent turn; if it dies before session.status_idle, reopening the stream and replaying from the thread's last_cursor would resume it. Long turns can hit Val Town request limits — tied to the same reconnection work.