Skip to main content
Glama

gatebound-support

Python/FastAPI backend for Gatebound's ElevenLabs support agent: MCP tools for the agent, the Discord ticket flow, the post-call webhook, and the knowledge-base sync CLI.

docs/SPEC.md is the contract this service is built against — read it for exact env names, endpoint paths, tool shapes, and the ticket flow. docs/ARCHITECTURE.md has the component map, trust boundaries and failure modes; docs/DECISIONS.md the reasoning (no database access from this stack, MCP instead of webhook tools, ticket links instead of agent-created tickets, knowledge base synced from Git, cheapest models while iterating). This file is just the "how do I run it" quick start.

Live: agent "Gatebound Support" in the text-first widget on gatebound.rosenvall.se; service at gatebound-support.rosenvall.se, deployed from the private homelab GitOps repo.

ElevenLabs agent as code (elevenlabs/)

Everything about the agent lives in the ElevenLabs CLI project layout and is pushed, never edited in the UI:

$env:ELEVENLABS_API_KEY = "..."
cd elevenlabs
python scripts/build_agent_config.py     # prompt + settings -> agent_configs/Gatebound-Support.json
elevenlabs agents push                   # create/update the agent
elevenlabs tests push                    # the six behaviour tests in test_configs/
python scripts/run_agent_tests.py        # run them, table + exit code (add --verbose for transcripts)
python scripts/sync_agent_refs.py        # mirror live knowledge_base + mcp_server_ids into the config
$env:MCP_SECRET = "..."; uv run python scripts/probe_mcp.py   # talk to the deployed /mcp like ElevenLabs does

The MCP server entry and its bearer secret are registered once per workspace (see docs/DECISIONS.md); the agent references it by id in mcp_server_ids.

Related MCP server: support-agent-mcp

Run locally

uv sync
copy .env.example .env   # edit as needed; every integration defaults to "unset" (disabled)
uv run gatebound-support serve

The server listens on PORT (default 8080). With everything left unset it still starts and answers /healthz and /status; the ticket flow works without Discord (tickets land as pending_manual), and the MCP endpoint at /mcp rejects all calls until MCP_SECRET is set.

Tests

uv run pytest -q
uv run ruff check .

kb-sync

Syncs a directory of markdown files into an ElevenLabs agent's knowledge base (SPEC §7):

uv run gatebound-support kb-sync --dir path\to\docs --manifest path\to\kb-manifest.json --agent <agent_id>
uv run gatebound-support kb-sync --dir ... --manifest ... --agent ... --dry-run   # print the plan, write nothing
uv run gatebound-support kb-sync --dir ... --manifest ... --agent ... --check     # exit 1 on drift, for CI/CronJob

Requires ELEVENLABS_API_KEY.

Discord voice

gatebound-support voicebot is a second, separate long-lived process (own container command, own Kubernetes Deployment — same image, command: ["gatebound-support", "voicebot"], no HTTP service) that runs the Discord gateway connection and bridges voice:

  • /support or /call (from anywhere — the player does not need to already be in a voice channel): the bot creates a private temporary voice channel (support-<display name>, in the same category as the support text channel if it has one) that only the player, the bot, and roles with Administrator/Manage Guild can see or join — a support call must never be overhearable or joinable by other guild members. The player gets an ephemeral reply with a link to the channel ("join it and I'll pick up"); if they're already in a voice channel and the bot has Move Members, it pulls them straight in instead. A ticket + private Discord thread is opened for the call up front (source discord_voice). The ElevenLabs Conversational AI session — the same agent the website widget talks to, over the SDK's Conversation + a custom AudioInterface (see src/gatebound_support/voicebot/) — only starts once the player actually joins that channel; if nobody joins within 2 minutes the channel is deleted and "Call not started" is posted to the ticket thread. The post-call transcript webhook posts the summary and transcript to the thread when a real call ends, exactly like the website ticket flow.

  • /hangup: ends the current call early.

  • The call also ends when the player leaves the private channel, the agent says goodbye (its own end_call tool), or after VOICE_MAX_MINUTES (default 15) — in every case the temporary voice channel is deleted and "Call ended (<duration>)" is posted to the ticket thread. On startup the bot also sweeps for and deletes any leftover empty support-* channels from a previous crash/restart.

  • One call per Discord server at a time, from the moment /support reserves the guild's slot (channel creation, waiting for join, and the call itself) — a second /support while busy gets an ephemeral "the line is busy" reply.

  • /ticket, and a persistent "Open a ticket" button pinned in the support channel, open a text ticket via a modal (category + description) without going through the ElevenLabs agent at all — same private-thread flow, source discord_text. One open ticket per Discord user at a time; opening a second one just links back to the first.

  • /close, a persistent "Close ticket" button on every ticket's opening post and on the voice call's "Call ended" message, and reacting with ✅ or 🔒 to any bot-authored message inside a ticket thread, all close the ticket the same way: only the ticket's own Discord user or a member with Manage Threads on the parent channel may do it (everyone else gets an ephemeral refusal, or — for a reaction, which has no ephemeral channel — a short message posted in the thread). Closing calls the internal API (status → closed), ends an active voice call for that ticket first if there is one, posts "Ticket closed by <mention>.", renames the thread to [closed] <old name>, and archives + locks it. A closed ticket never blocks its owner from opening a new one.

  • Reaction shortcuts on the pinned instructions message (an alternative to typing slash commands): react, and the bot removes your reaction once it's acted on it.

    Reaction

    Action

    🎫

    Same as /ticket, but without a modal (reactions can't open one): category other, summary "Opened by reaction; the player describes the problem in the thread" — the player is asked to describe the problem in the thread itself. Already has an open ticket → a reminder is posted there instead of opening a new one.

    📞

    Same as /support: creates the private voice channel, ticket and thread, and waits for the join — since there's no ephemeral reply for a reaction, the "join <channel> and I'll pick up" text is posted as the thread's first message instead.

    A per-user in-flight guard ignores a reaction that arrives while that user's previous ticket-creation/call-start reaction is still being handled, so double-tapping doesn't double-fire.

The bot talks to this same service's /internal API (bearer MCP_SECRET, not part of the public contract in docs/SPEC.md) to create, update, and close tickets, since it's a separate process from the one holding SQLite.

Discord bot permissions needed (added when inviting/authorizing the bot, or updating its existing OAuth2 scopes): View Channel, Send Messages, Create Private Threads, Send Messages in Threads, Manage Threads (needed for the text ticket flow and to let Manage-Threads staff close any ticket, not just their own); plus, for voice, Connect, Speak, Use Voice Activity, and — permanent, not optionalManage Channels and Manage Roles (needed to create the private per-call voice channel and set its permission overwrites every time /support runs). Move Members is optional: when granted, a player already in a voice channel is pulled straight into their private call channel instead of having to click the link themselves. The reaction shortcuts additionally want Manage Messages so the bot can remove a player's reaction from the pinned message after acting on it — without it the reaction still triggers the action, it just isn't auto-removed (a Forbidden from the removal attempt is caught and ignored).

Cost note: ElevenLabs bills voice conversation minutes at a materially higher rate than text conversations — a /support call is not "free" the way a widget chat message is.

Run it locally the same way as serve:

uv run gatebound-support voicebot
uv run gatebound-support voicebot --check   # validates settings + opus + imports, no connection, exits 0/1

Docker

docker build --platform linux/amd64 -t gatebound-support:dev .
docker run --rm -p 8080:8080 --env-file .env gatebound-support:dev

Runs as uid 1000, writes SQLite to DATA_DIR (default /data in the container).

Environment variables

See docs/SPEC.md sections 1 and 8 for the full list and what each one is for. Quick reference (all default to "unset", meaning that integration is disabled):

Variable

Purpose

PORT, DATA_DIR, LOG_LEVEL

runtime

PUBLIC_BASE_URL, WEB_API_BASE_URL, WEB_PUBLIC_URL

this service's own URL, the web app's internal API, and its public URL

SUPPORT_API_TOKEN

shared secret for calls to the web support API

SUPPORT_IDENTITY_SECRET

HS256 key for the player identity JWT

MCP_SECRET

bearer secret ElevenLabs sends on /mcp

ELEVENLABS_API_KEY, ELEVENLABS_WEBHOOK_SECRET, ELEVENLABS_AGENT_ID

ElevenLabs API access, post-call webhook HMAC key, and agent id

DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_BOT_TOKEN, DISCORD_GUILD_ID, DISCORD_SUPPORT_CHANNEL_ID, DISCORD_STAFF_WEBHOOK_URL

the ticket flow's Discord integration (DISCORD_BOT_TOKEN/DISCORD_GUILD_ID/DISCORD_SUPPORT_CHANNEL_ID are also what the voicebot process uses)

VOICE_MAX_MINUTES

voicebot: max length of a /support call in minutes (default 15)

SUPPORT_INTERNAL_URL

voicebot: in-cluster base URL of this service's own /internal API (default assumes the standard gatebound-support namespace/service names)

Copy .env.example to .env and fill in what you need; everything else can stay unset.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables MCP clients to drive Relay, an AI support-triage agent, by exposing tools for customer lookup, documentation search, ticket classification, reply, and escalation, with full guardrails and read-only mode option.
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Exposes order status lookup and knowledge base search tools from the Support Agent AI over MCP, enabling MCP clients to handle customer support queries with grounded, citation-backed answers.
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI assistants to search and retrieve support tickets, add notes, close tickets, and access SLA policies through MCP tools, resources, and prompts over Streamable HTTP.
    MIT