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', + }, + ], }; /**