From a8ddf856991f30c928cc44d1f8c6c1de7cb07a83 Mon Sep 17 00:00:00 2001 From: Alex Fedotyev <61838744+alex-fedotyev@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:48:30 +0000 Subject: [PATCH] feat(mcp): include source section in list/describe source tools Surface the optional source `section` label in the clickstack_list_sources and clickstack_describe_source tool output, so agents see the same source grouping the source selector shows. The key is omitted when a source has no section, matching the read-only external API behavior. Co-Authored-By: Claude Opus 4.8 --- .changeset/mcp-source-section.md | 10 ++++++ .../api/src/mcp/__tests__/sources.test.ts | 32 +++++++++++++++++++ .../src/mcp/tools/sources/describeSource.ts | 4 +++ .../api/src/mcp/tools/sources/listSources.ts | 4 +++ 4 files changed, 50 insertions(+) create mode 100644 .changeset/mcp-source-section.md diff --git a/.changeset/mcp-source-section.md b/.changeset/mcp-source-section.md new file mode 100644 index 0000000000..d27abc7a7c --- /dev/null +++ b/.changeset/mcp-source-section.md @@ -0,0 +1,10 @@ +--- +"@hyperdx/api": patch +--- + +feat: include the source Section in MCP source tools + +The `clickstack_list_sources` and `clickstack_describe_source` MCP tools now +return the optional Section label on each source, so agents see the same source +grouping that the source selector shows. Sources without a section are +unchanged. diff --git a/packages/api/src/mcp/__tests__/sources.test.ts b/packages/api/src/mcp/__tests__/sources.test.ts index 008e4fe852..e396165817 100644 --- a/packages/api/src/mcp/__tests__/sources.test.ts +++ b/packages/api/src/mcp/__tests__/sources.test.ts @@ -78,6 +78,7 @@ describe('MCP Source Tools', () => { traceIdExpression: 'TraceId', connection: connection._id, name: 'Logs', + section: 'Billing', }); const context: McpContext = { @@ -149,6 +150,19 @@ describe('MCP Source Tools', () => { expect(log.keyColumns).toHaveProperty('body'); }); + it('includes a source section when set and omits it when unset', async () => { + const result = await callTool(client, 'clickstack_list_sources'); + const output = JSON.parse(getFirstText(result)); + + const log = output.sources.find((s: any) => s.kind === SourceKind.Log); + expect(log.section).toBe('Billing'); + + const trace = output.sources.find( + (s: any) => s.kind === SourceKind.Trace, + ); + expect(trace.section).toBeUndefined(); + }); + it('should return empty sources for a team with no sources', async () => { await client.close(); await server.clearDBs(); @@ -222,6 +236,24 @@ describe('MCP Source Tools', () => { }); }); + it('includes the source section when set and omits it when unset', async () => { + const withSection = await callTool(client, 'clickstack_describe_source', { + sourceId: logSource._id.toString(), + }); + expect(JSON.parse(getFirstText(withSection)).source.section).toBe( + 'Billing', + ); + + const withoutSection = await callTool( + client, + 'clickstack_describe_source', + { sourceId: traceSource._id.toString() }, + ); + expect( + JSON.parse(getFirstText(withoutSection)).source.section, + ).toBeUndefined(); + }); + it('should include map attribute keys', async () => { const result = await callTool(client, 'clickstack_describe_source', { sourceId: traceSource._id.toString(), diff --git a/packages/api/src/mcp/tools/sources/describeSource.ts b/packages/api/src/mcp/tools/sources/describeSource.ts index 5674c7e2cf..24d0616f39 100644 --- a/packages/api/src/mcp/tools/sources/describeSource.ts +++ b/packages/api/src/mcp/tools/sources/describeSource.ts @@ -58,6 +58,10 @@ async function describeSourceSchema( timestampColumn: source.timestampValueExpression, }; + if (source.section) { + meta.section = source.section; + } + if ( 'eventAttributesExpression' in source && source.eventAttributesExpression diff --git a/packages/api/src/mcp/tools/sources/listSources.ts b/packages/api/src/mcp/tools/sources/listSources.ts index 0c1e614689..d205adc117 100644 --- a/packages/api/src/mcp/tools/sources/listSources.ts +++ b/packages/api/src/mcp/tools/sources/listSources.ts @@ -44,6 +44,10 @@ export function registerListSources( timestampColumn: s.timestampValueExpression, }; + if (s.section) { + meta.section = s.section; + } + if ('eventAttributesExpression' in s && s.eventAttributesExpression) { meta.eventAttributesColumn = s.eventAttributesExpression; }