Skip to content

Security audit — 2026-05-01

  • Reviewer: discord-mcp implementer subagent (Plan 12 Phase E)
  • Scope: codebase as of commit 438b679 (branch feature/plan12-test-infra-polish)
  • Status: PASS with 1 P1 fixed in audit + 2 P2 deferred

The audit was performed before tagging v0.12.0 and pre-1.0 release. Findings are categorized:

  • P0 — must fix before release. Blocks Phase F.
  • P1 — should fix in Plan 12 Phase E.
  • P2 — defer to Plan 13+ with documented rationale.

grep -rn "DISCORD_TOKEN" packages/ returns 12 source files (excluding tests and config.ts). Each call site was reviewed:

  • packages/mcp-core/src/errors/client.ts:68 — references the env var name in an error message (“Set DISCORD_TOKEN env to a fresh token”). No value echo.
  • packages/mcp-server/src/cli.ts — references the env var name in --help.
  • packages/mcp-server/src/transports/stdio.ts:31,52-54 — passes the token value to discord.js Client / REST, never logs it.
  • packages/mcp-server/src/lib/checks/token-online.ts:65 — constructs Authorization: Bot <token> for the live /users/@me probe; no log.
  • packages/mcp-server/src/lib/checks/token-format.ts — reads process.env.DISCORD_TOKEN and asserts shape; never logs the value.
  • packages/mcp-server/src/commands/init.ts — emits the literal placeholder ${env:DISCORD_TOKEN} into generated client configs (intentional UX, no value).

grep for console.log|logger.*DISCORD_TOKEN and ${config.DISCORD_TOKEN} returns zero matches. No accidental token echo path identified.

Result: PASS. No findings.

The wrapMessages / wrapUntrusted helpers (packages/mcp-core/src/tools/_lib/untrusted.ts) fence Discord-authored content inside <untrusted_*> markers so agents treat it as data. Plan 7 §3.10 enumerated 16 read-side tools that must wrap their text output.

Verified each tool calls wrapMessages or wrapUntrusted at least once:

ToolWrap callsStatus
messages_get2OK
messages_list_pins2OK
messages_search_recent2OK
members_list2OK
members_get2OK
members_get_current_user2OK
automod_get_rule2OK
automod_list_rules2OK
events_get2OK
events_list2OK
stickers_get_guild_sticker2OK
webhooks_get_message2OK
interactions_get_followup2OK
interactions_get_original_response2OK
application_get_current2OK
application_modify_current2OK
onboarding_get2OK
users_get2OK

Three additional tools surfaced by the broader grep were inspected:

  • users_get_current — returns the bot’s own /users/@me profile. The username field is operator-controlled, not attacker-influenced. Not in scope for the untrusted-wrap requirement.
  • members_search — renders user-authored username into the text field via a list (- $\{username\} (\user:${user_id}`)`). The structured payload is fine but the rendered Markdown is agent-visible. P2: consider wrapping the rendered list in Plan 13.
  • stickers_list_guildtext only contains a count and the guild ID; user-authored sticker names go to data only. Not in scope for the text-wrapping rule.

Result: PASS. P2 noted for members_search (Plan 13).

grep -rln "destructiveHint: true" packages/mcp-core/src/tools/ returned 27 tools. Each was checked for preconditions: ['confirm_required']:

Initial pass found 1 P1 finding:

  • reactions_delete_all — destructive (clears reactions, irreversible) but missing preconditions: ['confirm_required'].

Fixed in this audit commit (packages/mcp-core/src/tools/reactions/delete_all.ts): added the precondition + **DESTRUCTIVE — IRREVERSIBLE.** description prefix. Existing unit tests bypass the precondition by calling t.run() directly, so no test changes were needed.

The remaining 26 destructive tools all gate behind confirm_required:

  • webhooks/{delete,delete_message,delete_with_token}
  • users/leave_guild
  • stickers/delete_guild_sticker
  • stage_instances/delete
  • soundboard/delete_guild_sound
  • roles/delete
  • monetization/entitlements_delete_test
  • messages/{delete,bulk_delete}
  • members/{kick,bulk_ban,ban}
  • invites/delete
  • interactions/{delete_original_response,delete_followup}
  • guild/{delete_integration,begin_prune}
  • events/delete
  • emojis/delete
  • commands/{delete_guild,delete_global}
  • channels/delete
  • automod/delete_rule
  • app_emojis/delete

Result: PASS after fix. 1 P1 closed, 0 outstanding.

audit/redact.ts enumerates exactly the 15 tools listed in Plan 8 Phase F:

  • messages_send, messages_edit, messages_bulk_delete
  • webhooks_execute, webhooks_edit_message
  • components_v2_send, components_v2_edit, components_v2_send_from_template
  • intelligence_summarize_channel, intelligence_classify_messages, intelligence_draft_response, intelligence_moderate_content, intelligence_extract_entities
  • interactions_create_response, interactions_edit_original_response

Both consumers of redactArgs(args, toolName) are wired:

  • middleware/audit.ts:56 — used for the JSONL args_redacted field.
  • middleware/telemetry.ts:110 — used as the mcp.args.redacted event attribute on every mcp.tool.* span.

The global sensitive-key set (token, bearer_token, auth, password, secret) is matched at any nesting depth, with case-insensitive key comparison. String values longer than 200 chars are truncated to 100 + ...[TRUNCATED].

Result: PASS. No findings.

CategoryStatusNotes
InjectionPASSzod safeParse at every tool boundary (validateMiddleware).
Broken authPASSToken never logged; never appears in span attrs / metric labels.
Sensitive data exposurePASSredactArgs covers per-tool + global sensitive keys.
XML external entitiesN/ANo XML processing in the codebase.
Broken access controlDOCUMENTEDAgents inherit full bot permissions. Scope-based limiting deferred to Plan 13+.
Security misconfigurationPASSDefaults: MCP_DRY_RUN=true, OTEL_ENABLED=false.
XSSN/Astdio transport renders no HTML.
DeserializationPASSAll inputs go through zod safeParse before handler runs.
Vulnerable depsP2pnpm audit reports 1 moderate: Astro <6.1.6 XSS in define:vars. Site-only, doesn’t affect runtime mcp-core / mcp-server. Defer to Plan 13.
Logging gapsPASSEvery mutating call goes through audit.ts middleware (Plan 8 Phase E).
PriorityFindingStatus
P0None
P1reactions_delete_all missing confirm_requiredFIXED in this audit commit
P2members_search text rendering of usernamesDeferred to Plan 13
P2Astro 5.x define:vars XSS — site-onlyDeferred to Plan 13 (bump to ≥6.1.6)

No P0 findings. Phase F (tag v0.12.0) may proceed.