hyperdx/packages/api/src/mcp/prompts/dashboards/index.ts
Brandon Pereira 9781ae6387
feat: integrate Model Context Protocol (MCP) server for dashboards & investigations (#2030)
## Summary

Adds an MCP (Model Context Protocol) server to the HyperDX API, enabling AI assistants (Claude, Cursor, OpenCode, etc.) to query observability data, manage dashboards, and explore data sources directly via standardized tool calls.

Key changes:
- **MCP server** (`packages/api/src/mcp/`) — Streamable HTTP transport at `/api/mcp`, authenticated via Personal API Access Key
- **Tools** — `hyperdx_list_sources`, `hyperdx_query`, `hyperdx_get_dashboard`, `hyperdx_save_dashboard`, `hyperdx_delete_dashboard`, `hyperdx_query_tile`
- **Dashboard prompts** — Detailed prompt templates that guide LLMs in generating valid, high-quality dashboards
- **Shared logic** — Refactored dashboard validation/transformation out of the external API router into reusable utils (`packages/api/src/routers/external-api/v2/utils/dashboards.ts`)
- **Documentation** — `MCP.md` with setup instructions for Claude Code, OpenCode, Cursor, MCP Inspector, and other clients
- **Tests** — Unit tests for dashboard tools, query tools, tracing, and response trimming

### Screenshots

https://github.com/user-attachments/assets/8c5aa582-c79e-47e0-8f75-e03feabdf8a6

### How to test locally

1. Start the dev stack: `yarn dev`
2. Connect an MCP client (e.g. MCP Inspector):
   ```bash
   cd packages/api && yarn dev:mcp
   ```
   Then configure the inspector:
   - **Transport Type:** Streamable HTTP
   - **URL:** `http://localhost:8080/api/mcp`
   - **Header:** `Authorization: Bearer <your-personal-access-key>`
   - Click **Connect**
3. Alternatively, connect via Claude Code or OpenCode:
   ```bash
   claude mcp add --transport http hyperdx http://localhost:8080/api/mcp \
     --header "Authorization: Bearer <your-personal-access-key>"
   ```
4. Try listing sources, querying data, or creating/updating a dashboard through the connected AI assistant.
5. Run unit tests:
   ```bash
   cd packages/api && yarn ci:unit
   ```

### References

- Linear Issue: HDX-3710
2026-04-14 14:39:07 +00:00

195 lines
5.6 KiB
TypeScript

import { z } from 'zod';
import { getConnectionsByTeam } from '@/controllers/connection';
import { getSources } from '@/controllers/sources';
import logger from '@/utils/logger';
import type { PromptDefinition } from '../../tools/types';
import {
buildCreateDashboardPrompt,
buildDashboardExamplesPrompt,
buildQueryGuidePrompt,
} from './content';
import {
buildSourceSummary,
getFirstConnectionId,
getFirstSourceId,
} from './helpers';
const dashboardPrompts: PromptDefinition = (server, context) => {
const { teamId } = context;
// ── create_dashboard ──────────────────────────────────────────────────────
server.registerPrompt(
'create_dashboard',
{
title: 'Create a Dashboard',
description:
'Create a HyperDX dashboard with the MCP tools. ' +
'Follow the recommended workflow, pick tile types, write queries, ' +
'and validate results — using your real data sources.',
argsSchema: {
description: z
.string()
.optional()
.describe(
'What the dashboard should monitor (e.g. "API error rates and latency")',
),
},
},
async ({ description }) => {
let sourceSummary: string;
let traceSourceId: string;
let logSourceId: string;
try {
const [sources, connections] = await Promise.all([
getSources(teamId),
getConnectionsByTeam(teamId),
]);
sourceSummary = buildSourceSummary(
sources.map(s => ({
_id: s._id,
name: s.name,
kind: s.kind,
connection: s.connection,
})),
connections.map(c => ({ _id: c._id, name: c.name })),
);
traceSourceId = getFirstSourceId(
sources.map(s => ({ _id: s._id, kind: s.kind })),
'trace',
);
logSourceId = getFirstSourceId(
sources.map(s => ({ _id: s._id, kind: s.kind })),
'log',
);
} catch (e) {
logger.warn(
{ teamId, error: e },
'Failed to fetch sources for create_dashboard prompt',
);
sourceSummary =
'Could not fetch sources. Call hyperdx_list_sources to discover available data.';
traceSourceId = '<SOURCE_ID>';
logSourceId = '<SOURCE_ID>';
}
return {
messages: [
{
role: 'user' as const,
content: {
type: 'text' as const,
text: buildCreateDashboardPrompt(
sourceSummary,
traceSourceId,
logSourceId,
description,
),
},
},
],
};
},
);
// ── dashboard_examples ────────────────────────────────────────────────────
server.registerPrompt(
'dashboard_examples',
{
title: 'Dashboard Examples',
description:
'Get copy-paste-ready dashboard examples for common observability patterns: ' +
'service_overview, error_tracking, latency, log_analysis, infrastructure_sql.',
argsSchema: {
pattern: z
.string()
.optional()
.describe(
'Filter to a specific pattern: service_overview, error_tracking, latency, log_analysis, infrastructure_sql',
),
},
},
async ({ pattern }) => {
let traceSourceId: string;
let logSourceId: string;
let connectionId: string;
try {
const [sources, connections] = await Promise.all([
getSources(teamId),
getConnectionsByTeam(teamId),
]);
traceSourceId = getFirstSourceId(
sources.map(s => ({ _id: s._id, kind: s.kind })),
'trace',
);
logSourceId = getFirstSourceId(
sources.map(s => ({ _id: s._id, kind: s.kind })),
'log',
);
connectionId = getFirstConnectionId(
connections.map(c => ({ _id: c._id })),
);
} catch (e) {
logger.warn(
{ teamId, error: e },
'Failed to fetch sources for dashboard_examples prompt',
);
traceSourceId = '<TRACE_SOURCE_ID>';
logSourceId = '<LOG_SOURCE_ID>';
connectionId = '<CONNECTION_ID>';
}
return {
messages: [
{
role: 'user' as const,
content: {
type: 'text' as const,
text: buildDashboardExamplesPrompt(
traceSourceId,
logSourceId,
connectionId,
pattern,
),
},
},
],
};
},
);
// ── query_guide ───────────────────────────────────────────────────────────
server.registerPrompt(
'query_guide',
{
title: 'Query Writing Guide',
description:
'Look up HyperDX query syntax: aggregation functions, ' +
'Lucene/SQL filters, raw SQL macros, column naming, ' +
'per-tile constraints, and common mistakes.',
},
async () => {
return {
messages: [
{
role: 'user' as const,
content: {
type: 'text' as const,
text: buildQueryGuidePrompt(),
},
},
],
};
},
);
};
export default dashboardPrompts;