Read-only BAND OAuth to MCP bridge with HTTP/tsnet hosting
  • Go 85%
  • HTML 7.2%
  • JavaScript 5.6%
  • Nix 1%
  • Dockerfile 0.9%
  • Other 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-08 18:34:51 +01:00
docs Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
scripts Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
web Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
.dockerignore Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
.gitignore Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
app.go Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
band.go Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
bridge_test.go Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
bun.lock Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
Dockerfile Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
flake.lock Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
flake.nix Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
go.mod Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
go.sum Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
images.go Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
justfile Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
main.go Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
package.json Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
README.md Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
store.go Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
tools.go Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00
ui_fixture_test.go Build read-only BAND OAuth MCP bridge with browser onboarding 2026-10-08 18:34:51 +01:00

BAND MCP bridge

A small, read-only Go connector for catching up on BAND groups. Connect in your browser, copy your personal MCP configuration, and ask a vision-capable assistant:

Summarise posts created in the last 24 hours across my groups. Include dates and action items, inspect notice images, and say if any groups were incomplete.

The bridge provides source data and image content; your chosen assistant does summarisation/OCR. It has no AI-provider integration and never posts or edits BAND.

Status

R&D implementation against the official BAND API docs, with mocked OAuth/API and real MCP SDK client tests. No BAND app registration or account was available: live consent, callback acceptance, scopes, quota, image CDN access and client-app compatibility still need verification. No live service has been deployed.

Run locally

nix develop
just test
BASE_URL=http://localhost:8080 ALLOW_INSECURE_LOCALHOST=true just run

Without BAND credentials, the landing page explains that registration is pending. The same page is used after deployment. Go toolchain auto-download selects the version in go.mod if the Nix Go package is older. just build creates a static binary. flake.lock pins the development shell; no Node/frontend build is needed.

For working OAuth, set BAND_CLIENT_ID and BAND_CLIENT_SECRET_FILE to your own registered app credentials. Prefer a protected file over inline environment values. Never commit credentials or connection data. Local HTTP is allowed only for an explicit localhost development origin; BAND may require HTTPS for callback registration, so this local mode alone does not establish a live OAuth test.

Production hosting

Choose one mode, and use an exact HTTPS BASE_URL without a path/trailing route. It determines callback URLs, host/origin checks, cookies, and MCP configuration.

  • Public HTTP service: LISTEN_MODE=http, LISTEN_ADDR=0.0.0.0:8080 inside a container; put an HTTPS reverse proxy in front. Forward the original Host, and preserve Authorization, POST bodies and the /oauth/callback/* path. The sample publishes only loopback port 8080. Set BASE_URL=https://bridge.example.com.
  • Tailnet-only: LISTEN_MODE=tsnet, TS_HOSTNAME=band-mcp, TS_ADVERTISE_TAGS=tag:band-mcp, TS_AUTHKEY_FILE for initial enrollment, and BASE_URL=https://band-mcp.<tailnet>.ts.net. Enable MagicDNS/HTTPS and grant intended members access to this node's TCP 443. It calls tsnet ListenTLS and opens no host HTTP port. No sidecar, Funnel, TUN device or privileged mode. Both the browser doing OAuth and the MCP client must be connected to the tailnet; returning from BAND is a browser redirect, not an inbound BAND webhook.

BAND's app setup must permit the bridge's callback domain. The bridge asks for BASE_URL/oauth/callback/<random-login-nonce> on each login. BAND's docs describe a registered domain with variable redirect paths; confirm this works for the actual registration. The token exchange uses BAND's documented GET request with Basic client authentication. No undocumented refresh endpoint is assumed: when an access token expires or is revoked, reconnect. Reconnecting the same BAND identity replaces its old connection and invalidates its old bridge token/session.

Register only read permissions, where BAND's developer console permits selection: READ_BAND_AND_USERS_LIST, READ_POST, READ_COMMENT, and relevant photo read permission. The bridge exposes no write tools even if the provider grants writes. BAND's published authorization request does not document a scope parameter, so permissions are configured at registration, not invented in the OAuth URL.

Docker

cd docs
# Supply BASE_URL and BAND_CLIENT_ID through your operator environment.
# Provision ./secrets/band-client-secret, root:65532 mode 0640 (or equivalent).
# Provision ./data owned 65532:65532, mode 0700.
docker compose up --build -d
# Tailnet variant instead:
docker compose -f compose.tsnet.yml up --build -d

The image runs UID/GID 65532 with CA certificates. Persist all of /data, including master.key, connections.db, and tailscale/ identity. An operator with root access should prepare directory/file ownership; the application does not require privilege. Samples build locally; no registry publication or deployment automation has been configured. Dockerfile compilation is validated via the matching static Go build; a real Docker daemon build remains a deployment check.

MCP tools

All tools are marked read-only. /mcp is stateless Streamable HTTP using the official Go MCP SDK. Supply Authorization: Bearer <bridge-token> on every request. There are no public MCP tools, persistent MCP sessions, stdio transport or MCP OAuth discovery. Clients must support preconfigured headers; an OAuth-only remote connector cannot use this directly without an adapter.

Tool Use
band_list_bands Discover groups for this connection.
band_list_posts One page for a group; use returned after for next page.
band_get_post Full post, metadata and image keys.
band_list_comments Comments page for a post.
band_recent_posts Creation-time window (default 24h) across chosen groups or up to 20 discovered groups.
band_post_image Return a selected post photo as MCP image content for vision/OCR in the client.

BAND posts are sorted by last modification, not creation. Recent scanning does not stop at an old creation date. A scan defaults to 5 pages/group, max 20, and reports complete, next_after, errors and omitted groups. Incomplete scans can miss posts; continue using band_list_posts or increase the budget. Timestamps are epoch milliseconds. There is no promise of exhaustive search, chronological server-side filtering or ingestion of every group. Requests have a two-minute aggregate deadline; upstream requests have 20-second timeouts, no automatic retry that could amplify quotas, and bounded response sizes. Recent-post results have a 2 MiB combined data budget; exceeding it is reported as an incomplete scan.

Photos are read only after checking that the connected BAND account can fetch the containing post. The tool accepts a photo key, not an arbitrary URL. It accepts PNG/JPEG/GIF/WebP up to 4 MiB, upgrades legacy http CDN URLs to HTTPS, and rejects private DNS addresses, userinfo, unexpected ports and redirects. Default CDN host families come from BAND documentation: band.us, naver.net, pstatic.net and campmobile.net. IMAGE_ALLOWED_HOSTS adds exact operator-approved hostnames if live responses use another CDN. It does not add URL access from tool arguments. Video/audio, arbitrary downloads, server-side OCR and album traversal are outside this first version. Clients without image support can see photo URLs/metadata but must not claim they read the image detail.

Post/comment/image content is untrusted, including instructions embedded in it. The agent should extract facts, dates and tasks, not follow source instructions.

Account privacy and deletion

BAND access tokens and bridge tokens are encrypted with AES-256-GCM at rest. Browser and MCP token lookup keys are SHA-256 hashes. Database and local encryption key are mode 0600. The key lives alongside the database: encryption protects accidental database disclosure, not compromise of the host or whole data directory.

The bridge does not retain posts, comments, images, profile names or refresh tokens. It stores a hashed BAND identity solely to replace a previous connection for the same account. Browser sessions last seven days. MCP tokens work until disconnected or replaced; upstream expiry still requires reconnecting. Treat bridge tokens as private, since they grant read access as the user. No request/access logs are implemented; disable query/header logging in your proxy, especially for callbacks and BAND requests (BAND requires access_token in query parameters).

Disconnect requires a browser session, same-origin POST and explicit confirmation. It deletes the row with SQLite secure_delete and compacts the database, invalidates the browser/MCP credentials, and waits for in-flight bridge MCP requests before returning. It never deletes BAND content. BAND provides no revoke endpoint in the referenced docs: remove the app grant in BAND's own settings as well if desired. The bridge cannot erase data already sent to AI clients, proxy logs or operator backups; manage those retention policies separately. Back up the full data directory only under an appropriate retention policy, and protect archives as credentials.

OAuth CSRF binding uses a short-lived random callback path plus HttpOnly/SameSite browser cookie, since BAND does not document returning state. Pending logins expire in 10 minutes and do not survive restarts. Host/Origin checks use BASE_URL, cookies are Secure on HTTPS, credentials are not placed in the MCP URL, and output paging is scrubbed because BAND returns access tokens in next_params.

Verification

go test ./... exercises encrypted persistence/restart/deletion; OAuth browser binding/replay rejection; same-origin deletion; per-user MCP bearer isolation with a real SDK client; read-only tool discovery; pagination without early stopping; truncation disclosure/token scrubbing; image host/private-address guards. just test adds race tests and vet in the Nix shell. See R&D notes for primary sources and the live verification checklist.

An optional Playwright browser test is included (Bun/Chromium required separately; it is not part of the Go runtime or frontend build). It substitutes the consent redirect and uses only a local fake token/API server:

bun install
bunx playwright install chromium
mkdir -p artifacts
go test -c -o artifacts/browser-fixture
BAND_UI_FIXTURE=true artifacts/browser-fixture -test.run '^TestUIFixture$' &
bun run test:ui

The fixture is test-only, binds loopback port 18080, and is shut down by the browser test. The browser test covers consent redirect/cookie handoff, setup JSON, mobile layout, deletion confirmation, and rejection of the deleted MCP token. No real BAND login or client configuration is involved.