Merge pull request #19 from SootyOwl/refactor/streamline-server

Streamline and unify server creation, self-documenting tools.
This commit is contained in:
Barry Yip 2026-03-17 22:19:39 +08:00 committed by GitHub
commit ef0391f862
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 744 additions and 200 deletions

View File

@ -19,7 +19,7 @@ RUN npm run build
# Expose HTTP port # Expose HTTP port
EXPOSE 8080 EXPOSE 8080
# entrypoint to run the MCP server
ENTRYPOINT ["node", "build/app.js"]
# Default command to run the MCP server with HTTP transport # Default command to run the MCP server with HTTP transport
# Use ENTRYPOINT + CMD for flexibility CMD ["--transport", "http", "--port", "8080"]
ENTRYPOINT ["node", "build/index.js"]
CMD ["--transport", "stdio", "--port", "8080"]

14
Makefile Normal file
View File

@ -0,0 +1,14 @@
.PHONY: help
.DEFAULT_GOAL := help
# initialise .env file for all targets if it exists
-include .env
help: ## Show this help.
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
# Document each target that should appear in help using a comment placed after the target's colon, starting with '##' (as shown above).
# Targets without such a comment will not appear in the help output.
run-inspector: ## Run the MCP inspector.
@npx @modelcontextprotocol/inspector -e DISCORD_TOKEN=$(DISCORD_TOKEN)

View File

@ -293,6 +293,9 @@ You can use Docker containers with both Claude and Cursor:
- `discord_create_text_channel`: Create a text channel - `discord_create_text_channel`: Create a text channel
- `discord_delete_channel`: Delete a channel - `discord_delete_channel`: Delete a channel
- `discord_create_category`: Create a channel category
- `discord_edit_category`: Edit a channel category
- `discord_delete_category`: Delete a channel category
### Forum Functions ### Forum Functions

318
package-lock.json generated
View File

@ -42,15 +42,15 @@
} }
}, },
"node_modules/@discordjs/builders": { "node_modules/@discordjs/builders": {
"version": "1.11.2", "version": "1.12.2",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.11.2.tgz", "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.12.2.tgz",
"integrity": "sha512-F1WTABdd8/R9D1icJzajC4IuLyyS8f3rTOz66JsSI3pKvpCAtsMBweu8cyNYsIyvcrKAVn9EPK+Psoymq+XC0A==", "integrity": "sha512-AugKmrgRJOHXEyMkABH/hXpAmIBKbYokCEl9VAM4Kh6FvkdobQ+Zhv7AR6K+y5hS7+VQ7gKXPYCe1JQmV07H1g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@discordjs/formatters": "^0.6.1", "@discordjs/formatters": "^0.6.1",
"@discordjs/util": "^1.1.1", "@discordjs/util": "^1.1.1",
"@sapphire/shapeshift": "^4.0.0", "@sapphire/shapeshift": "^4.0.0",
"discord-api-types": "^0.38.1", "discord-api-types": "^0.38.26",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.4", "ts-mixer": "^6.0.4",
"tslib": "^2.6.3" "tslib": "^2.6.3"
@ -87,9 +87,9 @@
} }
}, },
"node_modules/@discordjs/rest": { "node_modules/@discordjs/rest": {
"version": "2.5.0", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz",
"integrity": "sha512-PWhchxTzpn9EV3vvPRpwS0EE2rNYB9pvzDU/eLLW3mByJl0ZHZjHI2/wA8EbH2gRMQV7nu+0FoDF84oiPl8VAQ==", "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@discordjs/collection": "^2.1.1", "@discordjs/collection": "^2.1.1",
@ -97,10 +97,10 @@
"@sapphire/async-queue": "^1.5.3", "@sapphire/async-queue": "^1.5.3",
"@sapphire/snowflake": "^3.5.3", "@sapphire/snowflake": "^3.5.3",
"@vladfrangu/async_event_emitter": "^2.4.6", "@vladfrangu/async_event_emitter": "^2.4.6",
"discord-api-types": "^0.38.1", "discord-api-types": "^0.38.16",
"magic-bytes.js": "^1.10.0", "magic-bytes.js": "^1.10.0",
"tslib": "^2.6.3", "tslib": "^2.6.3",
"undici": "6.21.1" "undici": "6.21.3"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@ -134,13 +134,13 @@
} }
}, },
"node_modules/@discordjs/ws": { "node_modules/@discordjs/ws": {
"version": "1.2.2", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.2.tgz", "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz",
"integrity": "sha512-dyfq7yn0wO0IYeYOs3z79I6/HumhmKISzFL0Z+007zQJMtAFGtt3AEoq1nuLXtcunUE5YYYQqgKvybXukAK8/w==", "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@discordjs/collection": "^2.1.0", "@discordjs/collection": "^2.1.0",
"@discordjs/rest": "^2.5.0", "@discordjs/rest": "^2.5.1",
"@discordjs/util": "^1.1.0", "@discordjs/util": "^1.1.0",
"@sapphire/async-queue": "^1.5.2", "@sapphire/async-queue": "^1.5.2",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
@ -179,9 +179,9 @@
} }
}, },
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -197,15 +197,17 @@
} }
}, },
"node_modules/@modelcontextprotocol/sdk": { "node_modules/@modelcontextprotocol/sdk": {
"version": "1.11.0", "version": "1.20.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.1.tgz",
"integrity": "sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==", "integrity": "sha512-j/P+yuxXfgxb+mW7OEoRCM3G47zCTDqUPivJo/VzpjbG8I9csTXtOprCf5FfOfHK4whOJny0aHuBEON+kS7CCA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ajv": "^6.12.6",
"content-type": "^1.0.5", "content-type": "^1.0.5",
"cors": "^2.8.5", "cors": "^2.8.5",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.5",
"eventsource": "^3.0.2", "eventsource": "^3.0.2",
"eventsource-parser": "^3.0.0",
"express": "^5.0.1", "express": "^5.0.1",
"express-rate-limit": "^7.5.0", "express-rate-limit": "^7.5.0",
"pkce-challenge": "^5.0.0", "pkce-challenge": "^5.0.0",
@ -279,9 +281,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/body-parser": { "node_modules/@types/body-parser": {
"version": "1.19.5", "version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -300,9 +302,9 @@
} }
}, },
"node_modules/@types/express": { "node_modules/@types/express": {
"version": "5.0.1", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz",
"integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -312,9 +314,9 @@
} }
}, },
"node_modules/@types/express-serve-static-core": { "node_modules/@types/express-serve-static-core": {
"version": "5.0.6", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz",
"integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -325,9 +327,9 @@
} }
}, },
"node_modules/@types/http-errors": { "node_modules/@types/http-errors": {
"version": "2.0.4", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -339,18 +341,19 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.17.50", "version": "20.19.22",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.50.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.22.tgz",
"integrity": "sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A==", "integrity": "sha512-hRnu+5qggKDSyWHlnmThnUqg62l29Aj/6vcYgUaSFL9oc7DVjeWEQN3PRgdSc6F8d9QRMWkf36CLMch1Do/+RQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~6.19.2" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/qs": { "node_modules/@types/qs": {
"version": "6.9.18", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -362,26 +365,36 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/send": { "node_modules/@types/send": {
"version": "0.17.4", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/mime": "^1",
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/serve-static": { "node_modules/@types/serve-static": {
"version": "1.15.7", "version": "1.15.9",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz",
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/http-errors": "*", "@types/http-errors": "*",
"@types/node": "*", "@types/node": "*",
"@types/send": "*" "@types/send": "<1"
}
},
"node_modules/@types/serve-static/node_modules/@types/send": {
"version": "0.17.5",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
"integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
} }
}, },
"node_modules/@types/ws": { "node_modules/@types/ws": {
@ -394,9 +407,9 @@
} }
}, },
"node_modules/@vladfrangu/async_event_emitter": { "node_modules/@vladfrangu/async_event_emitter": {
"version": "2.4.6", "version": "2.4.7",
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
"integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==", "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=v14.0.0", "node": ">=v14.0.0",
@ -417,9 +430,9 @@
} }
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.14.1", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@ -442,6 +455,22 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/arg": { "node_modules/arg": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
@ -529,9 +558,9 @@
} }
}, },
"node_modules/cookie": { "node_modules/cookie": {
"version": "0.7.1", "version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@ -581,9 +610,9 @@
} }
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
@ -617,33 +646,33 @@
} }
}, },
"node_modules/discord-api-types": { "node_modules/discord-api-types": {
"version": "0.38.8", "version": "0.38.30",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.8.tgz", "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.30.tgz",
"integrity": "sha512-xuRXPD44FcbKHrQK15FS1HFlMRNJtsaZou/SVws18vQ7zHqmlxyDktMkZpyvD6gE2ctGOVYC/jUyoMMAyBWfcw==", "integrity": "sha512-KhAqlBrg+rVK+Ob7INMF5o63yW4/GUzRatG/AjyVsIO8lgcLyR8qCl2HokIVzWwmzkJYG0CEPXsKMOqau3E8NA==",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"scripts/actions/documentation" "scripts/actions/documentation"
] ]
}, },
"node_modules/discord.js": { "node_modules/discord.js": {
"version": "14.19.3", "version": "14.23.2",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.19.3.tgz", "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.23.2.tgz",
"integrity": "sha512-lncTRk0k+8Q5D3nThnODBR8fR8x2fM798o8Vsr40Krx0DjPwpZCuxxTcFMrXMQVOqM1QB9wqWgaXPg3TbmlHqA==", "integrity": "sha512-tU2NFr823X3TXEc8KyR/4m296KLxPai4nirN3q9kHCpY4TKj96n9lHZnyLzRNMui8EbL07jg9hgH2PWWfKMGIg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@discordjs/builders": "^1.11.2", "@discordjs/builders": "^1.12.1",
"@discordjs/collection": "1.5.3", "@discordjs/collection": "1.5.3",
"@discordjs/formatters": "^0.6.1", "@discordjs/formatters": "^0.6.1",
"@discordjs/rest": "^2.5.0", "@discordjs/rest": "^2.6.0",
"@discordjs/util": "^1.1.1", "@discordjs/util": "^1.1.1",
"@discordjs/ws": "^1.2.2", "@discordjs/ws": "^1.2.3",
"@sapphire/snowflake": "3.5.3", "@sapphire/snowflake": "3.5.3",
"discord-api-types": "^0.38.1", "discord-api-types": "^0.38.29",
"fast-deep-equal": "3.1.3", "fast-deep-equal": "3.1.3",
"lodash.snakecase": "4.1.1", "lodash.snakecase": "4.1.1",
"magic-bytes.js": "^1.10.0", "magic-bytes.js": "^1.10.0",
"tslib": "^2.6.3", "tslib": "^2.6.3",
"undici": "6.21.1" "undici": "6.21.3"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@ -653,9 +682,9 @@
} }
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.5.0", "version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@ -739,21 +768,21 @@
} }
}, },
"node_modules/eventsource": { "node_modules/eventsource": {
"version": "3.0.5", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.5.tgz", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
"integrity": "sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==", "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"eventsource-parser": "^3.0.0" "eventsource-parser": "^3.0.1"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/eventsource-parser": { "node_modules/eventsource-parser": {
"version": "3.0.0", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
"integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==", "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
@ -764,6 +793,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
@ -802,9 +832,9 @@
} }
}, },
"node_modules/express-rate-limit": { "node_modules/express-rate-limit": {
"version": "7.5.0", "version": "7.5.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
"integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 16" "node": ">= 16"
@ -813,7 +843,7 @@
"url": "https://github.com/sponsors/express-rate-limit" "url": "https://github.com/sponsors/express-rate-limit"
}, },
"peerDependencies": { "peerDependencies": {
"express": "^4.11 || 5 || ^5.0.0-beta.1" "express": ">= 4.11"
} }
}, },
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
@ -822,6 +852,12 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"license": "MIT"
},
"node_modules/finalhandler": { "node_modules/finalhandler": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
@ -955,6 +991,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/http-errors/node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -994,6 +1039,12 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"license": "MIT"
},
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@ -1146,12 +1197,13 @@
} }
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "8.2.0", "version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
"integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"license": "MIT", "license": "MIT",
"engines": { "funding": {
"node": ">=16" "type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/pkce-challenge": { "node_modules/pkce-challenge": {
@ -1176,6 +1228,15 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.14.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
@ -1201,18 +1262,34 @@
} }
}, },
"node_modules/raw-body": { "node_modules/raw-body": {
"version": "3.0.0", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "3.1.2",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"iconv-lite": "0.6.3", "iconv-lite": "0.7.0",
"unpipe": "1.0.0" "unpipe": "1.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.10"
}
},
"node_modules/raw-body/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/router": { "node_modules/router": {
@ -1394,9 +1471,9 @@
} }
}, },
"node_modules/statuses": { "node_modules/statuses": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
@ -1482,11 +1559,12 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.8.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -1496,18 +1574,18 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "6.21.1", "version": "6.21.3",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18.17" "node": ">=18.17"
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.19.8", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/unpipe": { "node_modules/unpipe": {
@ -1519,6 +1597,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/v8-compile-cache-lib": { "node_modules/v8-compile-cache-lib": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
@ -1557,9 +1644,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.18.2", "version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
@ -1588,18 +1675,19 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "3.25.13", "version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.13.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-Q8mvk2iWi7rTDfpQBsu4ziE7A6AxgzJ5hzRyRYQkoV3A3niYsXVwDaP1Kbz3nWav6S+VZ6k2OznFn8ZyDHvIrg==", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
}, },
"node_modules/zod-to-json-schema": { "node_modules/zod-to-json-schema": {
"version": "3.24.5", "version": "3.24.6",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
"integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
"license": "ISC", "license": "ISC",
"peerDependencies": { "peerDependencies": {
"zod": "^3.24.1" "zod": "^3.24.1"

View File

@ -11,7 +11,9 @@
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc", "build": "tsc",
"start": "node build/index.js", "start": "node build/index.js",
"start-app": "node build/app.js",
"dev": "node --loader ts-node/esm src/index.ts", "dev": "node --loader ts-node/esm src/index.ts",
"dev-app": "node --loader ts-node/esm src/app.ts",
"test-api": "node test-api.js", "test-api": "node test-api.js",
"prepublishOnly": "npm run build" "prepublishOnly": "npm run build"
}, },

344
src/app.ts Normal file
View File

@ -0,0 +1,344 @@
import { InMemoryEventStore } from '@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest, SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { randomUUID } from 'crypto';
import { Client, GatewayIntentBits } from 'discord.js';
import { config as dotenvConfig } from 'dotenv';
import express, { Request, Response } from 'express';
import { info, Level, setLevel, warning } from './notifications.js';
import * as schemas from './schemas.js';
import * as handlers from './tools/tools.js';
import { createToolContext } from './tools/tools.js';
// Load environment variables from .env file if exists
dotenvConfig();
/**
* Create and return a configured MCP server wired with the Discord client.
* The server registers tools programmatically using the handlers and schemas
* exported from the local modules to avoid a large switch statement.
*/
function createMcpServer(client: Client) {
const server = new McpServer(
{ name: 'discord-mcp-server', version: '1.0.0' },
{ capabilities: { tools: {}, logging: {} } }
);
// attach the Discord client to the tool context for use in handlers
const toolContext = createToolContext(client);
// attach the Discord client to the server for logging purposes
// The underlying Server type from the SDK doesn't declare a `client` property,
// so assert to `any` to attach a runtime-only property used by handlers.
(client as any).server = server.server;
(server as any).discord = client;
// Map of tool name to handler + schema. Keep this small and explicit so it's
// easy to see which handler is wired to which tool. Use the exported
// Zod schemas from `schemas.ts` where available.
const toolMap: Array<{ name: string; schema?: any; handler: any }> = [
{ name: 'discord_login', schema: schemas.DiscordLoginSchema, handler: handlers.loginHandler },
{ name: 'discord_list_servers', schema: schemas.ListServersSchema, handler: handlers.listServersHandler },
{ name: 'discord_send', schema: schemas.SendMessageSchema, handler: handlers.sendMessageHandler },
{ name: 'discord_get_server_info', schema: schemas.GetServerInfoSchema, handler: handlers.getServerInfoHandler },
{ name: 'discord_create_text_channel', schema: schemas.CreateTextChannelSchema, handler: handlers.createTextChannelHandler },
{ name: 'discord_delete_channel', schema: schemas.DeleteChannelSchema, handler: handlers.deleteChannelHandler },
{ name: 'discord_get_forum_channels', schema: schemas.GetForumChannelsSchema, handler: handlers.getForumChannelsHandler },
{ name: 'discord_create_forum_post', schema: schemas.CreateForumPostSchema, handler: handlers.createForumPostHandler },
{ name: 'discord_get_forum_post', schema: schemas.GetForumPostSchema, handler: handlers.getForumPostHandler },
{ name: 'discord_reply_to_forum', schema: schemas.ReplyToForumSchema, handler: handlers.replyToForumHandler },
{ name: 'discord_delete_forum_post', schema: schemas.DeleteForumPostSchema, handler: handlers.deleteForumPostHandler },
{ name: 'discord_search_messages', schema: schemas.SearchMessagesSchema, handler: handlers.searchMessagesHandler },
{ name: 'discord_read_messages', schema: schemas.ReadMessagesSchema, handler: handlers.readMessagesHandler },
{ name: 'discord_add_reaction', schema: schemas.AddReactionSchema, handler: handlers.addReactionHandler },
{ name: 'discord_add_multiple_reactions', schema: schemas.AddMultipleReactionsSchema, handler: handlers.addMultipleReactionsHandler },
{ name: 'discord_remove_reaction', schema: schemas.RemoveReactionSchema, handler: handlers.removeReactionHandler },
{ name: 'discord_delete_message', schema: schemas.DeleteMessageSchema, handler: handlers.deleteMessageHandler },
{ name: 'discord_create_webhook', schema: schemas.CreateWebhookSchema, handler: handlers.createWebhookHandler },
{ name: 'discord_send_webhook_message', schema: schemas.SendWebhookMessageSchema, handler: handlers.sendWebhookMessageHandler },
{ name: 'discord_edit_webhook', schema: schemas.EditWebhookSchema, handler: handlers.editWebhookHandler },
{ name: 'discord_delete_webhook', schema: schemas.DeleteWebhookSchema, handler: handlers.deleteWebhookHandler },
{ name: 'discord_create_category', schema: schemas.CreateCategorySchema, handler: handlers.createCategoryHandler },
{ name: 'discord_edit_category', schema: schemas.EditCategorySchema, handler: handlers.editCategoryHandler },
{ name: 'discord_delete_category', schema: schemas.DeleteCategorySchema, handler: handlers.deleteCategoryHandler },
];
// Register each tool on the MCP server
for (const t of toolMap) {
try {
server.tool(
t.name,
t.schema ? t.schema.description ?? '' : '',
t.schema ? t.schema.shape ?? t.schema : undefined, async (args: any) => {
// Handlers follow the signature: (args, context) => Promise<ToolResponse>
return await t.handler(args, toolContext);
});
} catch (err) {
// If a particular tool registration fails, log and continue (keeps boot resilient)
// eslint-disable-next-line no-console
warning(server.server, `Failed to register tool ${t.name}: ${String(err)}`);
}
}
// Handle logging method separately
server.server.setRequestHandler(SetLevelRequestSchema, async (request) => {
const levelParam = request.params.level as Level | undefined;
const ok = setLevel(levelParam || 'info');
// Empty result on success, error on failure
if (!ok) {
throw { code: -32602, message: 'Invalid log level' };
}
return {};
});
return server;
}
// Helper to create a Discord client with sensible defaults used by the CLI
function createDiscordClient(token?: string) {
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent] });
if (token) {
client.token = token;
// Auto-login on startup if token is available
(async () => {
try {
await client.login(token);
info((client as any).server, 'Successfully logged in to Discord');
} catch (err: any) {
if (typeof err.message === 'string' && err.message.includes('Privileged intent provided is not enabled or whitelisted')) {
warning((client as any).server, 'Discord login failed due to missing privileged intents. Please enable the required intents in the Discord Developer Portal.');
} else {
warning((client as any).server, 'Discord login failed: ' + String(err));
}
}
})();
}
let clientStatusInterval: NodeJS.Timeout | null = null;
function logClientState(context: string) {
// only log if server is attached and connected
if (!(client as any).server || !(client as any).server.transport) {
return;
}
info((client as any).server, `Discord client state [${context}]: ${JSON.stringify({
isReady: client.isReady(),
hasToken: !!client.token,
user: client.user ? {
id: client.user.id,
tag: client.user.tag,
} : null
})}`);
}
clientStatusInterval = setInterval(() => {
logClientState("periodic check");
}, 10000);
client.on('ready', () => {
logClientState("on ready event");
});
client.on('disconnect', () => {
logClientState("on disconnect event");
if (clientStatusInterval) {
clearInterval(clientStatusInterval);
clientStatusInterval = null;
}
});
return client;
}
// Configuration from command-line arguments or environment variables
const config = {
DISCORD_TOKEN: (() => {
try {
// First check for command-line argument
const tokenIndex = process.argv.indexOf('--config');
if (tokenIndex !== -1 && tokenIndex + 1 < process.argv.length) {
const configArg = process.argv[tokenIndex + 1];
if (typeof configArg === 'string') {
try {
const parsedConfig = JSON.parse(configArg);
return parsedConfig.DISCORD_TOKEN;
} catch (err) {
// If not valid JSON, try using the string directly
return configArg;
}
}
}
// Then try environment variable
return process.env.DISCORD_TOKEN;
} catch (err) {
process.stderr.write('Error parsing configuration: ' + String(err) + '\n');
return null;
}
})(),
TRANSPORT: (() => {
// Check for transport type argument
const transportIndex = process.argv.indexOf('--transport');
if (transportIndex !== -1 && transportIndex + 1 < process.argv.length) {
return process.argv[transportIndex + 1];
}
// Default to stdio
return 'stdio';
})(),
HTTP_PORT: (() => {
// Check for port argument
const portIndex = process.argv.indexOf('--port');
if (portIndex !== -1 && portIndex + 1 < process.argv.length) {
return parseInt(process.argv[portIndex + 1]);
}
// Default port for MCP
return 8080;
})()
};
const discord = createDiscordClient(config.DISCORD_TOKEN);
const server = createMcpServer(discord);
const app = express();
app.use(express.json());
// region MCP Streamable HTTP Transport Handlers
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
const mcpPostHandler = async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string;
if (sessionId) {
process.stderr.write(`Received MCP request for session: ${sessionId}\n`);
} else {
process.stderr.write('Request body:' + JSON.stringify(req.body) + '\n');
}
try {
let transport: StreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// New session initialization
const eventStore = new InMemoryEventStore();
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
eventStore,
enableJsonResponse: true, // Enable JSON response mode
onsessioninitialized: sessionId => {
// Store the transport by session ID for future requests
transports[sessionId] = transport;
process.stderr.write(`Initialized new MCP session: ${sessionId}\n`);
}
});
// Set up onclose handler to clean up transport
transport.onclose = () => {
const sid = transport.sessionId;
if (sid && transports[sid]) {
delete transports[sid];
process.stderr.write(`Closed and removed MCP session: ${sid}\n`);
}
};
// Connect the transport to the MCP server before handling the request
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
return;
} else {
// Invalid request without session ID
res.status(400).json({
jsonrpc: "2.0",
error: { code: -32000, message: "Invalid or missing MCP session ID." },
id: req.body.id || null
});
return;
}
// Handle the request with the existing transport - no need to reconnect
await transport.handleRequest(req, res, req.body);
} catch (err) {
process.stderr.write('Error handling MCP request: ' + String(err) + '\n');
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32000, message: "Internal server error." },
id: req.body.id || null
});
}
}
};
// Handle GET requests for SSE streams (using built-in support from StreamableHTTP)
const mcpGetHandler = async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !transports[sessionId]) {
res.status(400).send('Invalid or missing session ID');
return;
}
// Check for Last-Event-ID header for resumability
const lastEventId = req.headers['last-event-id'] as string | undefined;
if (lastEventId) {
process.stderr.write(`Client reconnecting with Last-Event-ID: ${lastEventId}\n`);
} else {
process.stderr.write(`Establishing new SSE stream for session ${sessionId}\n`);
}
const transport = transports[sessionId];
await transport.handleRequest(req, res);
};
// Handle DELETE requests for session termination (according to MCP spec)
const mcpDeleteHandler = async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !transports[sessionId]) {
res.status(400).send('Invalid or missing session ID');
return;
}
process.stderr.write(`Received session termination request for session ${sessionId}\n`);
try {
const transport = transports[sessionId];
await transport.handleRequest(req, res);
} catch (error) {
process.stderr.write('Error handling session termination:' + String(error) + '\n');
if (!res.headersSent) {
res.status(500).send('Error processing session termination');
}
}
};
// endregion
// Handle both Stdio and HTTP transports based on configuration
if (config.TRANSPORT.toLowerCase() === 'http') {
// HTTP transport endpoints
app.post('/mcp', mcpPostHandler);
app.get('/mcp', mcpGetHandler);
app.delete('/mcp', mcpDeleteHandler);
const port = config.HTTP_PORT || 8080;
app.listen(port, error => {
if (error) {
process.stderr.write('Error starting MCP HTTP server: ' + String(error) + '\n');
process.exit(1);
}
process.stderr.write(`MCP Streamable HTTP Server listening on port ${port}\n`);
});
// Handle server shutdown
process.on('SIGINT', async () => {
process.stderr.write('Shutting down server...\n');
// Close all active transports to properly clean up resources
for (const sessionId in transports) {
try {
process.stderr.write(`Closing transport for session ${sessionId}\n`);
await transports[sessionId].close();
delete transports[sessionId];
} catch (error) {
process.stderr.write(`Error closing transport for session ${sessionId}:` + String(error) + '\n');
}
}
process.stderr.write('Server shutdown complete\n');
process.exit(0);
});
} else {
// Stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
process.stderr.write('MCP Stdio Server started. Awaiting messages...\n');
}

45
src/notifications.ts Normal file
View File

@ -0,0 +1,45 @@
import { Server } from '@modelcontextprotocol/sdk/server'
export type Level = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency';
const levelPriority: Record<Level, number> = {
debug: 0,
info: 1,
notice: 2,
warning: 3,
error: 4,
critical: 5,
alert: 6,
emergency: 7,
};
export let currentLevel: Level = 'info';
export function setLevel(level: Level) {
if (!levelPriority.hasOwnProperty(level)) return false;
currentLevel = level;
return true;
}
export function log(server: Server, message: string, level: Level = 'info') {
if (levelPriority[level] < levelPriority[currentLevel]) return;
server.sendLoggingMessage(
{
level: level,
logger: 'mcp-discord',
data: { text: message },
}
);
}
export function info(server: Server, message: string) {
log(server, message, 'info');
}
export function warning(server: Server, message: string) {
log(server, message, 'warning');
}
export function error(server: Server, message: string) {
log(server, message, 'error');
}

View File

@ -1,28 +1,38 @@
import { z } from "zod"; import { z } from "zod";
export const DiscordLoginSchema = z.object({ export const DiscordLoginSchema = z.object({
token: z.string().optional() token: z.string({ description: "The bot token to use for login." }).optional()
}, {
description: "Login to Discord using a bot token. If no token is provided, the bot will attempt to use the token from the environment variable DISCORD_TOKEN."
}); });
export const SendMessageSchema = z.object({ export const SendMessageSchema = z.object({
channelId: z.string(), channelId: z.string({ description: "The ID of the channel to send the message to." }),
message: z.string(), message: z.string({ description: "The content of the message to send." }),
replyToMessageId: z.string().optional() replyToMessageId: z.string({ description: "The ID of the message to reply to, if any." }).optional()
}, {
description: "Send a message to a specified channel, optionally as a reply to another message."
}); });
export const GetForumChannelsSchema = z.object({ export const GetForumChannelsSchema = z.object({
guildId: z.string() guildId: z.string({ description: "The ID of the server (guild) to get forum channels from." })
}, {
description: "Get all forum channels in a specified server (guild)."
}); });
export const CreateForumPostSchema = z.object({ export const CreateForumPostSchema = z.object({
forumChannelId: z.string(), forumChannelId: z.string({ description: "The ID of the forum channel where the thread will be created." }),
title: z.string(), title: z.string({ description: "The title of the forum post (thread)." }),
content: z.string(), content: z.string({ description: "The body content of the forum post." }),
tags: z.array(z.string()).optional() tags: z.array(z.string({ description: "A tag to attach to the forum post." })).optional()
}, {
description: "Create a new forum post (thread) in a specified forum channel."
}); });
export const GetForumPostSchema = z.object({ export const GetForumPostSchema = z.object({
threadId: z.string() threadId: z.string({ description: "The ID of the forum thread to retrieve." })
}, {
description: "Get details of a specific forum post (thread) by its ID."
}); });
export const ListForumThreadsSchema = z.object({ export const ListForumThreadsSchema = z.object({
@ -32,128 +42,166 @@ export const ListForumThreadsSchema = z.object({
}); });
export const ReplyToForumSchema = z.object({ export const ReplyToForumSchema = z.object({
threadId: z.string(), threadId: z.string({ description: "The ID of the forum thread to reply to." }),
message: z.string() message: z.string({ description: "The content of the reply message." })
}, {
description: "Reply to a specific forum post (thread) by its ID."
}); });
export const CreateTextChannelSchema = z.object({ export const CreateTextChannelSchema = z.object({
guildId: z.string(), guildId: z.string({ description: "The ID of the server (guild) where the text channel will be created." }),
channelName: z.string(), channelName: z.string({ description: "The name for the new text channel." }),
topic: z.string().optional(), topic: z.string({ description: "The (optional) topic/description for the channel." }).optional(),
reason: z.string().optional() reason: z.string({ description: "Optional reason for audit logs when creating the channel." }).optional()
}, {
description: "Create a new text channel in a specified server (guild)."
}); });
// Category schemas // Category schemas
export const CreateCategorySchema = z.object({ export const CreateCategorySchema = z.object({
guildId: z.string(), guildId: z.string({ description: "The ID of the server (guild) where the category will be created." }),
name: z.string(), name: z.string({ description: "The name of the category to create." }),
position: z.number().optional(), position: z.number({ description: "Optional sorting position index for the category." }).optional(),
reason: z.string().optional() reason: z.string({ description: "Optional reason for audit logs when creating the category." }).optional()
}, {
description: "Create a new category in a specified server (guild)."
}); });
export const EditCategorySchema = z.object({ export const EditCategorySchema = z.object({
categoryId: z.string(), categoryId: z.string({ description: "The ID of the category to edit." }),
name: z.string().optional(), name: z.string({ description: "New name for the category (optional)." }).optional(),
position: z.number().optional(), position: z.number({ description: "New position index for the category (optional)." }).optional(),
reason: z.string().optional() reason: z.string({ description: "Optional reason for audit logs when editing the category." }).optional()
}, {
description: "Edit an existing category's properties."
}); });
export const DeleteCategorySchema = z.object({ export const DeleteCategorySchema = z.object({
categoryId: z.string(), categoryId: z.string({ description: "The ID of the category to delete." }),
reason: z.string().optional() reason: z.string({ description: "Optional reason for audit logs when deleting the category." }).optional()
}, {
description: "Delete a category by its ID."
}); });
export const DeleteChannelSchema = z.object({ export const DeleteChannelSchema = z.object({
channelId: z.string(), channelId: z.string({ description: "The ID of the channel to delete." }),
reason: z.string().optional() reason: z.string({ description: "Optional reason for audit logs when deleting the channel." }).optional()
}, {
description: "Delete a channel by its ID."
}); });
export const ReadMessagesSchema = z.object({ export const ReadMessagesSchema = z.object({
channelId: z.string(), channelId: z.string({ description: "The ID of the channel to read messages from." }),
limit: z.number().min(1).max(100).optional().default(50) limit: z.number({ description: "How many recent messages to fetch (1-100)." }).min(1).max(100).optional().default(50)
}, {
description: "Read recent messages from a specified channel."
}); });
export const GetServerInfoSchema = z.object({ export const GetServerInfoSchema = z.object({
guildId: z.string() guildId: z.string({ description: "The ID of the server (guild) to get information for." })
}, {
description: "Get information about a specific server (guild) by its ID."
}); });
export const AddReactionSchema = z.object({ export const AddReactionSchema = z.object({
channelId: z.string(), channelId: z.string({ description: "The ID of the channel containing the message to react to." }),
messageId: z.string(), messageId: z.string({ description: "The ID of the message to add a reaction to." }),
emoji: z.string() emoji: z.string({ description: "The emoji to use for the reaction (unicode or custom)." })
}, {
description: "Add a reaction to a specific message in a channel."
}); });
export const AddMultipleReactionsSchema = z.object({ export const AddMultipleReactionsSchema = z.object({
channelId: z.string(), channelId: z.string({ description: "The ID of the channel containing the message to react to." }),
messageId: z.string(), messageId: z.string({ description: "The ID of the message to add reactions to." }),
emojis: z.array(z.string()) emojis: z.array(z.string({ description: "An emoji to add (unicode or custom)." }))
}, {
description: "Add multiple reactions to a specific message in a channel."
}); });
export const RemoveReactionSchema = z.object({ export const RemoveReactionSchema = z.object({
channelId: z.string(), channelId: z.string({ description: "The ID of the channel containing the message to modify reactions on." }),
messageId: z.string(), messageId: z.string({ description: "The ID of the message to remove the reaction from." }),
emoji: z.string(), emoji: z.string({ description: "The emoji reaction to remove." }),
userId: z.string().optional() userId: z.string({ description: "Optional ID of the user whose reaction should be removed; if omitted, removes the current bot's reaction." }).optional()
}, {
description: "Remove a reaction from a specific message in a channel."
}); });
export const DeleteForumPostSchema = z.object({ export const DeleteForumPostSchema = z.object({
threadId: z.string(), threadId: z.string({ description: "The ID of the forum thread to delete." }),
reason: z.string().optional() reason: z.string({ description: "Optional reason for audit logs when deleting the forum post." }).optional()
}, {
description: "Delete a forum post (thread) by its ID."
}); });
export const DeleteMessageSchema = z.object({ export const DeleteMessageSchema = z.object({
channelId: z.string(), channelId: z.string({ description: "The ID of the channel containing the message to delete." }),
messageId: z.string(), messageId: z.string({ description: "The ID of the message to delete." }),
reason: z.string().optional() reason: z.string({ description: "Optional reason for audit logs when deleting the message." }).optional()
}, {
description: "Delete a message by its ID in a specified channel."
}); });
export const CreateWebhookSchema = z.object({ export const CreateWebhookSchema = z.object({
channelId: z.string(), channelId: z.string({ description: "The ID of the channel to create the webhook in." }),
name: z.string(), name: z.string({ description: "The name to assign to the webhook." }),
avatar: z.string().optional(), avatar: z.string({ description: "Optional avatar URL or data for the webhook." }).optional(),
reason: z.string().optional() reason: z.string({ description: "Optional reason for audit logs when creating the webhook." }).optional()
}, {
description: "Create a webhook in a specified channel."
}); });
export const SendWebhookMessageSchema = z.object({ export const SendWebhookMessageSchema = z.object({
webhookId: z.string(), webhookId: z.string({ description: "The ID of the webhook to send the message with." }),
webhookToken: z.string(), webhookToken: z.string({ description: "The token for the webhook (used for authentication)." }),
content: z.string(), content: z.string({ description: "The message content to send via the webhook." }),
username: z.string().optional(), username: z.string({ description: "Optional username to display for the webhook message." }).optional(),
avatarURL: z.string().optional(), avatarURL: z.string({ description: "Optional avatar URL to display for the webhook message." }).optional(),
threadId: z.string().optional() threadId: z.string({ description: "Optional ID of the thread to post the webhook message into." }).optional()
}, {
description: "Send a message using a webhook."
}); });
export const EditWebhookSchema = z.object({ export const EditWebhookSchema = z.object({
webhookId: z.string(), webhookId: z.string({ description: "The ID of the webhook to edit." }),
webhookToken: z.string().optional(), webhookToken: z.string({ description: "Optional token for the webhook if required to authorize edits." }).optional(),
name: z.string().optional(), name: z.string({ description: "Optional new name for the webhook." }).optional(),
avatar: z.string().optional(), avatar: z.string({ description: "Optional new avatar URL or data for the webhook." }).optional(),
channelId: z.string().optional(), channelId: z.string({ description: "Optional channel ID to move the webhook to." }).optional(),
reason: z.string().optional() reason: z.string({ description: "Optional reason for audit logs when editing the webhook." }).optional()
}, {
description: "Edit a webhook's properties."
}); });
export const DeleteWebhookSchema = z.object({ export const DeleteWebhookSchema = z.object({
webhookId: z.string(), webhookId: z.string({ description: "The ID of the webhook to delete." }),
webhookToken: z.string().optional(), webhookToken: z.string({ description: "Optional token for the webhook if required for deletion." }).optional(),
reason: z.string().optional() reason: z.string({ description: "Optional reason for audit logs when deleting the webhook." }).optional()
}, {
description: "Delete a webhook by its ID and token."
}); });
export const ListServersSchema = z.object({}); export const ListServersSchema = z.object({}, {
description: "List all servers (guilds) the bot is a member of."
});
export const SearchMessagesSchema = z.object({ export const SearchMessagesSchema = z.object({
guildId: z.string().min(1, "guildId is required"), guildId: z.string({ description: "The ID of the server (guild) to search in." }).min(1, "guildId is required"),
// Optional filters // Optional filters
content: z.string().optional(), content: z.string({ description: "Search for messages that contain this text." }).optional(),
authorId: z.string().optional(), authorId: z.string({ description: "Filter messages to those authored by this user ID." }).optional(),
mentions: z.string().optional(), mentions: z.string({ description: "Filter messages that mention a specific user or role ID." }).optional(),
has: z.enum(['link','embed','file','poll','image','video','sound','sticker','snapshot']).optional(), has: z.enum(['link', 'embed', 'file', 'poll', 'image', 'video', 'sound', 'sticker', 'snapshot'], { description: "Filter messages that contain a specific type of content." }).optional(),
maxId: z.string().optional(), maxId: z.string({ description: "Only include messages with IDs less than or equal to this (pagination)." }).optional(),
minId: z.string().optional(), minId: z.string({ description: "Only include messages with IDs greater than or equal to this (pagination)." }).optional(),
channelId: z.string().optional(), channelId: z.string({ description: "If provided, restrict search to a specific channel ID." }).optional(),
pinned: z.boolean().optional(), pinned: z.boolean({ description: "If true, only include pinned messages; if false, only include unpinned; if omitted, include both." }).optional(),
authorType: z.enum(['user','bot','webhook']).optional(), authorType: z.enum(['user', 'bot', 'webhook'], { description: "Filter by the type of author (user, bot, or webhook)." }).optional(),
sortBy: z.enum(['timestamp','relevance']).optional(), sortBy: z.enum(['timestamp', 'relevance'], { description: "Field to sort search results by." }).optional(),
sortOrder: z.enum(['desc','asc']).optional(), sortOrder: z.enum(['desc', 'asc'], { description: "Sort direction for results." }).optional(),
limit: z.number().min(1).max(100).default(25).optional(), limit: z.number({ description: "Number of results to return (1-100)." }).min(1).max(100).default(25).optional(),
offset: z.number().min(0).default(0).optional() offset: z.number({ description: "Number of results to skip (for pagination)." }).min(0).default(0).optional()
}, {
description: "Search messages in a server with various filters."
}); });