Arena Fighter

Read as Markdown

Arena MCP: connect an AI agent

Every hosted bot belongs to a user account. MCP is one of the complete onboarding paths; its browser flow creates the user and chooses a handle before consenting to bot and event access. Existing bots can be claimed on the account page with their token; MCP registration does not claim or transfer them.

When GET https://arena.victris.ai/api reports a non-null mcp, add that URL to your AI client's remote MCP settings. The transport is Streamable HTTP. It opens a browser to sign in with Google, GitHub or email, choose a public handle, and approve the client. Email signup still requires confirmation in the browser that requested it. After signup, the pending connection is restored; account passwords are never sent to an MCP tool.

The client can read your handle and bot list, register new bots within your account's quota, delete a bot that has never played, retire an owned bot while preserving its history, and inspect or join event ladders under the same admission rules as the browser. The agent must ask for your explicit approval before retiring a specific bot or joining an event. My Bots (/account) lists connected clients and lets you disconnect them. Arena rejects their existing OAuth tokens immediately after disconnection. Bot tokens they already received remain valid: rotate those separately if needed.

An MCP server does not run code on your computer. Use a local coding agent or the CLI to download a starter, write and test your bot, and run it with its own WebSocket token. Clients without remote MCP/OAuth support can keep using an agent key and the HTTP API.

Without a browser: an agent key

A client that can't open a browser for OAuth (over SSH, in a container) can send one of your agent keys from My Bots instead, as Authorization: Bearer <key>. Making the key is the approval: new keys have the same tools and quota, and work until you revoke them on My Bots. Keys created before account actions were introduced retain registration access; create a new key to authorize MCP retirement and enrollment. For Claude Code:

claude mcp add --transport http arena https://arena.victris.ai/mcp --header "Authorization: Bearer <agent key>"

Keep the key out of shared config and source control, as you would a bot token. An admin's key is also an admin credential for the HTTP API: keep it off machines you don't trust. An authorized operator agent can read, preview and apply live limits with the HTTP settings endpoints or arena admin settings, using that admin key without a browser. See agent settings administration. These are HTTP/CLI operations; the MCP tools below retain their documented player scope.

Tools

tool input result
whoami none handle, bot limit, number of bots counting against it (all but retired ones), account page URL and account_actions (whether retirement/enrollment consent is present)
list_my_bots none this account's public bot records with status and played; no tokens
register_bot {"name":"my-bot"} name, secret token, bot WebSocket endpoint, storage note
delete_bot {"name":"my-bot"} {name, outcome: "deleted"} for a bot with no match on file; bot_has_played otherwise (use retire_bot with approval). Marked destructive, so clients ask first
retire_bot {"name":"my-bot","confirmed":true} {name, outcome: "retired"}; frees quota, stops play and keeps name, versions and history, including for unplayed bots. Destructive and idempotent; ask the owner first
list_my_ladders {"ladder":"optional-id"} or none {ladders} with definitions, your membership (null, pending, active or revoked), enrollment_active and exact enrollment_url; a supplied ID includes an unlisted ladder. Unknown IDs return unknown_ladder
enroll_ladder {"ladder":"event-id","confirmed":true,"invitation":"optional code"} {ladders} with current memberships. Open becomes active, approval stays pending, invite requires a valid code. Ask the owner first; repeated enrollment is safe
get_agent_guide none authoring guide, starter download index and simulator links

Save a new bot's token in a private environment file; never commit it or print it in shared logs. Tokens are shown at creation only and stored hashed. Registration is not idempotent: if a response is lost, inspect My Bots instead of creating another identity or automatically rotating a token. A duplicate name fails without changing the bot. For a chat-only client, copy the new token to the machine where you will run the bot.

retire_bot and enroll_ladder require both account-actions consent and confirmed: true. The flag records the client's assertion of the owner's approval; it is not a separate human approval channel. Clients must obtain explicit approval for the named bot/event before calling. Existing OAuth grants and agent keys keep their previous write scope. If whoami reports account_actions: false, these tools return mcp_account_actions_required: disconnect and reconnect the OAuth client to review the updated browser consent, or create a new agent key. Read-only membership discovery remains available, including a browser enrollment link.

Retirement never deletes played history or releases the name, and repeating it does not add another retirement audit entry. Suspended/banned bots return bot_moderated; another account's bot or a human seat returns no_such_bot. bot_limit points to retirement as a way to release quota. Claiming bots and token/version management remain available on My Bots or their documented HTTP routes.

Before starting an event bot, call list_my_ladders with the intended ladder ID. Public needs no membership. For an event, enrollment_active: true means admission is active; the schedule and enabled state still govern queueing. Pending admission needs staff approval. Revoked membership includes the staff reason and cannot be self-restored. Enrollment preserves invitation_required, invalid_invitation, membership_revoked and ladder_closed errors, and permits preregistration before opening. Browser account routes still reject MCP credentials; enrollment runs directly through the shared policy.

Operator setup

MCP is off by default. Implementing the endpoint does not change your Supabase project's settings. Use a staging project first; Supabase's OAuth server is currently beta.

  1. Configure the existing Supabase sign-in methods and asymmetric JWT signing keys (ES256 or RS256), as for ordinary Arena accounts.
  2. Enable Authentication → OAuth Server. Allow dynamic client registration for clients that need it, or pre-register the clients you support. Registering an OAuth client registers the AI application, not a player account.
  3. Set the Auth Site URL to the ladder's public origin and the authorization/consent path to /oauth/consent. Keep /account in the allowed sign-in redirect URLs; redirects also carry next to preserve pending consent. Production uses HTTPS.
  4. Install the audience hook, replacing its example audience with the canonical public MCP URL (the ladder's --public-url plus /mcp). Select public.arena_mcp_access_token under Authentication → Hooks → Custom Access Token. If a hook already exists, merge the audience logic into it. The sample binds every OAuth-server client's access tokens to this Arena MCP resource, so use a dedicated project or adapt it if other applications use this OAuth server. Normal Google/GitHub/email browser sessions retain the authenticated audience.
  5. Set ARENA_MCP_ENABLED=1 on the ladder, or use arena serve --mcp. Set its canonical --public-url; discovery and JWT validation use it, never request Host headers. The production Docker Compose file forwards this runtime variable. In Coolify, set it on the production application and deploy the release containing MCP; changing the environment alone does not update the running container.
  6. Test discovery, browser consent, registration and revocation with the MCP Inspector and the actual AI clients you intend to support before enabling it for players.

For local development, the hook's example audience is https://arena.victris.ai/mcp. With Supabase Auth configured separately, start npm run arena -- serve --mcp. The account and consent pages are built by Vite and served by the ladder, as all other Arena pages are. npm run dev:web proxies MCP and discovery to the local ladder. Its frontend origin must match the configured Auth Site URL and the ladder public URL for browser consent requests; serving the built pages is simpler for OAuth testing.

npx @modelcontextprotocol/inspector

Choose Streamable HTTP and connect to https://arena.victris.ai/mcp. Verify a fresh signup, an existing account, cancellation, handle conflicts, duplicate bot names, the bot limit, and disconnection. Repository tests use the official MCP SDK, signed JWT fixtures and a fake Supabase OAuth API; they do not verify a live project's settings or every client.

Authentication and permissions

The MCP endpoint publishes /.well-known/oauth-protected-resource/mcp (also available at /.well-known/oauth-protected-resource). A missing/invalid token gets a 401 and a WWW-Authenticate challenge pointing there. Metadata names the Supabase Auth issuer.

MCP requires either one of the account's agent keys, or a signed Supabase access token whose audience includes the canonical MCP URL and whose signed client_id identifies a locally approved client for that user. Browser sessions cannot call MCP; OAuth-client tokens cannot call any /api/account route, even if they also carry the ordinary authenticated audience. Tool calls invoke the same registration logic as browser and agent-key requests, with no service-role key or token forwarding to another API.

Supabase currently supports identity scopes (openid, email, profile, phone), not custom scopes such as bots:create. Arena stores its bot/event permission in SQLite, separately for each account/client pair, and checks it on every MCP request and tool call. The consent page verifies client identity against Supabase before saving permission. Its routes accept browser JWTs, not cookies or user-supplied client IDs.

Revocation first disables the local grant, then asks Supabase to revoke OAuth consent. If Supabase is unavailable, Arena access is still disabled; the page reports that OAuth cleanup failed. Clear that client's OAuth grant in Supabase before reconnecting if it auto-approves without displaying consent. Reapproval never revives tokens issued before revocation. Because JWT timestamps use seconds, a new token must be issued after the revocation second; refresh/reconnect if an immediate retry is refused.

The transport is stateless and POST-only: no persistent MCP session or background SSE channel. It returns JSON responses, rejects foreign browser origins, bounds input bodies at 16 KiB, and marks secret-bearing responses no-store. Audit entries identify MCP client IDs (agent_key:<id> for a key) without recording credentials or invitation codes. SQLite schema 18 adds account-actions consent to grants and keys, defaulting existing credentials to registration-only. Older builds reject it: back up before upgrading, and restore a compatible database backup to roll back.

Sources: Supabase MCP deployment, OAuth setup, OAuth scopes, access token hooks, MCP authorization.