The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Discourse MCP listing page.
A Model Context Protocol (MCP) stdio server that exposes Discourse forum capabilities as tools and resources for AI agents.
src/index.ts → compiled to dist/index.js (binary name: discourse-mcp)@modelcontextprotocol/sdk--allow_writes is sufficient and deprecates read_only=false; 0.3.0 added operator-selectable toolsets, structured directory output, and expanded opt-in administration capabilities; 0.2.x introduced breaking changes from 0.1.x, including JSON-only tool output; category/group resources remain deprecated compatibility surfaces alongside canonical list tools)Then, in your MCP client, either:
Call the discourse_select_site tool with { "site": "https://try.discourse.org" } to choose a site, or
Start the server tethered to a site using --site https://try.discourse.org (in which case discourse_select_site is hidden).
Enable writes (opt‑in, safe‑guarded)
This exposes discourse_select_site plus the read-only Data Explorer tools. Add --site, authentication, and the write flags as needed; see Built-in toolsets.
Alternative: if you prefer a global binary after install, the package exposes
discourse-mcp.
The server registers tools under the MCP server name @discourse/mcp. Choose a target Discourse site either by:
Using the discourse_select_site tool at runtime (validates via /about.json), or
Supplying --site <url> to tether the server to a single site at startup (validates via /about.json and hides discourse_select_site).
Auth
--auth_pairs '[{"site":"https://example.com","api_key":"...","api_username":"system"}]'--auth_pairs '[{"site":"https://example.com","user_api_key":"...","user_api_client_id":"..."}]'http_basic_user and http_basic_pass to any auth_pairs entry. This is useful for Discourse sites protected by HTTP Basic Authentication at the reverse proxy level.auth_pairs; the matching entry is used for the selected site. If both user_api_key and api_key are provided for the same site, user_api_key takes precedence.Write safety
--allow_writes is enabled. This includes post, topic, private-message, category, user, upload, draft, and saved Data Explorer query mutations.auth_pairs entry for the selected site; otherwise they return an error.Flags & defaults
--help, -h, or positional help: print current CLI help and exit successfully before loading profiles or starting a transport.
--version, -v, or positional version: print one package-version line and exit successfully. -v means version; logging verbosity uses --log_level.
--allow_writes (default: false): enable mutation tools. This single explicit opt-in is sufficient.
--read_only <boolean>: deprecated compatibility setting. true is an explicit read-only override and conflicts with --allow_writes; false has no effect and should be removed from commands and profiles.
--timeout_ms <number> (default: 15000)
--concurrency <number> (default: 4)
--log_level <silent|error|info|debug> (default: info)
debug: Shows HTTP request URLs, statuses, and detailed network/retry information (response bodies are never logged because admin APIs may echo sensitive content)info: Shows retry attempts and general operational messageserror: Shows only errorssilent: No logging output--show_emails (default: false). includes emails in user tools. Requires admin access
--tools_mode <auto|discourse_api_only|tool_exec_api> (default: auto)
--toolsets <name[,name...]>: Expose selected built-in domains. Omit for the compact default catalog (all non-opt-in domains); use --toolsets all to include opt-in category/group/tag-group, moderation, workflow, and AI administration domains. See Built-in toolsets.
--site <url>: Tether MCP to a single site and hide discourse_select_site.
--default-search <prefix>: Unconditionally prefix every search query (e.g., tag:ai order:latest).
--max-read-length <number>: Maximum characters returned for post content (default 50000). Applies to discourse_read_post and per-post content in discourse_read_topic and discourse_read_private_message. The tools prefer raw content by requesting include_raw=true.
--allowed_upload_paths <paths>: Comma-separated list or JSON array of directories allowed for local file uploads. Required to enable local file uploads in discourse_upload_file. Example: --allowed_upload_paths "/home/user/images,/tmp/uploads" or --allowed_upload_paths '["/home/user/images"]'. These security-sensitive paths do not receive ~ expansion.
--transport <stdio|http> (default: stdio): Use standard input/output by default, or loopback-only Streamable HTTP with JSON responses. HTTP explicitly supports one stateful MCP client/session per process. Every post-initialize request must carry the returned Mcp-Session-Id; a second initialize is rejected. After session DELETE/close, restart the process before connecting another client. /health returns 503 restart_required in that closed state. Request bodies are bounded to 4 MiB.
--port <number> (default: 3000): Port to listen on when using HTTP transport.
--cache_dir <path> (reserved)
--profile <path.json> (see below)
Profile file (keep secrets off the command line)
Run with:
Flags still override values from the profile. A leading current-user ~, ~/, or ~\ in the profile path expands to the current home directory; ~otheruser, shell-style expansion elsewhere, and upload-allowlist expansion are intentionally unsupported.
Toolsets let an operator expose only the built-in domains needed by an MCP client. They are optional: when --toolsets and the profile field are both omitted, the server registers the default catalog (including search, discourse_search, and discourse_filter_topics). Administrative and specialized domains marked (opt-in) below—including themes—must be selected explicitly. Use --toolsets all only when every built-in domain is deliberately required.
Pass one name or a comma-separated union:
Profiles use an array (a comma-separated string is also accepted):
Available toolsets are:
| Toolset | Built-in tools |
|---|---|
site | discourse_select_site (also retained implicitly as bootstrap for any untethered subset) |
search | Topic-level search/filtering plus post-level keyword evidence |
topics | Core topic/post reads, exact stream selection, post search, user-post activity, and mutations |
users | User lookup/listing, user-post activity, and user mutations |
chat | Chat message retrieval |
drafts | Draft retrieval, save, and deletion |
uploads | File upload |
data_explorer | Query retrieval, execution, creation, update, and deletion |
private_messages | Authenticated personal/group PM listing and reading, plus write-gated creation, replies, and participant invitations |
activity (opt-in) | Reply relationships, site-wide post activity, topic view history, user activity summaries and timelines, and directory/cohort metrics |
administration (opt-in) | Category discovery, admin-visible site settings, and explicitly confirmed user activation/approval state changes |
site_settings (opt-in, admin-sensitive) | Masked site-setting inspection plus write-gated, preflighted updates of ordinary non-secret settings |
webhooks (opt-in, admin-sensitive) | Safe webhook and delivery-history inspection plus write-gated lifecycle, ping, and single-event redelivery operations |
themes (opt-in, admin-sensitive) | Theme/component inspection plus write-gated local creation, editing, installation, remote synchronization, asset upload, and guarded deletion |
groups (opt-in) | Exhaustive empty-input group directory listing, explicit page/filter compatibility mode, complete group CRUD and membership operations, and fixed-page group-authored post evidence |
tag_groups (opt-in, staff-sensitive) | Public visibility-filtered search plus staff inventory/detail and write-gated, preflighted create/update/delete lifecycle |
moderation (opt-in) | Authenticated review queue triage, user behavioral counters, bounded post revisions, and one freshly preflighted reviewable action |
workflows (opt-in) | Admin-only workflow discovery, graph authoring, expression evaluation, pin-data, draft runs, step runs, executions, and version management |
ai_agents (opt-in) | Admin-only AI agent discovery, typed lifecycle, bot-user creation, and portable import/export |
ai_custom_tools (opt-in) | Admin-only database-backed scripted custom-tool guide, lifecycle, actual execution testing, and import/export |
ai_features (opt-in) | Admin-only AI feature discovery and exact-area, non-secret feature-setting updates; also includes agent discovery |
analytics (opt-in) | Staff-visible Discourse report discovery/execution and the Discourse Solved support dashboard |
ai_insights (opt-in) | Read-oriented Discourse AI cached summaries, semantic search, and staff sentiment classifications |
all (sentinel) | Expands to every built-in toolset, including opt-in domains; absorbs other selections |
Toolset membership is intentionally separate from safety and authorization:
all expands to every real domain and is never tool metadata.discourse_select_site is automatically retained as a bootstrap capability for every untethered subset. With --site, it remains hidden as usual.--toolsets data_explorer exposes query retrieval and execution by default; add --allow_writes to expose saved-query mutations.--tools_mode; use --tools_mode discourse_api_only when the MCP tool list must contain only the selected built-in domains. The server logs an informational notice when selected toolsets are combined with remote discovery.uploads in read-only mode. The server logs an informational notice when this occurs.Directory capabilities are deliberately opt-in, so omitting --toolsets adds zero category/group/tag-group tools:
administration for discourse_list_categories. Empty input performs bounded exhaustive traversal through the 1-based category-search endpoint when the deployment permits it. If anonymous POST is rejected, bounded paginated nested category-index GETs (and only on their rejection, legacy /site.json) are returned with explicit anonymous_fallback/legacy_site_json incomplete metadata—never as exhaustive. Optional term, max_pages, max_requests, max_results, and deadline_ms bound focused discovery; fallback term matching is applied locally because category index does not implement search terms. Category records retain URL/hierarchy fields; parent_category_id is canonical and nullable, while pid is a legacy alias retained for compatibility. The existing rich no-input projection is intentional: the reproducible 300-record fixture in src/test/directory_tools.test.ts measures about 45 KB, so this release preserves its useful counts/access fields rather than adding a second fields contract.groups for discourse_list_groups. {} is the canonical exhaustive, deduplicating operation. Supplying any explicit existing key—including { "page": 0 } or { "asc": false }—preserves the historical one-page/filter query behavior. Both modes return { groups, meta, extras?, total_rows_groups?, load_more_groups? }; filtered mode is intentionally complete: false.outputSchema and return structuredContent. The JSON text content is the same normalized value for clients that do not consume structured output. Malformed upstream records return ordinary isError: true tool results rather than protocol output-validation failures.Select the dedicated tag_groups domain for six tools:
discourse_search_tag_groups is public, Guardian-filtered discovery. It always sends an explicit limit and reports possible truncation. Search omits tag-group IDs, parents, and permissions, so it is not authoritative inventory; case-insensitive exact group names are the correlation key. q and names combine with AND semantics, and upstream treats %/_ as SQL LIKE wildcards.discourse_list_tag_groups and discourse_get_tag_group require configured API credential shape plus upstream staff authority. The local helper cannot prove a staff role; Discourse is authoritative and privacy-preservingly returns 404 to non-staff. Reads can work when tagging is disabled.discourse_create_tag_group, discourse_update_tag_group, and discourse_delete_tag_group additionally require effective write mode and upstream tagging_enabled. MCP inputs and normalized outputs represent permissions as explicit entries, for example [{"group_id":0,"access":"full"},{"group_id":9,"access":"readonly"}]; group ID 0 is Discourse's built-in everyone group. The server converts these entries to Discourse's numeric permission map (1 = full, 3 = readonly) only at the HTTP boundary. parent_tag is an optional {id} or {name} selector: omit it or use null when creating without a parent; blank client placeholders are treated as omitted. New selector names require allow_tag_creation=true because persistent tags are created and normal indexing/plugin hooks run.expected_state_hash, merge omitted fields locally, and send complete tags/parent/one-per-topic/permissions because partial upstream bodies clear state. Tag/parent removals, permission replacement, and possible materialization of serializer-synthesized everyone/full legacy permissions require explicit confirmations (including acknowledge_possible_synthetic_permission_materialization). The hash is an MCP optimistic precondition, not an upstream atomic lock; races can still occur after preflight.outcome_unknown errors.These toolsets control discovery only. Staff role, Guardian visibility, scoped-key authority, write mode, and site settings remain call-time/upstream decisions.
The webhooks and site_settings toolsets are opt-in and admin-sensitive. Selecting either toolset controls discovery only—it is not authorization. Every call requires matching selected-site admin-style authentication, Discourse remains the final authorization and validation authority, and mutations additionally require --allow_writes. The existing site-setting read remains available through administration, but site-setting mutation is discoverable only through an explicit site_settings selection.
Webhook delivery, ping, and redelivery make requests to external systems; enqueue or HTTP success does not prove that the destination processed an event correctly. Webhook secrets are never returned, URL userinfo is removed, query values are masked, and raw event headers are never passed through. Event payload/body previews require explicit sensitive-content confirmation and are bounded and credential-redacted. Bulk redelivery is intentionally unsupported.
Site settings affect the entire forum. Reads mask both upstream-secret and credential-like names; pass overridden_only: true to list only settings whose current value differs from the default. Updates support only one freshly visible ordinary setting at a time, require an expected current value and confirmation, and verify the result with an exact re-read. Secret/credential, upload, uploaded-image-list, and structured object settings, bulk updates, and existing-user backfills are intentionally unsupported.
The opt-in themes toolset is admin-sensitive and is never included in the default catalog. Read-only selection registers only discourse_list_themes and discourse_get_theme; every mutation additionally requires --allow_writes. Toolset selection does not grant admin access: configure matching site authentication and Discourse remains authoritative for admin, repository-allowlist, dependency, compiler, import, and migration checks.
Theme HTML, JavaScript, SCSS, settings migrations, assets, and third-party repositories can execute or deploy code for every visitor. The tools require operation-specific confirmations, but they do not sandbox, validate, or declare third-party code safe. Local archives and assets are accepted only from bounded base64 input or regular files beneath symlink-resolved --allowed_upload_paths roots.
Use deliberate operator configuration rather than enabling every toolset:
Local themes and ZIP-imported themes can be edited directly (although ZIP source values are omitted by Discourse's detail serializer); Git-backed themes must be changed in their repository and synchronized. Components cannot be default/user-selectable or own color schemes. Text fields and upload fields are separate schema variants—never send placeholder upload IDs with SCSS/HTML/JavaScript:
Installation likewise uses one nested source variant. A repository install needs no archive placeholders:
This release intentionally excludes private-repository key management, repository repointing, export, bulk deletion, arbitrary themeable site-setting mutation, and generic controller parameter pass-through.
The opt-in groups toolset covers the complete custom-group lifecycle: directory listing and full detail reads; create, update, and permanent delete; paginated member and owner reads; explicit selector-specific tools for adding/removing members and promoting/demoting owners by username, numeric user ID, or existing-account email; pending-request listing and approve/deny decisions; and authenticated request, public-join, and public-leave flows. A separate invitation tool handles addresses that may not have accounts yet, avoiding confusion between account lookup and forum invitations.
All mutations require --allow_writes. Creation and deletion additionally require staff/admin-style API credentials at the MCP access gate. Discourse remains authoritative for Guardian checks, group visibility, staff versus owner capabilities, automatic-group restrictions, membership settings, invitation limits, and the fields a caller may update. Core automatic groups cannot be created, deleted, or have membership/ownership changed; their permitted presentation and interaction settings can still be updated by authorized staff. Selecting the toolset does not grant any of these permissions.
The opt-in moderation toolset exposes discourse_get_review_queue_count, discourse_list_reviewables, discourse_list_reviewable_topics, and discourse_get_reviewable in read-only mode. These tools require configured authentication, but intentionally do not impose an MCP admin-only gate: Discourse Guardian remains authoritative for staff and category-moderator visibility. Selecting the toolset grants no moderation permission.
For queue totals, use discourse_get_review_queue_count; its count is the number of pending reviewable records visible to the caller, not the number of individual flags. Use discourse_list_reviewables with only status: "pending" and offset: 0 for ordinary triage—do not invent topic, category, type, or user filters—and follow next_offset until has_more is false. Numeric topic/category placeholders of 0 and optional text placeholders of blank/all/any are treated as omitted, so strict-schema clients cannot accidentally filter to ID 1 or send invalid universal sentinels. List results already contain bounded evidence and dynamic actions; avoid fanning out discourse_read_topic or detail calls across the queue. discourse_list_reviewable_topics is only a convenience aggregation: upstream includes pending topics at or above its minimum review-priority threshold, omits queue items without topics, and reports score_count as the number of review score/flag records—not reviewable items. It must not be used to infer the complete queue size.
When --allow_writes is set, discourse_perform_reviewable_action is also registered. Call list/detail first and submit one exact available_actions[].id with confirm: true; choose from the full action description, not a repeated label such as “Delete post.” Discourse UI action IDs can be prefixed (post-… or user-…), while the route requires the associated server_action; the MCP validates and maps this automatically. Moderation mutations are serialized and paced across the complete fresh-GET/PUT operation, so a concurrent model batch cannot bypass the write throttle. The tool checks an optional expected version, rejects unadvertised fields, and returns normalized success/count fields. A failure after the PUT is marked as an unknown outcome with identifiers and must be verified rather than blindly retried. Discourse still enforces claims, optimistic conflicts, action validity, and Guardian permissions. The tools expose evidence and explicit operations; they do not recommend moderation decisions.
The default private_messages toolset provides a PM-aware interface rather than reusing generic public-topic mutations. Listing and reading require configured authentication. Creation, replies, and invitations additionally require --allow_writes. Discourse remains authoritative for mailbox visibility, PM membership, recipient limits, group messageability, and all Guardian/API-key checks.
Personal mailboxes support inbox, sent, archive, unread, and new. Group mailboxes support all except sent; a personal inbox does not include every group inbox. If discourse_list_private_messages omits username, it resolves the authenticated user through /session/current.json. A supplied username selects a mailbox path—it does not impersonate that user. Discourse permits another user's inbox/sent/archive only where its authorization rules allow it, while unread and new remain owner-only even for admins.
PM recipients are typed as usernames, group names, or email addresses. During creation, a messageable group wins over a same-named user in Discourse's upstream classification. A nonexistent or non-messageable group_names value can therefore surface as a user-not-found-style upstream error. Unknown email recipients may immediately create staged users when site settings and sender permissions allow it; use a canonical username when staged-user creation is not intended.
Email invitations are intentionally opaque. A successful response does not confirm delivery or immediate participant access: an address belonging to an existing account may produce a successful no-op, while a new address receives access only after invitation redemption. Use username to add a known account immediately. Group invitation lookup is exact-case, so use the canonical group name. Optional author_username sends Api-Username; switching identities is supported only by an appropriate global API key, while User API Keys remain bound to their owner.
The workflows toolset targets the experimental discourse-workflows plugin (enable_discourse_workflows) and requires an admin API key. A typical loop is:
identifier for its parameter schema, output ports, and $json contracts.nodes and connections, or use MCP-side operations[] for mechanical edits. Omitting a node from a whole-graph update deletes it.discourse_get_workflow_execution.published: true after testing.Flat connections such as [{"from":"Start","to":"Check","type":"main"}] are accepted and converted to Discourse's nested wire format. Use the source node's catalog output key: condition/filter ports are true and false, not always main. MCP rejects one-sided graph updates before HTTP. Runs are not dry-runs and can create posts, send chat messages, or call external HTTP.
The three AI administration domains require a Discourse admin API key (or an admin user API key accepted by the selected endpoint). They are independently opt-in and default-off. Mutations—and custom-tool test execution—also require --allow_writes.
The agent index is intentionally concise by default: discourse_ai_list_agents omits system prompts and per-agent configuration, returning summary counts—including subagent_count—plus slim tool/model catalogs. Use discourse_ai_get_agent with an ID to inspect one full configuration. view: "full" is available only for clients that explicitly need the complete upstream index.
Agent create/update schemas accept subagent_ids, an ordered allowlist of up to 20 unique existing agent IDs that the parent may delegate to. Negative IDs are valid for system agents. Discourse validates that every ID exists, rejects self-delegation, and prevents a configured tool from colliding with the generated spawn_agent tool; use the full agent list or detail response to resolve IDs before writing.
discourse_ai_list_custom_tools follows the same pattern: it returns compact records and preset signatures without scripts, bindings, or verbose parameter documentation. Use discourse_ai_get_custom_tool for one stored tool, or call the guide with topic: "presets" and a preset_id for one complete preset example.
ai_custom_tools manages Discourse's database-backed AiTool records. It is separate from remote tools dynamically discovered at /ai/tools, which remain controlled by --tools_mode. Script authoring is synchronous MiniRacer JavaScript: define invoke(parameters); do not use async, browser APIs, or Node modules. Call discourse_ai_get_custom_tool_guide with only the focused topic you need. preset_id is optional and meaningful only for topic: "presets"; it is ignored for other topics. Use topic: "preamble" for the exact selected-server contract before creating or substantially changing a script. The same exact live preamble and minimal template is exposed as the conditional discourse://ai/custom-tools/authoring-guide resource when this toolset is selected. Resources are application-driven; the guide tool is model-controlled, so autonomous clients should use the tool rather than assume a host attached the resource. A future optional authoring prompt would be user-controlled and would guide an explicitly initiated workflow rather than replace model-callable discovery.
Safety: discourse_ai_test_custom_tool actually executes code and can issue external requests or cause site side effects. Feature updates alter production behavior immediately and are limited to non-secret settings returned from one exact ai-features/<module> area. Custom-tool source, prompts, bindings, exports, and test parameters should be treated as sensitive. Use the narrowest toolset combination and test on a non-production site first.
Remote Tool Execution API (optional)
tools_mode=auto (default) or tool_exec_api, the server discovers remote tools via GET /ai/tools after you select a site (or immediately at startup if --site is provided) and registers them dynamically. Set --tools_mode=discourse_api_only to disable remote tool discovery.Networking & resilience
Privacy
Resources provide application-addressable static/semi-static read-only data. Category and group resources are retained as deprecated compatibility surfaces; their canonical model-callable replacements are the opt-in directory tools above. Other resources remain appropriate when an MCP host attaches them.
discourse://site/categories (deprecated; use discourse_list_categories with --toolsets administration)
{ categories: [{id, name, slug, parent_category_id, pid, read_restricted, topic_count, post_count, perms?}], meta: {total, reported_total, pages_fetched, complete, has_more, truncated_reason?} }parent_category_id is canonical; pid is a legacy compatibility alias. perms is an array of {gid, perm} where perm: 1=full, 2=create_post, 3=readonly.perms is populated only when the selected identity can retrieve permission enrichment; otherwise it is omitted rather than fabricated.discourse://site/tags
{ tags: [{id, name, count}], meta: {total} }discourse://site/groups (deprecated; use discourse_list_groups with --toolsets groups)
{ groups: [{id, name, automatic, user_count, vis, members_vis, mention, msg, public_admission, public_exit, allow_membership_requests}], meta: {total, reported_total, pages_fetched, complete, has_more, truncated_reason?} }gid values from category permissions to group names, replicate group settings during migrationsdiscourse://chat/channels
{ channels: [{id, title, slug, status, members_count, description}], meta: {total} }discourse://user/chat-channels
{ public_channels: [...], dm_channels: [...], meta: {total} }discourse://user/drafts
{ drafts: [{draft_key, sequence, title, category_id, created_at, reply_preview}], meta: {total} }discourse://ai/custom-tools/authoring-guide (conditional)
ai_custom_tools is selected (including through all)empty_tool JavaScript preset: Discourse's current preamble plus minimal invoke/details templatetext/javascript; requires selected-site admin credentialsdiscourse_ai_get_custom_tool_guide and topic: "preamble"The expanded read catalog exposes upstream evidence rather than MCP-authored judgments:
discourse_search remains topic-focused. Use discourse_search_posts when matched posts are required: it preserves bounded post IDs with highlighted blurbs, authors, topics/categories, and truthful continuation. This supersedes the older proposal to add an unbounded list of bare post IDs to topic-search results. discourse_ai_semantic_search is a separate opt-in Discourse AI embedding search. The opt-in activity tool discourse_list_latest_posts is a fixed 50-row chronological feed with a post-ID cursor, not search.discourse_read_topic_posts selects exact IDs, earliest/latest posts, an around-post window, or username-filtered posts. Latest/earliest selections use at most two upstream requests and cap the selected set at 50. The opt-in activity tool discourse_get_post_replies distinguishes recursive descendant IDs, 20-row direct replies, and the site-bounded ancestor history.activity domain contains discourse_get_user_summary for profile-visible aggregates, discourse_list_user_actions for a paginated named event timeline, and discourse_list_directory_items for visible directory/cohort metrics. The existing default discourse_list_user_posts remains the compatible post/reply view.administration domain makes categories and admin-visible site settings model-callable and provides confirmed activation/approval state changes. User creation requires a global admin API key; responses without an upstream user_id remain explicitly unconfirmed.author_username requires a global API key, and creation responses report both requested and actual attribution so ignored impersonation cannot be mistaken for success.analytics domain discovers the staff-visible report catalog before executing a report and exposes the Solved support dashboard. Dashboard “unanswered” means unsolved with no qualifying regular reply—not no response from a designated team.ai_insights domain requires Discourse AI. Cached summaries report staleness, semantic search remains Guardian-filtered, and sentiment values are upstream model classifications rather than objective argument, satisfaction, or risk labels.Plugin-specific 404 responses are intentionally reported as capability_or_resource_unavailable: the same upstream response can mean a disabled plugin/setting, a hidden resource, or a nonexistent resource. Toolset selection does not grant visibility or staff access.
Example compositions:
discourse_read_topic_posts in latest/replies_only mode, then let the calling model summarize the ordered evidence.discourse_list_group_posts and topic IDs; group authorship is evidence, not proof of organizational responsibility.Built‑in tools (always present unless noted). All tools return strict JSON (no Markdown).
discourse_search
{ query: string; max_results?: number (1–50, default 10) }{ results: [{id, slug, title}], meta: {total, has_more} }discourse_read_topic
{ topic_id: number; post_limit?: number (1–50, default 5); start_post_number?: number }{ id, title, slug, category_id, tags, posts_count, posts: [{id, post_number, username, created_at, raw}], meta }discourse_read_post
{ post_id: number }{ id, topic_id, topic_slug, post_number, username, created_at, raw, truncated }discourse_get_user
{ username: string }{ id, username, name, trust_level, created_at, bio, admin, moderator }discourse_list_user_posts
{ username: string; page?: number (0-based); limit?: number (1–50, default 30) }{ posts: [{id, topic_id, post_number, slug, title, created_at, excerpt, category_id}], meta: {page, limit, has_more} }discourse_filter_topics
{ filter?: string; view?: "filtered"|"top"|"hot" (default "filtered"); top_period?: "daily"|"weekly"|"monthly"|"quarterly"|"yearly"|"all"; page?: number (0-based); per_page?: number (1–50) }filter and uses /filter.json. Top rejects filter, uses /top.json, and defaults to weekly. Hot rejects filter/top_period and is defined exactly as Discourse's daily top score—not semantic controversy, toxicity, or real-time velocity.{ results: [{id, slug, title, category_id, tags, created_at, last_posted_at, bumped_at, posts_count, reply_count, views, like_count, posters_count, closed, archived, pinned, visible, last_poster_username, posters}], meta: {view, top_period, page, per_page, returned, has_more, total?} }. Missing optional values remain null; total and continuation are never fabricated.=category = without subcats, - prefix = exclude); tag/tags (comma = OR, + = AND) and tag_group; status:(open|closed|archived|listed|unlisted|public); personal in: (bookmarked|watching|tracking|muted|pinned); dates: created/activity/latest-post-(before|after) with YYYY-MM-DD or relative days N; numeric: likes[-op]-(min|max), posts-(min|max), posters-(min|max), views-(min|max); order: activity|created|latest-post|likes|likes-op|posters|title|views|category with optional -asc; free text terms are matched.--toolsets moderation; all require authentication)
discourse_get_review_queue_count: {} → { count, unit: "pending_reviewable_queue_items", status: "pending", scope }, where count is the authoritative number of pending reviewable records visible to the caller, not individual flags.discourse_list_reviewables: stable review filters and offset pagination → normalized reviewables with named status plus numeric status_id, current versions, bounded evidence, scores, targets, and dynamic actions. For normal triage send only status and offset; use meta.total and follow next_offset. Upstream page size is fixed at 10.discourse_list_reviewable_topics: {} → a non-exhaustive aggregation of pending topics at or above Discourse's minimum review priority. score_count counts review score/flag records, not reviewable queue items; queue items without topics are absent.discourse_get_reviewable: { reviewable_id; include_explanation? } → refreshed bounded context, side-loaded references, editable fields, score evidence, and exact available actions; no recommendation is generated. Avoid bulk fan-out because list results already contain triage evidence.discourse_perform_reviewable_action (only when writes enabled): { reviewable_id; action_id; expected_version?; additional_fields?; confirm: true } → serialized, freshly preflighted action with normalized success and remaining-count fields. Submit the displayed dynamic action ID; MCP maps its server_action to the Discourse route.discourse_get_chat_messages
{ channel_id: number; page_size?: number (1–50, default 50); target_message_id?: number; direction?: "past" | "future"; target_date?: string (ISO 8601) }{ channel_id, messages: [{id, username, created_at, message, edited, thread_id, in_reply_to_id}], meta }discourse_get_draft
{ draft_key: string; sequence?: number }{ draft_key, sequence, found, data: {title, reply, category_id, tags, action} }discourse_save_draft (only when writes enabled; see Write safety)
{ draft_key: string; reply: string; title?: string; category_id?: number; tags?: string[]; sequence?: number (default 0); action?: "createTopic" | "reply" | "edit" | "privateMessage" }{ draft_key, sequence, saved }discourse_delete_draft (only when writes enabled; see Write safety)
{ draft_key: string; sequence: number }{ draft_key, deleted }discourse_list_private_messages (requires authentication)
{ username?: string; mailbox?: "inbox"|"sent"|"archive"|"unread"|"new"; group_name?: string; page?: number (0-based, default 0); per_page?: number (1–100, default 30) }{ mailbox, username, group_name, messages: [{topic_id, slug, title, posts_count, reply_count, created_at, last_posted_at, bumped_at, last_read_post_number, unread_posts, unseen, topic_archived, message_archived, notification_level, recent_participants}], meta: {page, per_page, has_more} }username uses the authenticated user. Group mailboxes do not support sent; unread and new cannot target another user.discourse_read_private_message (requires authentication)
{ topic_id: number; post_limit?: number (1–50, default 5); start_post_number?: number }{ topic_id, slug, title, archetype, subtype, posts_count, last_read_post_number, topic_archived, message_archived, allowed_users, allowed_groups, posts, meta }allowed_users and allowed_groups are direct records, not an expanded ACL. Public topics are rejected.discourse_create_private_message (only when writes enabled; see Write safety)
{ title: string; raw: string (<= 30k chars); usernames?: string[]; group_names?: string[]; email_addresses?: string[]; author_username?: string } (at least one recipient required){ id, topic_id, post_number, slug, title }discourse_reply_private_message (only when writes enabled; see Write safety)
{ topic_id: number; raw: string (<= 30k chars); reply_to_post_number?: number; author_username?: string }{ id, topic_id, post_number, reply_to_post_number, slug }discourse_invite_to_private_message (only when writes enabled; see Write safety)
{ topic_id: number; username?: string; group_name?: string; email_address?: string; notify_group_members?: boolean; custom_message?: string (<= 3000 chars); author_username?: string } (exactly one recipient required){ topic_id, recipient_type: "email", status: "submitted", participant_added: false, outcome_confirmed: false }custom_message is email-only, group notifications default to enabled, and group names are exact-case.discourse_create_post (only when writes enabled; see Write safety)
{ topic_id: number; raw: string (<= 30k chars); author_username?: string }{ id, topic_id, post_number }discourse_create_topic (only when writes enabled; see Write safety)
{ title: string; raw: string (<= 30k chars); category_id?: number; tags?: string[]; author_username?: string }{ id, topic_id, slug, title }discourse_update_topic (only when writes enabled; see Write safety)
{ topic_id: number; title?: string; category_id?: number; tags?: string[]; featured_link?: string; original_title?: string; original_tags?: string[] }{ success, topic_id, updated_fields, topic: {id, title, slug, category_id, tags, featured_link} }discourse_list_users (requires admin API key)
{ query?: "active"|"new"|"staff"|"suspended"|"silenced"|"pending"|"staged"; filter?: string; order?: "created"|"last_emailed"|"seen"|"username"|"trust_level"|"days_visited"|"posts"; asc?: boolean; page?: number }{ users: [{id, username, name, email, avatar_template, trust_level, created_at, last_seen_at, admin, moderator, suspended, silenced}], meta: {page, has_more} }avatar_template contains {size} placeholder - replace with pixel size (e.g., 120) to get avatar URLdiscourse_create_user (only when writes enabled; see Write safety)
{ username: string (1-20 chars); email: string; name: string; password: string; active?: boolean; approved?: boolean; upload_id?: number }{ success, username, name, email, active, avatar_updated, message, avatar_error? }upload_id is provided but avatar update fails, avatar_error contains the error messagediscourse_update_user (only when writes enabled; see Write safety)
{ username: string; name?: string; bio_raw?: string; location?: string; website?: string; title?: string; date_of_birth?: string; locale?: string; profile_background_upload_url?: string; card_background_upload_url?: string; upload_id?: number }{ success, username, updated_fields, avatar_updated, user: {...}, avatar_error? }upload_id is provided but avatar update fails, avatar_error contains the error messagediscourse_upload_file (only when writes enabled; see Write safety)
{ upload_type: "avatar"|"profile_background"|"card_background"|"composer"; image_data?: string (base64); url?: string; filename?: string; user_id?: number }{ id, url, short_url, short_path, original_filename, extension, width, height, filesize, human_filesize }image_data (requires filename), remote HTTP(S) URL, or absolute local file pathuser_id is required for avatar/profile_background/card_background uploads--allowed_upload_paths configuration (security: prevents arbitrary file reads)short_url (e.g., upload://abc123.png) to embed images in posts.discourse_create_category (only when writes enabled; see Write safety)
{ name: string; color?: hex; text_color?: hex; emoji?: string; icon?: string; parent_category_id?: number; description?: string }{ id, slug, name }discourse_select_site (hidden when --site is provided)
{ site: string }{ site, title }Requirements: Node >= 24, pnpm.
Install / Build / Typecheck / Test
Project layout
src/index.tssrc/http/client.tssrc/tools/registry.tssrc/resources/registry.tssrc/tools/builtin/*src/tools/remote/tool_exec_api.tssrc/util/json_response.tssrc/util/logger.ts, src/util/redact.tsDependency and lockfile policy
packageManager: pnpm@10.14.0) is the authoritative development workflow, but both pnpm-lock.yaml and package-lock.json are tracked for downstream/npm compatibility. Regenerate and commit both whenever dependencies change; CI runs frozen pnpm and clean npm ci builds so neither can silently drift.@modelcontextprotocol/sdk is intentionally pinned exactly to the reviewed 1.30.0 release. SDK updates are deliberate security/compatibility changes and must pass typecheck, real transport/output-schema tests, production high-severity audits for both lockfiles, and packaging smoke tests. Dev-only audit exceptions require a dated owner and remediation plan.Testing notes
dist/test/**/*.js). Ensure pnpm build before pnpm test if invoking scripts individually.Publishing (optional)
@discourse/mcp and exposes a bin named discourse-mcp. Prefer npx @discourse/mcp@latest for frictionless usage.Conventions
See AGENTS.md for additional guidance on using this server from agent frameworks.
try.discourse.org:This MCP server supports two types of Discourse API authentication:
Admin API Keys (api_key + api_username)
Api-Key and Api-UsernameUser API Keys (user_api_key + optional user_api_client_id)
User-Api-Key and User-Api-Client-IdThis package includes a convenient command to generate User API Keys:
The command uses Discourse's device authorization flow on supported sites (Discourse 2026.6.0 and newer):
ABCD-2345.No encrypted payload needs to be copied back into the terminal. For older Discourse sites, the command automatically falls back to the legacy authorization URL and payload prompt.
User API Keys require an OAuth-like flow documented at https://meta.discourse.org/t/user-api-keys-specification/48536. Key steps:
/user-api-key/new with your public key, application name, client ID, and requested scopesYou can also manually create User API Keys via the Discourse UI (if enabled by the site):
create_post missing? You're in read‑only mode. Enable writes as described above.--tools_mode=discourse_api_only.discourse_select_site? Yes, start with --site <url> to tether to a single site.--timeout_ms, and note built‑in retry/backoff on 429/5xx.--log_level debug to see detailed error information including: