Skip to content

Commit 748315d

Browse files
feat(mcp): advertise server title, description, website, and icon (#3394)
## Summary MCP directories and clients score servers on the `Implementation` metadata in the initialize response ([spec 2026-07-28: Implementation](https://modelcontextprotocol.io/specification/2026-07-28/schema#implementation), [Icon](https://modelcontextprotocol.io/specification/2026-07-28/schema#icon)): a human-readable title, a useful description, a public website, and a scalable or >=128px icon. We only sent `name` and `version`. This adds to the MCP server's `serverInfo`: - `title`: "ClickStack" - `description`: a short summary of what the server does - `websiteUrl`: the ClickStack docs - `icons`: the HyperDX logo SVG (`sizes: ["any"]`), served by the app at `${FRONTEND_URL}/favicons/hyperdx/favicon.svg`. Omitted when `FRONTEND_URL` is unset, since icon sources must be absolute URLs Instructions and capabilities are unchanged. All fields exist on the SDK 1.29 `Implementation` type. Covered by `packages/api/src/mcp/__tests__/serverMetadata.test.ts`. ### How to reproduce **Starting point:** local full stack via `yarn dev` (the MCP server needs the API). **On `main`:** 1. Run `yarn dev`. 2. Run `npx @modelcontextprotocol/inspector` and connect it to the local MCP endpoint (see `MCP.md`). 3. Check the initialize response: `serverInfo` has only `name` and `version`. **On this branch:** 1. Repeat the steps above. 2. `serverInfo` now also has `title`, `description`, `websiteUrl`, and `icons`. `tools/list` and instructions match `main`. ### References - Linear Issue: - Related PRs:
1 parent 85675fc commit 748315d

3 files changed

Lines changed: 90 additions & 10 deletions

File tree

‎.changeset/mcp-server-metadata.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperdx/api': patch
3+
---
4+
5+
feat(mcp): advertise a title, description, website URL, and SVG icon in the MCP server's initialize response
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { Implementation } from '@modelcontextprotocol/sdk/types.js';
2+
3+
import { McpContext } from '@/mcp/tools/types';
4+
5+
const context: McpContext = { teamId: 'team-id', userId: 'user-id' };
6+
7+
async function getServerInfo(
8+
frontendUrl: string,
9+
): Promise<Implementation | undefined> {
10+
let createTestClient!: typeof import('./mcpTestUtils').createTestClient;
11+
jest.isolateModules(() => {
12+
jest.doMock('@/config', () => ({
13+
...jest.requireActual('@/config'),
14+
FRONTEND_URL: frontendUrl,
15+
}));
16+
({ createTestClient } =
17+
jest.requireActual<typeof import('./mcpTestUtils')>('./mcpTestUtils'));
18+
});
19+
jest.dontMock('@/config');
20+
const client = await createTestClient(context);
21+
return client.getServerVersion();
22+
}
23+
24+
describe('MCP server metadata', () => {
25+
it('advertises a title, description, website, and app-hosted icon', async () => {
26+
const info = await getServerInfo('https://app.example.com/');
27+
28+
expect(info).toMatchObject({
29+
name: 'clickstack',
30+
title: 'ClickStack',
31+
websiteUrl: expect.stringMatching(/^https:\/\//),
32+
description: expect.any(String),
33+
});
34+
expect(info?.icons).toEqual([
35+
{
36+
src: 'https://app.example.com/favicons/hyperdx/favicon.svg',
37+
mimeType: 'image/svg+xml',
38+
sizes: ['any'],
39+
},
40+
]);
41+
});
42+
43+
it('keeps the FRONTEND_URL sub-path in the icon URL', async () => {
44+
const info = await getServerInfo('https://host.example.com/hyperdx');
45+
46+
expect(info?.icons?.[0]?.src).toBe(
47+
'https://host.example.com/hyperdx/favicons/hyperdx/favicon.svg',
48+
);
49+
});
50+
51+
it('omits the icon when FRONTEND_URL is unset', async () => {
52+
const info = await getServerInfo('');
53+
54+
expect(info?.title).toBe('ClickStack');
55+
expect(info?.icons).toBeUndefined();
56+
});
57+
});

‎packages/api/src/mcp/mcpServer.ts‎

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2+
import type { Implementation } from '@modelcontextprotocol/sdk/types.js';
23

3-
import { CODE_VERSION } from '@/config';
4+
import { CODE_VERSION, FRONTEND_URL } from '@/config';
45

56
import dashboardPrompts from './prompts/dashboards/index';
67
import alertsTools from './tools/alerts/index';
@@ -35,16 +36,33 @@ const SERVER_INSTRUCTIONS = [
3536
'dashboard authoring.',
3637
].join('\n');
3738

39+
// Served by the app; omitted when FRONTEND_URL is unset because MCP icon
40+
// sources must be absolute URLs. String concatenation (not `new URL`) keeps
41+
// any sub-path the app is mounted under. The HyperDX logo is intentional.
42+
const SERVER_ICON_URL = FRONTEND_URL
43+
? `${FRONTEND_URL.replace(/\/+$/, '')}/favicons/hyperdx/favicon.svg`
44+
: undefined;
45+
46+
const SERVER_INFO: Implementation = {
47+
name: 'clickstack',
48+
title: 'ClickStack',
49+
version: `${CODE_VERSION}-beta`,
50+
description:
51+
'Query and investigate logs, metrics, traces, and sessions stored in ' +
52+
'ClickHouse, and manage ClickStack dashboards, alerts, data sources, ' +
53+
'and saved searches.',
54+
websiteUrl: 'https://clickhouse.com/docs/use-cases/observability/clickstack',
55+
...(SERVER_ICON_URL && {
56+
icons: [
57+
{ src: SERVER_ICON_URL, mimeType: 'image/svg+xml', sizes: ['any'] },
58+
],
59+
}),
60+
};
61+
3862
export function createServer(context: McpContext) {
39-
const server = new McpServer(
40-
{
41-
name: 'clickstack',
42-
version: `${CODE_VERSION}-beta`,
43-
},
44-
{
45-
instructions: SERVER_INSTRUCTIONS,
46-
},
47-
);
63+
const server = new McpServer(SERVER_INFO, {
64+
instructions: SERVER_INSTRUCTIONS,
65+
});
4866

4967
const registerTool = createRegisterTool(server, context);
5068
const registrar = { server, context, registerTool };

0 commit comments

Comments
 (0)