From f4c4dae91cc7d4e7a52198b27e4b0457069c8e58 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Fri, 30 Oct 2026 18:38:45 +0000 Subject: [PATCH] feat(mcp): the server gives hosts its title and icon (#3095) `initialize` now answers with serverInfo.title "CodeGraph" and one embedded SVG in serverInfo.icons, on the direct path and from the daemon proxy, which share SERVER_INFO. A host that shows them can list CodeGraph by name and logo instead of a plain `codegraph`. The icon is the site's favicon (site/public/favicon.svg) on a light tile. The bare mark is dark ink on nothing, and on a dark background only its two white dots show (checked in a browser at 16 to 64 px). It is spelled out in the source instead of as a base64 blob, so a reader can see it is three lines and three circles. `sizes` is left off the icon on purpose. It's optional and says nothing about an SVG, and @modelcontextprotocol/sdk 1.18.0 typed it as a string where the spec settled on an array, so a host on that SDK would fail the whole handshake on the array form. Left out from PR #879, on purpose: `_meta.icons` and icons on every tool. Those add about 1 KB per tool to each tools/list for no host we could find that needs them; Codex reads serverInfo.icons. What each host shows, read from its source or binary on 2026-10-30: - Codex: keeps serverInfo.title and .icons and sends them to its app (McpServerStatus.serverInfo); the terminal UI's /mcp doesn't read them. - opencode: reads serverInfo.title into the server list; its dialogs still print the config name, and it doesn't read icons. - Claude Code 2.1.296: connects fine with these fields (checked with `claude mcp list` in a sandbox home), and no read of title or icons turns up in its binary, so it keeps showing the config name. - Cursor: closed source and not on this machine. Its docs mention no server title or icon, so this is unverified. Validation: 3 tests in mcp-initialize fail on main's session and pass here; mcp-daemon now pins the proxy's serverInfo to the same object. Reimplements PR #879 by @fedosov. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/mcp-daemon.test.ts | 3 ++- __tests__/mcp-initialize.test.ts | 30 ++++++++++++++++++++++++++++++ src/mcp/session.ts | 25 +++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bb0d1c54..fcbd764b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### New Features - `codegraph files --format package` and `codegraph_files` with `format: "package"` now list a project's files by the package that owns them, the nearest `package.json` or `composer.json` above each file that names one, so a monorepo splits into its workspace members with no setup and files nothing claims get a section of their own (#1871). Thanks @LuisAlbertoMK. +- CodeGraph's MCP server now gives agents and editors its display name and icon, so one that shows them, like the Codex app, lists CodeGraph by name and logo instead of a plain `codegraph`. Thanks @fedosov. ### Fixes diff --git a/__tests__/mcp-daemon.test.ts b/__tests__/mcp-daemon.test.ts index 5b8b30500..0f77a8d97 100644 --- a/__tests__/mcp-daemon.test.ts +++ b/__tests__/mcp-daemon.test.ts @@ -43,6 +43,7 @@ import { getDaemonSocketPath } from '../src/mcp/daemon-paths'; import { CodeGraphPackageVersion } from '../src/mcp/version'; import { once } from 'events'; import { WASM_RUNTIME_FLAGS } from '../src/extraction/wasm-runtime-flags'; +import { SERVER_INFO } from '../src/mcp/session'; import { recordSpawns, removeSpawnLog, settleLosingCandidates } from './daemon-candidates'; const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js'); @@ -282,7 +283,7 @@ describe('Shared MCP daemon (issue #411)', () => { servers.push(first); sendInitialize(first.child, `file://${tempDir}`, 1); const firstResp = await waitFor(() => findResponse(first.stdout, 1), 10000); - expect(firstResp.result.serverInfo.name).toBe('codegraph'); + expect(firstResp.result.serverInfo).toEqual(SERVER_INFO); // The launcher is a PROXY (not the daemon itself) — that's the detach fix. await waitFor(() => first.stderr.some((l) => l.includes('Attached to shared daemon')), 8000); diff --git a/__tests__/mcp-initialize.test.ts b/__tests__/mcp-initialize.test.ts index f155ba973..ca1812462 100644 --- a/__tests__/mcp-initialize.test.ts +++ b/__tests__/mcp-initialize.test.ts @@ -18,9 +18,23 @@ import * as os from 'os'; import { CodeGraph } from '../src'; import { once } from 'events'; import { WASM_RUNTIME_FLAGS } from '../src/extraction/wasm-runtime-flags'; +import { SERVER_INFO } from '../src/mcp/session'; const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js'); +/** The fields of `serverInfo` a host can draw. */ +function expectServerBranding(info: { title?: string; icons?: Array> }): void { + expect(info.title).toBe('CodeGraph'); + expect(info.icons).toHaveLength(1); + const icon = info.icons![0]!; + expect(icon.mimeType).toBe('image/svg+xml'); + expect(String(icon.src)).toMatch(/^data:image\/svg\+xml;base64,/); + const svg = Buffer.from(String(icon.src).split(',')[1]!, 'base64').toString('utf8'); + expect(svg).toMatch(/^ } } +describe('MCP server title and icon', () => { + it('names the server and carries an icon no host has to fetch', () => { + expectServerBranding(SERVER_INFO); + }); + + it('keeps the machine name hosts key on, and nothing but shapes in the icon', () => { + expect(SERVER_INFO.name).toBe('codegraph'); + const svg = Buffer.from(SERVER_INFO.icons[0]!.src.split(',')[1]!, 'base64').toString('utf8'); + expect(svg.match(/<\w+/g)?.sort()).toEqual( + [' { let tempDir: string; let child: ChildProcessWithoutNullStreams | null = null; @@ -179,6 +208,7 @@ describe('MCP initialize handshake (issue #172)', () => { const json = JSON.parse(response.text); expect(json.id).toBe(0); expect(json.result.serverInfo.name).toBe('codegraph'); + expectServerBranding(json.result.serverInfo); }, 20000); it('answers resources/list and prompts/list with empty lists, not -32601 (issue #621)', async () => { diff --git a/src/mcp/session.ts b/src/mcp/session.ts index 610c342a3..3a83ff2b7 100644 --- a/src/mcp/session.ts +++ b/src/mcp/session.ts @@ -23,15 +23,40 @@ import { getTelemetry, ClientInfo } from '../telemetry'; import { getUpdateNotice } from '../upgrade/update-check'; import { ExploreSessionState } from './explore-session-state'; +/** + * The site's favicon on a light tile: the bare mark is dark ink on nothing, + * which a dark UI loses. Written out rather than as base64 so a reader can see + * it is two lines of shapes and nothing else. + */ +const SERVER_ICON_SVG = + '' + + '' + + '' + + '' + + ''; + /** * MCP Server Info — kept on the session because some clients log it. The * version tracks the real package version (was a hard-coded '0.1.0'). + * + * `title` and `icons` are what a host may show in place of `name`. They are + * newer than the protocol version this server claims, so a host that doesn't + * know them passes over them. The icon has no `sizes`: it is optional and says + * nothing about an SVG, and a host on an SDK that typed it as a string, as 1.18 + * did, would fail the whole handshake on the array the spec settled on. */ // Exported so the proxy can answer `initialize` locally with the IDENTICAL // payload the daemon would send — no drift between the two handshake paths. export const SERVER_INFO = { name: 'codegraph', + title: 'CodeGraph', version: CodeGraphPackageVersion, + icons: [ + { + src: `data:image/svg+xml;base64,${Buffer.from(SERVER_ICON_SVG).toString('base64')}`, + mimeType: 'image/svg+xml', + }, + ], }; /**