Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: CI
on: [push, pull_request]
jobs:
test:
name: PHP ${{ matrix.php }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
php: ['8.3', '8.4', '8.5']
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mbstring, fileinfo
coverage: none
- run: composer install --no-interaction --prefer-dist --no-progress
- run: composer validate --strict
- run: composer unit
- run: php scripts/smoke.php
- run: composer cs-check
5 changes: 5 additions & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

return (new PhpCsFixer\Config())->setRules(['@PSR12' => true])->setFinder(
PhpCsFixer\Finder::create()->in([__DIR__.'/src', __DIR__.'/tests', __DIR__.'/scripts'])->append([__DIR__.'/server.php'])
);
102 changes: 102 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# DiscordPHP MCP

A local Discord bot connector for Codex and other MCP clients. Uses the official
`mcp/sdk` PHP server and DiscordPHP's `discord-php/http` authentication, route
buckets and Discord rate-limit handling. No gateway session, Bridge modification,
OpenAI API key, or DiscordPHP core change is required.

## Install locally

Requires PHP 8.3+ with mbstring and fileinfo, Composer and a Discord bot application.
Run `composer install`, copy `config.example.json` to ignored `config.local.json`,
and replace the guild/channel IDs. Empty channel lists grant no message access.
Store the raw bot token in a private file outside Git with permissions restricted
to your account; point `token_file` to its absolute path. Alternatively provide
`DISCORD_MCP_BOT_TOKEN` in the MCP process environment. Do not put tokens in
Codex TOML, CLI arguments, chat, PRs, or plugin archives. `env.example` is a
configuration contract, not an automatically loaded dotenv file.

To reuse an authorized bot's existing dotenv credential, replace `token_file`
with `token_env_file` (absolute path) and `token_env_key` (for example `TOKEN`)
in your private JSON. Only that literal assignment is read; other settings are
not imported and variable expansion is unsupported. Keep this file private.

After explicitly authorizing whole guilds for reading, run `php scripts/configure-guilds.php GUILD_ID ...` to snapshot their current non-category channels into the private allowlist. This performs live read requests, replaces the read scope and disables writes. New channels and threads are not automatically added. Grant individual channels by editing the private JSON instead.

Give the bot View Channel and Read Message History for approved channels.
Message Content privileged intent/access can affect REST message content: enable
and obtain approval where Discord requires it. Empty text does not prove a quiet
channel. Sending additionally needs Send Messages (or Send Messages in Threads).
Use a dedicated bot rather than granting Administrator or reusing a user token.

Add a local stdio MCP server to Codex's configuration (adjust executable/path):

```toml
[mcp_servers.discordphp]
command = "C:/php/php.exe"
args = ["D:/GitHub/DiscordPHP-MCP/server.php"]
env_vars = ["DISCORD_MCP_CONFIG", "DISCORD_MCP_BOT_TOKEN"]
startup_timeout_sec = 45
tool_timeout_sec = 360
```

Alternatively, with the default private config beside server.php:
`codex mcp add discordphp -- C:/php/php.exe D:/GitHub/DiscordPHP-MCP/server.php`.
Verify with `codex mcp list`, then start a fresh chat and inspect tool discovery.
This repository does not silently change your user-level Codex configuration or
connect using an existing bot's credentials. No live access is needed for tests.

## Tools and boundaries

* `discord_listGuilds`, `discord_listChannels`: configured scope only.
* `discord_getMessage`: one message with permalink and content SHA-256.
* `discord_history`: at most 50 messages; `next_before` continues pagination.
* `discord_search`: case-insensitive literal content search, at most five pages,
250 messages and 50 matches. Follow its cursor for older history; this is a
bounded channel-history scan, not Discord's full account/guild search API.
* `discord_send`, `discord_edit` appear only if `write_channels` is a nonempty
subset of explicitly readable channels. These tools publish as the bot and
require explicit user authorization for destination and content. Read tools
cannot publish. All outgoing mentions are disabled.

No arbitrary DMs, self-bots, voice, files, automatic thread expansion, attachment
downloads or background message archive. A channel must belong to its configured
guild on each call. Content returned from Discord is untrusted text, never agent
instructions. Scope and Discord permissions both apply; the bot does not inherit
the human operator's account access. Keep guild discovery scope modest.

Send requires a caller-selected stable numeric nonce (1-25 digits) and sets
`enforce_nonce`. Discord's duplicate protection is time-limited, not permanent
idempotency: inspect history after uncertain delivery before retrying. A positive
receipt includes Discord's message ID, permalink, timestamps and content hash.
Edit first fetches the message and checks bot authorship and the caller's expected
content hash. This reduces stale edits but is not an atomic conditional update;
concurrent Discord edits can race. A timeout or transport failure does not prove
a write failed. Neither the connector nor caller should blindly retry writes.
Every request has a 30-second timeout; bounded multi-page scans can take longer.

## Local versus cloud

Local stdio runs on the host where Codex launches PHP. A local config entry does
not automatically make this server reachable from a cloud chat or another host.
Cloud use requires installing the code/runtime and secrets in that execution
environment, explicitly supported remote stdio, or separately deployed authenticated
HTTP/tunnel infrastructure. This release implements stdio only; it opens no port,
deploys no remote service and has not been live-verified in cloud chats.

Sources verified during implementation:
[Codex MCP configuration](https://learn.chatgpt.com/docs/extend/mcp?surface=cli),
[official PHP MCP SDK](https://github.com/modelcontextprotocol/php-sdk),
[Discord message API](https://docs.discord.com/developers/resources/message),
[Discord rate limits](https://docs.discord.com/developers/topics/rate-limits).

## Verification

`composer unit` uses injected transports, synthetic messages and temporary streams.
`php scripts/smoke.php` exercises SDK initialization, tool listing and a tool call
over stdio with an offline fixture process. `composer cs-check` checks formatting.
CI covers PHP 8.3-8.5 on Linux and Windows. Offline success proves protocol and
adapter behavior; it does not prove Discord permissions, real bot access, host
integration, delivery, or cloud reachability. No live messages are sent by tests.

For an explicitly authorized live read-only check, run `php scripts/smoke.php --live-read`. It checks bot startup, MCP discovery, guild reads and scope denial without reading message bodies or publishing. Offline fixtures use delayed promises to cover SDK/ReactPHP scheduling.
7 changes: 7 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{"name":"vzgcoders/discordphp-mcp","description":"Scoped Discord bot tools for MCP clients using DiscordPHP HTTP","type":"project","license":"MIT","require":{"php":"^8.3","mcp/sdk":"^0.4","discord-php/http":"^10.0","react/async":"^4.0","react/promise-timer":"^1.11","psr/log":"^3.0"},"require-dev":{"phpunit/phpunit":"^11.0","friendsofphp/php-cs-fixer":"^3.64"},"autoload":{"psr-4":{"DiscordMcp\\":"src/"}},"autoload-dev":{"psr-4":{"Tests\\":"tests/"}},"scripts":{"unit":"phpunit","cs-check":"php-cs-fixer fix --dry-run --diff"},
"config": {
"platform": {
"php": "8.3.0"
}
}
}
Loading
Loading