# Guildbridge

> 🏰 Remotely hosted MCP server for Discord

- **Type:** MCP server
- **Install:** `agentstack add mcp-dend-guildbridge`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [dend](https://agentstack.voostack.com/s/dend)
- **Installs:** 0
- **Category:** [Communication](https://agentstack.voostack.com/c/communication)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [dend](https://github.com/dend)
- **Source:** https://github.com/dend/guildbridge

## Install

```sh
agentstack add mcp-dend-guildbridge
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

GuildBridge

  
    A remote MCP server for Discord, deployed on Cloudflare Workers.
    
    About
    ·
    Tools
    ·
    Access Control
    ·
    Token Usage
    ·
    Contributing
  

## About

There is no official Discord MCP server, yet much of the coordination with contributors in the MCP community happens on Discord. GuildBridge fills that gap for me — it gives MCP clients authenticated, permission-aware access to Discord servers so that AI agents can read, search, and post messages where the conversation is already happening. It very much came to life on the heels of a problem that _I had_ that I solved by building my own MCP server.

>[!WARNING]
>The actual hosted version of this MCP server is not broadly available (I have restricted it to specific accounts and servers), but you can just as easily configure and deploy it yourself on your Cloudflare account.

>[!NOTE]
>When hosted, this MCP server authenticates users via [Discord OAuth2](https://discord.com/developers/docs/topics/oauth2) and makes all API calls with a [bot token](https://discord.com/developers/docs/reference#authentication). Role-Based Access Control (RBAC) is implemented server-side, as Discord's own auth surface doesn't enable a clean role separation and integration with messaging APIs in its OAuth implementation.

## Prerequisites

- [Node.js](https://nodejs.org/) (v18+)
- A [Cloudflare account](https://dash.cloudflare.com/) (using the free tier is sufficient)
- A [Discord application](https://discord.com/developers/docs/getting-started#creating-an-app) with:
  - A [bot user](https://discord.com/developers/docs/topics/oauth2#bots) added to the servers you want to access
  - [OAuth2](https://discord.com/developers/docs/topics/oauth2) configured (client ID + secret)

## Discord App Setup

1. Go to the [Discord Developer Portal](https://discord.com/developers/applications) and create (or select) an application.
2. Under **Bot**, click "Reset Token" to get your [bot token](https://discord.com/developers/docs/reference#authentication). Save it.
3. Under **OAuth2**, note the **Client ID** and **Client Secret**.
4. Under **OAuth2 > Redirects**, add your callback URL:
   - Local dev: `http://localhost:8788/callback`
   - Production: `https://.workers.dev/callback` (you will get this URI later when you deploy your MCP server to Cloudflare)
5. Under **OAuth2 > Scopes**, ensure [`identify` and `guilds`](https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes) are selected.
6. Under **Bot > Privileged Gateway Intents**, enable [**Message Content Intent**](https://discord.com/developers/docs/events/gateway#message-content-intent) if you want full message content in search results.
7. Invite the bot to your server(s) using the OAuth2 URL Generator with the `bot` scope and these [permissions](https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags): `View Channels`, `Read Message History`, `Send Messages`.

## Local Development

```bash
# Install dependencies
npm install

# Copy the example files and fill in your values
cp wrangler.jsonc.example wrangler.jsonc
cp .dev.vars.example .dev.vars

# Start the dev server
npm run dev
```

The server runs at `http://localhost:8788`. The [MCP endpoint](https://modelcontextprotocol.io/docs/concepts/transports#streamable-http) is at `/mcp`.

### `.dev.vars`

>[!NOTE]
>You will need to fill this out prior to deployment to ensure that the MCP server can actually talk to Discord's APIs.

| Variable | Description |
|---|---|
| `DISCORD_CLIENT_ID` | OAuth2 client ID from Discord Developer Portal |
| `DISCORD_CLIENT_SECRET` | OAuth2 client secret |
| `DISCORD_BOT_TOKEN` | [Bot token](https://discord.com/developers/docs/reference#authentication) (used for all Discord API calls) |
| `COOKIE_ENCRYPTION_KEY` | Random string for signing cookies — generate with `openssl rand -hex 16` |
| `CF_ACCESS_TEAM_DOMAIN` | Cloudflare Access team name — required for the admin panel |
| `CF_ACCESS_AUD` | Cloudflare Access Application Audience (AUD) tag — required for the admin panel |
| `DEV_SKIP_CF_ACCESS` | Set to `true` to bypass CF Access JWT validation in local dev |

## Deploy to Cloudflare

The Worker binds to three stateful Cloudflare resources: a **KV namespace** (OAuth state + allowlist), a **D1 database** (audit log), and a **Zero Trust Access application** (gates `/admin`). You can provision all three at once with Terraform, or create them individually with the wrangler CLI.

### Option A — Terraform

Provisions KV, D1, and the Access app + policy in one shot. Requires a Cloudflare API token with `Workers KV Storage:Edit`, `D1:Edit`, and `Access: Apps and Policies:Edit` scopes.

```bash
cd terraform
cp terraform.tfvars.example terraform.tfvars
# edit terraform.tfvars — set account ID, worker hostname, admin emails

export CLOUDFLARE_API_TOKEN=...
terraform init
terraform apply
```

Wire the outputs into your config:

| Output | Goes into |
|---|---|
| `kv_namespace_id` | `wrangler.jsonc` → `kv_namespaces[0].id` |
| `d1_database_id` | `wrangler.jsonc` → `d1_databases[0].database_id` |
| `d1_database_name` | `wrangler.jsonc` → `d1_databases[0].database_name` |
| `cf_access_aud` | `wrangler secret put CF_ACCESS_AUD` |

Then apply the D1 schema, set the remaining secrets, and deploy:

```bash
cd ..
npx wrangler d1 migrations apply "$(terraform -chdir=terraform output -raw d1_database_name)" --remote
npx wrangler secret bulk .dev.vars
terraform -chdir=terraform output -raw cf_access_aud | npx wrangler secret put CF_ACCESS_AUD
npm run deploy
```

If you used Terraform, skip the **Setup** subsections under [Admin Panel](#admin-panel) and [Observability](#observability) — those resources already exist.

### Option B — Manual (wrangler CLI)

```bash
# Create the KV namespace (https://developers.cloudflare.com/kv/)
npx wrangler kv namespace create OAUTH_KV
```

Copy the output `id` into `wrangler.jsonc` replacing `PLACEHOLDER_KV_ID`. (D1 and Access setup are covered under [Observability](#observability) and [Admin Panel](#admin-panel) below.)

```bash
# Set secrets (https://developers.cloudflare.com/workers/configuration/secrets/)
npx wrangler secret bulk .dev.vars

# Deploy
npm run deploy
```

---

After deploying, [Wrangler](https://developers.cloudflare.com/workers/wrangler/) will print your worker URL (e.g. `https://guildbridge..workers.dev`). Add `https:///callback` as a redirect URI in the Discord Developer Portal.

## Connect an MCP Client

Point any [MCP-compatible client](https://modelcontextprotocol.io/clients) at the server URL:

```
https://.workers.dev/mcp
```

Or locally:

```
http://localhost:8788/mcp
```

To test with the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector):

```bash
npx @modelcontextprotocol/inspector@latest
```

Enter the URL above, complete the Discord OAuth flow, and the tools will become available.

## Tools

| Tool | Description |
|---|---|
| `list_guilds` | List Discord servers you are in |
| `list_channels` | List channels in a server (optionally filtered by type) |
| `get_channel_info` | Get channel details (topic, type, etc.) |
| `read_messages` | Read messages from a channel (with pagination) |
| `search_messages` | Search messages in a server (by content, channel, author) |
| `send_message` | Send a message to a channel |
| `reply_to_message` | Reply to a specific message |

## Admin Panel

The admin panel at `/admin` lets you add and remove allowed Discord users at runtime, without redeploying. The allowlist is stored in KV and is the sole source the OAuth callback checks. The allowlist is **fail-closed** — an empty list rejects everyone, so you must seed at least one user via `/admin` before the first OAuth login will succeed.

### Setup

1. In the [Cloudflare Zero Trust dashboard](https://one.dash.cloudflare.com/), create an **Access Application** for `/admin*`.
2. Configure an identity provider (email OTP, Google, etc.).
3. Copy the **Application Audience (AUD)** tag and set it as the `CF_ACCESS_AUD` secret.
4. Set the `CF_ACCESS_TEAM_DOMAIN` secret to your Zero Trust team name.

Once deployed, visit `https://.workers.dev/admin` to manage the allowlist.

For local development, set `DEV_SKIP_CF_ACCESS=true` in `.dev.vars` to bypass CF Access JWT validation, then visit `http://localhost:8788/admin`.

## Observability

Every MCP tool invocation is audited. Events are dual-written to **D1** (ordered audit trail, queryable from the admin panel's Activity tab) and **Analytics Engine** (fire-and-forget metrics, queried via the Cloudflare dashboard SQL API).

**Captured per event:** timestamp, tool name, Discord user ID + username, outcome (ok/error), duration, `guild_id` (when present), `channel_id` (when present), created `message_id` (for `send_message`/`reply_to_message`), error message (on failure). Message content and search queries are never captured.

### Setup

```bash
# Create the D1 database (one-time)
npx wrangler d1 create guildbridge-audit
```

Copy the output `database_id` into `wrangler.jsonc` replacing `PLACEHOLDER_D1_ID`, then apply the schema:

```bash
# Local dev
npx wrangler d1 migrations apply guildbridge-audit --local

# Production
npx wrangler d1 migrations apply guildbridge-audit --remote
```

Analytics Engine requires no setup — the `TOOL_AUDIT` binding in `wrangler.jsonc` is enough. In local dev, `writeDataPoint` is a no-op stub; it only writes when deployed.

### Querying

**Admin panel:** `https://.workers.dev/admin` → Activity tab. Filter by tool or user ID.

**D1 directly:**

```bash
npx wrangler d1 execute guildbridge-audit --command \
  "SELECT * FROM audit_log ORDER BY ts DESC LIMIT 20"
```

**Analytics Engine** (aggregates):

```bash
npx wrangler analytics-engine sql \
  "SELECT blob1 AS tool, count() AS calls, avg(double1) AS avg_ms
   FROM guildbridge_tool_calls
   WHERE timestamp > now() - INTERVAL '7' DAY
   GROUP BY tool"
```

Field mapping: `indexes[0]` = userId, `blobs` = `[tool, username, outcome, guildId, channelId, messageId, error]`, `doubles` = `[durationMs]`.

## Access Control

Every tool call goes through a layered access check before touching the Discord API. Guild membership is verified via the user's OAuth token, and channel visibility is enforced by computing [Discord's permission algorithm](https://discord.com/developers/docs/topics/permissions#permission-overwrites) from the bot's perspective.

```mermaid
flowchart TD
    A[Tool call] --> B{Channel or guild scoped?}

    B -->|Guild scoped| C[assertGuildAccess]
    B -->|Channel scoped| D[assertChannelAccess]

    C --> E[Fetch user guilds via OAuth token]
    E --> F{User is member?}
    F -->|No| G[Access denied]

    D --> H[Fetch channel info via bot token]
    H --> I{Channel in a guild?}
    I -->|No| G
    I -->|Yes| C

    F -->|Yes| J[getGuildPermContext]
    J --> K[Fetch guild roles + member roles + guild info]
    K --> L{User is guild owner?}
    L -->|Yes| M[Access granted]
    L -->|No| N[computePermissions]

    N --> O[Base: @everyone role perms]
    O --> P[OR in member role perms]
    P --> Q{ADMINISTRATOR set?}
    Q -->|Yes| M
    Q -->|No| R[Apply @everyone channel overwrite]
    R --> S[Apply matching role channel overwrites]
    S --> T[Apply member-specific channel overwrite]
    T --> U{VIEW_CHANNEL set?}
    U -->|Yes| M
    U -->|No| G
```

For `list_channels` and `search_messages`, the same [permission computation](https://discord.com/developers/docs/topics/permissions#permission-hierarchy) is applied as a post-filter — channels the user can't see are stripped from results.

## Token Usage

GuildBridge uses two distinct Discord tokens with intentionally separate roles:

| Token | Stored in | Used for |
|---|---|---|
| **Bot token** | Server-side env var (`DISCORD_BOT_TOKEN`) | All Discord API calls — reading messages, sending messages, fetching channels, roles, and members |
| **User OAuth token** | Encrypted inside the MCP access token | Guild membership verification only (`/users/@me/guilds`) |

The bot token never leaves the server. The user's Discord OAuth token is obtained during the [OAuth2 login flow](https://discord.com/developers/docs/topics/oauth2), embedded into an encrypted MCP access token, and returned to the MCP client. GuildBridge does not store the user's token server-side — the MCP client holds the encrypted token and sends it with each request, where it is decrypted to extract the OAuth token for guild membership checks.

```mermaid
sequenceDiagram
    participant Client as MCP Client
    participant Server as GuildBridge
    participant Discord as Discord API

    note over Client,Discord: OAuth Flow (one-time setup)
    Client->>Server: Connect to /mcp
    Server-->>Client: 401 — authenticate via OAuth
    Client->>Server: /authorize
    Server->>Discord: Redirect to Discord OAuth
    Discord-->>Server: /callback with auth code
    Server->>Discord: Exchange code for user OAuth token
    Discord-->>Server: User OAuth token
    Server-->>Client: Encrypted MCP token (contains user OAuth token)

    note over Client,Discord: Tool Calls (ongoing)
    Client->>Server: Tool call + MCP token (Bearer)
    Server->>Server: Decrypt MCP token → extract user OAuth token
    Server->>Discord: Verify guild membership (Bearer user OAuth token)
    Discord-->>Server: User's guild list
    Server->>Discord: Execute tool action (Bot token from env)
    Discord-->>Server: API response
    Server-->>Client: Tool result
```

During the OAuth flow, short-lived session state is managed via:

- **CSRF token** — HTTP-only cookie, validates the approval form submission (600s TTL)
- **State token** — stored in [Cloudflare KV](https://developers.cloudflare.com/kv/), binds the OAuth request across redirects (600s TTL)
- **Approved clients cookie** — HMAC-signed, lets returning users skip the approval dialog (30 days)

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions, code style guidelines, and how to submit changes. Please also review the [AI Usage Policy](AI_POLICY.md) before contributing.

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [dend](https://github.com/dend)
- **Source:** [dend/guildbridge](https://github.com/dend/guildbridge)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-dend-guildbridge
- Seller: https://agentstack.voostack.com/s/dend
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
