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
10 changes: 10 additions & 0 deletions .changeset/rename-mcp-tools-to-clickstack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@hyperdx/api": patch
"@hyperdx/common-utils": patch
---

refactor(mcp): rename all MCP tool prefixes from `hyperdx_` to `clickstack_`

Rename the MCP server name from `hyperdx` to `clickstack` and update all 19
tool names (e.g. `hyperdx_search` → `clickstack_search`), along with
descriptions, prompts, error messages, and test references.
46 changes: 23 additions & 23 deletions MCP.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# HyperDX MCP Server
# ClickStack MCP Server

HyperDX exposes a
[Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that
Expand Down Expand Up @@ -41,7 +41,7 @@ instance URL (e.g. `http://localhost:8080`).
### Claude Code

```bash
claude mcp add --transport http hyperdx <your-hyperdx-url>/api/mcp \
claude mcp add --transport http clickstack <your-hyperdx-url>/api/mcp \
--header "Authorization: Bearer <your-personal-access-key>"
```

Expand All @@ -51,7 +51,7 @@ Add the following to your [OpenCode config](https://opencode.ai/docs/config):

```json
"mcp": {
"hyperdx": {
"clickstack": {
"enabled": true,
"type": "remote",
"url": "<your-hyperdx-url>/api/mcp",
Expand All @@ -70,7 +70,7 @@ settings):
```json
{
"mcpServers": {
"hyperdx": {
"clickstack": {
"url": "<your-hyperdx-url>/api/mcp",
"headers": {
"Authorization": "Bearer <your-personal-access-key>"
Expand Down Expand Up @@ -108,22 +108,22 @@ with:

| Tool | Description |
| ----------------------------- | -------------------------------------------------------------------------------------------- |
| `hyperdx_list_sources` | List all data sources and connections as a lightweight catalog (IDs, names, kinds) |
| `hyperdx_describe_source` | Full column schema, attribute keys, and sampled low-cardinality values for a single source |
| `hyperdx_timeseries` | Plot metrics over time as a line or stacked bar chart |
| `hyperdx_table` | Compute aggregated metrics as a table, single number, or pie chart |
| `hyperdx_search` | Browse individual log, event, or trace rows |
| `hyperdx_event_patterns` | Discover the most common log messages and event patterns using Drain clustering |
| `hyperdx_event_deltas` | Compare two row groups and rank properties by how their value distributions differ |
| `hyperdx_sql` | Execute raw ClickHouse SQL for advanced queries (JOINs, CTEs, sub-queries) |
| `hyperdx_get_dashboard` | List all dashboards or get full detail for a specific dashboard |
| `hyperdx_save_dashboard` | Create or update a dashboard with tiles (charts, tables, numbers, search, markdown) |
| `hyperdx_delete_dashboard` | Permanently delete a dashboard and its attached alerts |
| `hyperdx_query_tile` | Execute the query for a specific dashboard tile to validate results |
| `hyperdx_get_saved_search` | List all saved searches or get full detail for a specific saved search |
| `hyperdx_save_saved_search` | Create or update a saved search (reusable query against a data source) |
| `hyperdx_get_alert` | List alerts (summary) or get full detail with evaluation history; filter by state |
| `hyperdx_save_alert` | Create a new alert or update an existing one |
| `hyperdx_trace_waterfall` | Fetch all spans in a single trace as a parent/child waterfall tree with optional correlated logs |
| `hyperdx_trace_top_time_consuming_operations` | Aggregate breakdown of child operations by cumulative time across matching parent traces |
| `hyperdx_get_webhook` | List available webhook destinations for use as alert notification channels |
| `clickstack_list_sources` | List all data sources and connections as a lightweight catalog (IDs, names, kinds) |
| `clickstack_describe_source` | Full column schema, attribute keys, and sampled low-cardinality values for a single source |
| `clickstack_timeseries` | Plot metrics over time as a line or stacked bar chart |
| `clickstack_table` | Compute aggregated metrics as a table, single number, or pie chart |
| `clickstack_search` | Browse individual log, event, or trace rows |
| `clickstack_event_patterns` | Discover the most common log messages and event patterns using Drain clustering |
| `clickstack_event_deltas` | Compare two row groups and rank properties by how their value distributions differ |
| `clickstack_sql` | Execute raw ClickHouse SQL for advanced queries (JOINs, CTEs, sub-queries) |
| `clickstack_get_dashboard` | List all dashboards or get full detail for a specific dashboard |
| `clickstack_save_dashboard` | Create or update a dashboard with tiles (charts, tables, numbers, search, markdown) |
| `clickstack_delete_dashboard` | Permanently delete a dashboard and its attached alerts |
| `clickstack_query_tile` | Execute the query for a specific dashboard tile to validate results |
| `clickstack_get_saved_search` | List all saved searches or get full detail for a specific saved search |
| `clickstack_save_saved_search` | Create or update a saved search (reusable query against a data source) |
| `clickstack_get_alert` | List alerts (summary) or get full detail with evaluation history; filter by state |
| `clickstack_save_alert` | Create a new alert or update an existing one |
| `clickstack_trace_waterfall` | Fetch all spans in a single trace as a parent/child waterfall tree with optional correlated logs |
| `clickstack_trace_top_time_consuming_operations` | Aggregate breakdown of child operations by cumulative time across matching parent traces |
| `clickstack_get_webhook` | List available webhook destinations for use as alert notification channels |
64 changes: 32 additions & 32 deletions packages/api/src/mcp/__tests__/alerts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,15 @@ describe('MCP Alert Tools', () => {
}).save();
}

// ─── hyperdx_get_alert ────────────────────────────────────────────────────
// ─── clickstack_get_alert ────────────────────────────────────────────────────

describe('hyperdx_get_alert', () => {
describe('clickstack_get_alert', () => {
describe('list (no id)', () => {
it('should list all alerts with slim summary fields', async () => {
await createTestAlert({ name: 'Alert 1' });
await createTestAlert({ name: 'Alert 2' });

const result = await callTool(client, 'hyperdx_get_alert', {});
const result = await callTool(client, 'clickstack_get_alert', {});

expect(result.isError).toBeFalsy();
const output = JSON.parse(getFirstText(result));
Expand All @@ -153,7 +153,7 @@ describe('MCP Alert Tools', () => {
});

it('should return empty array when no alerts exist', async () => {
const result = await callTool(client, 'hyperdx_get_alert', {});
const result = await callTool(client, 'clickstack_get_alert', {});

expect(result.isError).toBeFalsy();
const output = JSON.parse(getFirstText(result));
Expand All @@ -165,7 +165,7 @@ describe('MCP Alert Tools', () => {
await createTestAlert({ name: 'OK', state: AlertState.OK });
await createTestAlert({ name: 'Disabled', state: AlertState.DISABLED });

const result = await callTool(client, 'hyperdx_get_alert', {
const result = await callTool(client, 'clickstack_get_alert', {
state: 'ALERT',
});

Expand All @@ -179,7 +179,7 @@ describe('MCP Alert Tools', () => {
it('should return empty array when no alerts match state filter', async () => {
await createTestAlert({ name: 'All Good', state: AlertState.OK });

const result = await callTool(client, 'hyperdx_get_alert', {
const result = await callTool(client, 'clickstack_get_alert', {
state: 'ALERT',
});

Expand All @@ -198,7 +198,7 @@ describe('MCP Alert Tools', () => {
const client2 = await createTestClient(otherTeamContext);

// List should be empty for the other team
const listResult = await callTool(client2, 'hyperdx_get_alert', {});
const listResult = await callTool(client2, 'clickstack_get_alert', {});
const output = JSON.parse(getFirstText(listResult));
expect(output).toHaveLength(0);

Expand All @@ -220,7 +220,7 @@ describe('MCP Alert Tools', () => {
// no name set
}).save();

const result = await callTool(client, 'hyperdx_get_alert', {});
const result = await callTool(client, 'clickstack_get_alert', {});

expect(result.isError).toBeFalsy();
const output = JSON.parse(getFirstText(result));
Expand All @@ -244,7 +244,7 @@ describe('MCP Alert Tools', () => {
// no name set
}).save();

const result = await callTool(client, 'hyperdx_get_alert', {});
const result = await callTool(client, 'clickstack_get_alert', {});

expect(result.isError).toBeFalsy();
const output = JSON.parse(getFirstText(result));
Expand All @@ -257,7 +257,7 @@ describe('MCP Alert Tools', () => {
it('should get full alert detail with history when valid id is provided', async () => {
const alert = await createTestAlert({ name: 'Detail Test' });

const result = await callTool(client, 'hyperdx_get_alert', {
const result = await callTool(client, 'clickstack_get_alert', {
id: alert._id.toString(),
});

Expand All @@ -274,7 +274,7 @@ describe('MCP Alert Tools', () => {
});

it('should return error for invalid ObjectId format', async () => {
const result = await callTool(client, 'hyperdx_get_alert', {
const result = await callTool(client, 'clickstack_get_alert', {
id: 'not-a-valid-id',
});

Expand All @@ -284,7 +284,7 @@ describe('MCP Alert Tools', () => {

it('should return error for non-existent alert id', async () => {
const fakeId = '000000000000000000000000';
const result = await callTool(client, 'hyperdx_get_alert', {
const result = await callTool(client, 'clickstack_get_alert', {
id: fakeId,
});

Expand All @@ -301,7 +301,7 @@ describe('MCP Alert Tools', () => {
};
const client2 = await createTestClient(otherTeamContext);

const getResult = await callTool(client2, 'hyperdx_get_alert', {
const getResult = await callTool(client2, 'clickstack_get_alert', {
id: alert._id.toString(),
});
expect(getResult.isError).toBe(true);
Expand All @@ -312,15 +312,15 @@ describe('MCP Alert Tools', () => {
});
});

// ─── hyperdx_save_alert ───────────────────────────────────────────────────
// ─── clickstack_save_alert ───────────────────────────────────────────────────

describe('hyperdx_save_alert', () => {
describe('clickstack_save_alert', () => {
describe('create', () => {
it('should create a saved-search alert', async () => {
const savedSearch = await createTestSavedSearch();
const webhook = await createTestWebhook();

const result = await callTool(client, 'hyperdx_save_alert', {
const result = await callTool(client, 'clickstack_save_alert', {
source: 'saved_search',
savedSearchId: savedSearch._id.toString(),
threshold: 50,
Expand All @@ -346,7 +346,7 @@ describe('MCP Alert Tools', () => {
const dashboard = await createTestDashboardWithTile();
const webhook = await createTestWebhook();

const result = await callTool(client, 'hyperdx_save_alert', {
const result = await callTool(client, 'clickstack_save_alert', {
source: 'tile',
dashboardId: dashboard._id.toString(),
tileId: 'tile-1',
Expand All @@ -371,7 +371,7 @@ describe('MCP Alert Tools', () => {
it('should reject tile source without dashboardId', async () => {
const webhook = await createTestWebhook();

const result = await callTool(client, 'hyperdx_save_alert', {
const result = await callTool(client, 'clickstack_save_alert', {
source: 'tile',
tileId: 'tile-1',
threshold: 100,
Expand All @@ -391,7 +391,7 @@ describe('MCP Alert Tools', () => {
const dashboard = await createTestDashboardWithTile();
const webhook = await createTestWebhook();

const result = await callTool(client, 'hyperdx_save_alert', {
const result = await callTool(client, 'clickstack_save_alert', {
source: 'tile',
dashboardId: dashboard._id.toString(),
threshold: 100,
Expand All @@ -410,7 +410,7 @@ describe('MCP Alert Tools', () => {
it('should reject saved_search source without savedSearchId', async () => {
const webhook = await createTestWebhook();

const result = await callTool(client, 'hyperdx_save_alert', {
const result = await callTool(client, 'clickstack_save_alert', {
source: 'saved_search',
threshold: 100,
thresholdType: 'above',
Expand All @@ -429,7 +429,7 @@ describe('MCP Alert Tools', () => {
const webhook = await createTestWebhook();
const fakeId = '000000000000000000000000';

const result = await callTool(client, 'hyperdx_save_alert', {
const result = await callTool(client, 'clickstack_save_alert', {
source: 'saved_search',
savedSearchId: fakeId,
threshold: 100,
Expand All @@ -448,7 +448,7 @@ describe('MCP Alert Tools', () => {
it('should reject webhook channel without webhookId', async () => {
const savedSearch = await createTestSavedSearch();

const result = await callTool(client, 'hyperdx_save_alert', {
const result = await callTool(client, 'clickstack_save_alert', {
source: 'saved_search',
savedSearchId: savedSearch._id.toString(),
threshold: 100,
Expand All @@ -467,7 +467,7 @@ describe('MCP Alert Tools', () => {
const savedSearch = await createTestSavedSearch();
const webhook = await createTestWebhook();

const result = await callTool(client, 'hyperdx_save_alert', {
const result = await callTool(client, 'clickstack_save_alert', {
source: 'saved_search',
savedSearchId: savedSearch._id.toString(),
threshold: 100,
Expand All @@ -487,7 +487,7 @@ describe('MCP Alert Tools', () => {
const savedSearch = await createTestSavedSearch();
const webhook = await createTestWebhook();

const result = await callTool(client, 'hyperdx_save_alert', {
const result = await callTool(client, 'clickstack_save_alert', {
source: 'saved_search',
savedSearchId: savedSearch._id.toString(),
threshold: 75,
Expand Down Expand Up @@ -532,7 +532,7 @@ describe('MCP Alert Tools', () => {
name: 'Original Name',
}).save();

const result = await callTool(client, 'hyperdx_save_alert', {
const result = await callTool(client, 'clickstack_save_alert', {
id: alert._id.toString(),
source: 'saved_search',
savedSearchId: savedSearch._id.toString(),
Expand All @@ -559,7 +559,7 @@ describe('MCP Alert Tools', () => {
const webhook = await createTestWebhook();
const fakeId = '000000000000000000000000';

const result = await callTool(client, 'hyperdx_save_alert', {
const result = await callTool(client, 'clickstack_save_alert', {
id: fakeId,
source: 'saved_search',
savedSearchId: savedSearch._id.toString(),
Expand All @@ -580,7 +580,7 @@ describe('MCP Alert Tools', () => {
const savedSearch = await createTestSavedSearch();
const webhook = await createTestWebhook();

const result = await callTool(client, 'hyperdx_save_alert', {
const result = await callTool(client, 'clickstack_save_alert', {
id: '!!!',
source: 'saved_search',
savedSearchId: savedSearch._id.toString(),
Expand All @@ -599,9 +599,9 @@ describe('MCP Alert Tools', () => {
});
});

// ─── hyperdx_get_webhook ──────────────────────────────────────────────────
// ─── clickstack_get_webhook ──────────────────────────────────────────────────

describe('hyperdx_get_webhook', () => {
describe('clickstack_get_webhook', () => {
it('should list all webhooks with slim fields', async () => {
await Webhook.create({
team: team._id,
Expand All @@ -616,7 +616,7 @@ describe('MCP Alert Tools', () => {
url: 'https://example.com/hook2',
});

const result = await callTool(client, 'hyperdx_get_webhook', {});
const result = await callTool(client, 'clickstack_get_webhook', {});

expect(result.isError).toBeFalsy();
const output = JSON.parse(getFirstText(result));
Expand All @@ -635,7 +635,7 @@ describe('MCP Alert Tools', () => {
});

it('should return empty array when no webhooks exist', async () => {
const result = await callTool(client, 'hyperdx_get_webhook', {});
const result = await callTool(client, 'clickstack_get_webhook', {});

expect(result.isError).toBeFalsy();
const output = JSON.parse(getFirstText(result));
Expand All @@ -656,7 +656,7 @@ describe('MCP Alert Tools', () => {
};
const client2 = await createTestClient(otherTeamContext);

const listResult = await callTool(client2, 'hyperdx_get_webhook', {});
const listResult = await callTool(client2, 'clickstack_get_webhook', {});
const output = JSON.parse(getFirstText(listResult));
expect(output).toHaveLength(0);

Expand Down
Loading
Loading