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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## [0.2.5](https://github.com/discourse/discourse-mcp/compare/v0.2.4...v0.2.5) (2026-02-03)

### Features

* Add Data Explorer plugin integration
- `explorer_schema` resource: database schema in compact text format (core tables by default)
- `explorer_schema_tables` resource: schema for specific or all tables
- `explorer_queries` resource: saved queries with pagination (30/page, sorted by last used)
- `discourse_get_query` tool: get query details including SQL and parameters
- `discourse_run_query` tool: execute query with parameters
- `discourse_create_query` tool: create new saved query
- `discourse_update_query` tool: update existing query
- `discourse_delete_query` tool: delete query
- `sql_query` prompt: guided SQL workflow for schema discovery and query execution

## [0.2.4](https://github.com/discourse/discourse-mcp/compare/v0.2.3...v0.2.4) (2026-01-20)

### Features
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@discourse/mcp",
"mcpName": "io.github.discourse/mcp",
"version": "0.2.4",
"version": "0.2.5",
"description": "Discourse MCP CLI server (stdio) exposing Discourse tools via MCP",
"author": "Discourse",
"license": "MIT",
Expand Down
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { type AuthMode } from "./http/client.js";
import { registerAllTools, type ToolsMode } from "./tools/registry.js";
import { tryRegisterRemoteTools } from "./tools/remote/tool_exec_api.js";
import { registerAllResources } from "./resources/registry.js";
import { registerAllPrompts } from "./prompts/registry.js";
import { SiteState, type AuthOverride } from "./site/state.js";

const DEFAULT_TIMEOUT_MS = 15000;
Expand Down Expand Up @@ -222,6 +223,7 @@ async function main() {
capabilities: {
tools: { listChanged: false },
resources: { listChanged: false },
prompts: { listChanged: false },
},
}
);
Expand Down Expand Up @@ -258,7 +260,10 @@ async function main() {
});

// Register MCP resources (URI-addressable read-only data)
registerAllResources(server, { siteState, logger });
registerAllResources(server, { siteState, logger, allowAdminTools });

// Register MCP prompts (guided workflows)
registerAllPrompts(server, { siteState, logger, allowAdminTools });

// If tethered and remote tool discovery is enabled, discover now
if (config.site && config.tools_mode !== "discourse_api_only") {
Expand Down
68 changes: 68 additions & 0 deletions src/prompts/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* MCP Prompts Registry
*
* Registers prompts that provide guided workflows for common tasks.
*/

import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { SiteState } from "../site/state.js";
import type { Logger } from "../util/logger.js";
import {
sqlQueryPromptName,
sqlQueryPromptSchema,
getSqlQueryPromptContent,
} from "./sql_query.js";

/** Narrowed interface for prompt registration */
export type PromptRegistrar = Pick<McpServer, "registerPrompt">;

export interface PromptContext {
siteState: SiteState;
logger: Logger;
allowAdminTools?: boolean;
}

/**
* Registers all MCP prompts.
*/
export function registerAllPrompts(
server: PromptRegistrar,
ctx: PromptContext
): void {
// Only register SQL query prompt if admin tools allowed
// Default to computed auth if not explicitly provided
const allowAdminTools = ctx.allowAdminTools ?? ctx.siteState.hasAdminAuth();
if (allowAdminTools) {
registerSqlQueryPrompt(server, ctx);
}
}

function registerSqlQueryPrompt(
server: PromptRegistrar,
_ctx: PromptContext
): void {
server.registerPrompt(
sqlQueryPromptName,
{
description:
"Guided workflow for database queries: discover schema, write SQL, run queries via Data Explorer",
argsSchema: sqlQueryPromptSchema.shape,
},
async (args) => {
const parsed = sqlQueryPromptSchema.safeParse(args);
const validArgs = parsed.success ? parsed.data : {};

return {
messages: [
{
role: "user",
content: {
type: "text",
text: getSqlQueryPromptContent(validArgs),
},
},
],
};
}
);
}
133 changes: 133 additions & 0 deletions src/prompts/sql_query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* SQL Query Workflow Prompt
*
* Provides a guided workflow for discovering schema, writing queries,
* and executing them via the Data Explorer plugin.
*/

import { z } from "zod";

export const sqlQueryPromptName = "sql_query";

export const sqlQueryPromptSchema = z.object({
goal: z
.string()
.optional()
.describe("What you want to learn from the data"),
});

export type SqlQueryPromptArgs = z.infer<typeof sqlQueryPromptSchema>;

export function getSqlQueryPromptContent(args: SqlQueryPromptArgs): string {
const goal = args.goal || "Explore the database";

return `# SQL Query Workflow

Goal: ${goal}

## Step 1: Discover Schema
Use the \`discourse://explorer/schema\` resource to explore available tables and columns.

Key tables you may find useful:
- **users** - User accounts (id, username, name, email, trust_level, created_at, last_seen_at)
- **topics** - Forum topics (id, title, user_id, category_id, created_at, views, posts_count)
- **posts** - Individual posts (id, topic_id, user_id, raw, cooked, created_at, post_number)
- **categories** - Topic categories (id, name, slug, parent_category_id)
- **tags** - Topic tags (id, name, topic_count)
- **topic_tags** - Join table for topics and tags
- **user_actions** - User activity log (user_id, action_type, target_topic_id, target_post_id)
- **notifications** - User notifications
- **groups** - User groups
- **group_users** - Group membership

## Step 2: Check Existing Queries
Use the \`discourse://explorer/queries\` resource to see if a similar query already exists.
This can save time and provide examples of working queries.

## Step 3: Write or Modify Query
- Use \`discourse_create_query\` to save a new query, or
- Use \`discourse_get_query\` to fetch an existing query's SQL for modification

### Query Parameter Syntax
Declare parameters in SQL comments at the top of your query:

\`\`\`sql
-- [params]
-- int :user_id
-- string :username = 'default_value'
-- null date :start_date

SELECT * FROM users WHERE id = :user_id
\`\`\`

### Supported Parameter Types
- **int** - Integer value
- **bigint** - Large integer
- **string** - Text value
- **boolean** - true/false
- **date** - Date (YYYY-MM-DD)
- **datetime** - Date and time
- **user_id** - User ID with autocomplete
- **post_id** - Post ID
- **topic_id** - Topic ID
- **category_id** - Category ID with autocomplete
- **group_id** - Group ID with autocomplete
- **badge_id** - Badge ID with autocomplete
- **int_list** - Comma-separated integers
- **string_list** - Comma-separated strings

Prefix with \`null\` to make a parameter optional: \`-- null int :optional_id\`

## Step 4: Run Query
Use \`discourse_run_query\` with the query ID and any required parameters.

Example:
\`\`\`json
{
"id": 123,
"params": { "user_id": 1 },
"limit": 100
}
\`\`\`

## Safety Notes
- Queries run in **read-only transactions** with a 10-second timeout
- **Sensitive columns** (emails, IPs, tokens) are marked in the schema - handle with care
- Use **LIMIT** to avoid returning too many rows (default is usually fine)
- The \`explain\` option shows the query execution plan for debugging performance

## Example Queries

### Recent active users
\`\`\`sql
SELECT username, last_seen_at, trust_level
FROM users
WHERE last_seen_at > CURRENT_DATE - INTERVAL '7 days'
ORDER BY last_seen_at DESC
LIMIT 50
\`\`\`

### Posts per category (last 30 days)
\`\`\`sql
SELECT c.name, COUNT(p.id) as post_count
FROM posts p
JOIN topics t ON p.topic_id = t.id
JOIN categories c ON t.category_id = c.id
WHERE p.created_at > CURRENT_DATE - INTERVAL '30 days'
GROUP BY c.id, c.name
ORDER BY post_count DESC
\`\`\`

### User activity with parameters
\`\`\`sql
-- [params]
-- user_id :user_id

SELECT action_type, COUNT(*) as count
FROM user_actions
WHERE user_id = :user_id
GROUP BY action_type
ORDER BY count DESC
\`\`\`
`;
}
Loading