mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 16:52:09 +08:00
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
22cf7d3aa1
commit
f4c4dae91c
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Record<string, unknown>> }): 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(/^<svg\b/);
|
||||
// `sizes` is left off on purpose: see SERVER_INFO.
|
||||
expect(Object.keys(icon).sort()).toEqual(['mimeType', 'src']);
|
||||
}
|
||||
|
||||
function spawnServer(cwd: string): ChildProcessWithoutNullStreams {
|
||||
return spawn(process.execPath, [...WASM_RUNTIME_FLAGS, BIN, 'serve', '--mcp'], {
|
||||
cwd,
|
||||
@@ -121,6 +135,21 @@ async function removeWhenReleased(dir: string, timeoutMs = 5000): Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
['<circle', '<circle', '<circle', '<line', '<line', '<line', '<rect', '<svg'],
|
||||
);
|
||||
expect(svg).not.toMatch(/href|url\(|on\w+=|style/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP initialize handshake (issue #172)', () => {
|
||||
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 () => {
|
||||
|
||||
@@ -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 =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="#16150f" stroke-width="2">' +
|
||||
'<rect width="32" height="32" rx="6" fill="#f7f6f2" stroke="none"/>' +
|
||||
'<line x1="16" y1="8" x2="8" y2="23"/><line x1="16" y1="8" x2="24" y2="23"/><line x1="8" y1="23" x2="24" y2="23"/>' +
|
||||
'<circle cx="16" cy="8" r="3.4" fill="#16150f"/><circle cx="8" cy="23" r="3.4" fill="#f7f6f2"/><circle cx="24" cy="23" r="3.4" fill="#f7f6f2"/>' +
|
||||
'</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',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user