From ff495d56b5e2a79e8f79dc606d93ff917ee1012d Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 18 Jun 2026 16:33:50 -0400 Subject: [PATCH] [MCP] Add effective server name header to MDM config --- README.md | 6 ++- skills/glean-mdm/SKILL.md | 2 +- src/config-writer.spec.ts | 47 ++++++++++++++++++--- src/config-writer.ts | 18 +++++++- src/config.spec.ts | 25 +++++++++++ src/config.ts | 5 +++ src/hosts/index.spec.ts | 87 +++++++++++++++++++++++++++++++++++++++ src/hosts/index.ts | 7 +++- 8 files changed, 186 insertions(+), 11 deletions(-) create mode 100644 src/hosts/index.spec.ts diff --git a/README.md b/README.md index 87ec1f8..0d8ae3b 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,11 @@ Or specify custom paths with `--mcp-config` and `--mdm-config`. [ { "serverName": "glean_default", - "url": "https://your-company-be.glean.com/mcp/default" + "url": "https://your-company-be.glean.com/mcp/default", + "headers": { + "X-Glean-Metadata": "mdm", + "X-Glean-MCP-Server-Name": "extension-glean_default" + } } ] ``` diff --git a/skills/glean-mdm/SKILL.md b/skills/glean-mdm/SKILL.md index bdb99bf..cfd7eee 100644 --- a/skills/glean-mdm/SKILL.md +++ b/skills/glean-mdm/SKILL.md @@ -36,7 +36,7 @@ Don't transcribe the flag or schema lists — they drift. Check `--help` and the The normal admin workflow is **config → install-schedule → run**: - **`config`** generates two files into the platform default directory (override with `--output-dir`): - - `mcp-config.json` — the MCP server(s) to provision (`serverName`, `url`). + - `mcp-config.json` — the MCP server(s) to provision (`serverName`, `url`, optional per-server `headers`). - `mdm-config.json` — the binary's own update behavior (`autoUpdate`, `versionUrl`, `binaryUrlPrefix`, `pinnedVersion`). - **`install-schedule`** registers the system runner (launchd / systemd / Task Scheduler); `uninstall-schedule` removes it; `uninstall` removes everything (schedule, config, logs, binary). - **`run`** does the per-user work: for each local user it installs the Glean editor extension, configures the MCP server entry in each supported host tool, then checks for a self-update. Run it as root/admin so it can enumerate all users and write their configs. diff --git a/src/config-writer.spec.ts b/src/config-writer.spec.ts index 1c5c1ff..1f94f94 100644 --- a/src/config-writer.spec.ts +++ b/src/config-writer.spec.ts @@ -23,7 +23,16 @@ describe('writeConfig', () => { }) const mcp = JSON.parse(readFileSync(join(outputDir, 'mcp-config.json'), 'utf-8')) - expect(mcp).toEqual([{ serverName: 'glean_default', url: 'https://example.com/mcp/default' }]) + expect(mcp).toEqual([ + { + serverName: 'glean_default', + url: 'https://example.com/mcp/default', + headers: { + 'X-Glean-Metadata': 'mdm', + 'X-Glean-MCP-Server-Name': 'extension-glean_default', + }, + }, + ]) const mdm = JSON.parse(readFileSync(join(outputDir, 'mdm-config.json'), 'utf-8')) expect(mdm).toEqual({ @@ -166,8 +175,22 @@ describe('writeConfig', () => { const mcp = JSON.parse(readFileSync(join(outputDir, 'mcp-config.json'), 'utf-8')) expect(mcp).toHaveLength(2) - expect(mcp[0]).toEqual({ serverName: 'server_a', url: 'https://a.example.com/mcp/default' }) - expect(mcp[1]).toEqual({ serverName: 'server_b', url: 'https://b.example.com/mcp/default' }) + expect(mcp[0]).toEqual({ + serverName: 'server_a', + url: 'https://a.example.com/mcp/default', + headers: { + 'X-Glean-Metadata': 'mdm', + 'X-Glean-MCP-Server-Name': 'extension-server_a', + }, + }) + expect(mcp[1]).toEqual({ + serverName: 'server_b', + url: 'https://b.example.com/mcp/default', + headers: { + 'X-Glean-Metadata': 'mdm', + 'X-Glean-MCP-Server-Name': 'extension-server_b', + }, + }) }) it('skips mcp-config.json entry when same serverName already exists', () => { @@ -210,7 +233,14 @@ describe('writeConfig', () => { const mcp = JSON.parse(readFileSync(mcpPath, 'utf-8')) expect(mcp).toHaveLength(2) expect(mcp[0]).toEqual({ serverName: 'legacy_server', url: 'https://legacy.example.com/mcp/default' }) - expect(mcp[1]).toEqual({ serverName: 'new_server', url: 'https://new.example.com/mcp/default' }) + expect(mcp[1]).toEqual({ + serverName: 'new_server', + url: 'https://new.example.com/mcp/default', + headers: { + 'X-Glean-Metadata': 'mdm', + 'X-Glean-MCP-Server-Name': 'extension-new_server', + }, + }) }) it('skips when serverName exists in a single-object format file', () => { @@ -286,7 +316,14 @@ describe('writeConfig', () => { const mcp = JSON.parse(readFileSync(mcpTarget, 'utf-8')) expect(mcp).toEqual([ { serverName: 'existing', url: 'https://existing.com/mcp' }, - { serverName: 'glean_default', url: 'https://example.com/mcp/default' }, + { + serverName: 'glean_default', + url: 'https://example.com/mcp/default', + headers: { + 'X-Glean-Metadata': 'mdm', + 'X-Glean-MCP-Server-Name': 'extension-glean_default', + }, + }, ]) const mdm = JSON.parse(readFileSync(mdmTarget, 'utf-8')) diff --git a/src/config-writer.ts b/src/config-writer.ts index bcc65e8..35c96c4 100644 --- a/src/config-writer.ts +++ b/src/config-writer.ts @@ -1,7 +1,14 @@ import { mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs' import { join } from 'node:path' -import { McpConfigSchema, type McpServerEntry, MdmConfigSchema } from './config.js' +import { + gleanMcpServerNameHeaderName, + gleanMdmMetadataHeaderName, + gleanMdmMetadataHeaderValue, + McpConfigSchema, + type McpServerEntry, + MdmConfigSchema, +} from './config.js' import { log } from './logger.js' import { getDefaultConfigDir } from './platform.js' @@ -41,7 +48,14 @@ export function writeConfig(options: WriteConfigOptions): void { const outputDir = options.outputDir ?? getDefaultConfigDir() mkdirSync(outputDir, { recursive: true }) - const newEntry = { serverName: options.serverName, url: options.serverUrl } + const newEntry = { + headers: { + [gleanMdmMetadataHeaderName]: gleanMdmMetadataHeaderValue, + [gleanMcpServerNameHeaderName]: `extension-${options.serverName}`, + }, + serverName: options.serverName, + url: options.serverUrl, + } McpConfigSchema.parse([newEntry]) const mdmData: Record = { diff --git a/src/config.spec.ts b/src/config.spec.ts index 6c55128..c49c653 100644 --- a/src/config.spec.ts +++ b/src/config.spec.ts @@ -54,6 +54,31 @@ describe('McpConfigSchema', () => { expect(result.success).toBe(true) }) + + it('preserves configured HTTP headers', () => { + const result = McpConfigSchema.safeParse({ + serverName: 'glean_default', + url: 'https://example.com/mcp/default', + headers: { + 'X-Glean-Metadata': 'mdm', + 'X-Glean-MCP-Server-Name': 'extension-glean_default', + }, + }) + + expect(result.success).toBe(true) + if (result.success) { + expect(result.data).toEqual([ + { + serverName: 'glean_default', + url: 'https://example.com/mcp/default', + headers: { + 'X-Glean-Metadata': 'mdm', + 'X-Glean-MCP-Server-Name': 'extension-glean_default', + }, + }, + ]) + } + }) }) describe('MdmConfigSchema', () => { diff --git a/src/config.ts b/src/config.ts index 156ef05..9bb51e8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,7 +6,12 @@ import { getDefaultMcpConfigPath, getDefaultMdmConfigPath } from './platform.js' const SEMVER_PATTERN = /^v?\d+\.\d+\.\d+$/ +export const gleanMdmMetadataHeaderName = 'X-Glean-Metadata' +export const gleanMdmMetadataHeaderValue = 'mdm' +export const gleanMcpServerNameHeaderName = 'X-Glean-MCP-Server-Name' + const McpServerEntrySchema = z.object({ + headers: z.record(z.string()).optional(), serverName: z.string().min(1), url: z.string().min(1), }) diff --git a/src/hosts/index.spec.ts b/src/hosts/index.spec.ts new file mode 100644 index 0000000..fab8caa --- /dev/null +++ b/src/hosts/index.spec.ts @@ -0,0 +1,87 @@ +import { mkdtempSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + buildConfiguration: vi.fn((config) => ({ + mcpServers: { + [config.serverName]: config, + }, + })), + configureJsonFile: vi.fn(), +})) + +vi.mock('@gleanwork/mcp-config-glean', () => ({ + createGleanRegistry: () => ({ + createBuilder: () => ({ buildConfiguration: mocks.buildConfiguration }), + getAllConfigs: () => [], + getClientsByPlatform: () => [ + { + configFormat: 'json', + configPath: { + darwin: '$HOME/.cursor/mcp.json', + linux: '$HOME/.cursor/mcp.json', + win32: '%APPDATA%\\Cursor\\mcp.json', + }, + displayName: 'Cursor', + id: 'cursor', + transports: ['http'], + userConfigurable: true, + }, + ], + }), +})) + +vi.mock('./json-configurator.js', () => ({ + configureJsonFile: mocks.configureJsonFile, +})) + +vi.mock('./toml-configurator.js', () => ({ + configureTomlFile: vi.fn(), +})) + +vi.mock('./yaml-configurator.js', () => ({ + configureYamlFile: vi.fn(), +})) + +import { configureHosts } from './index.js' + +describe('configureHosts', () => { + beforeEach(() => { + mocks.buildConfiguration.mockClear() + mocks.configureJsonFile.mockClear() + }) + + it('passes configured server headers to generated host configs', () => { + const userHomeDir = mkdtempSync(join(tmpdir(), 'glean-mdm-home-')) + + const results = configureHosts({ + servers: [ + { + headers: { + 'X-Glean-MCP-Server-Name': 'extension-glean_default', + 'X-Glean-Metadata': 'custom', + }, + serverName: 'glean_default', + url: 'https://example.com/mcp/default', + }, + ], + userHomeDir, + username: 'test-user', + }) + + expect(results).toEqual([{ host: 'Cursor', success: true }]) + expect(mocks.buildConfiguration).toHaveBeenCalledWith({ + headers: { + 'X-Glean-Metadata': 'custom', + 'X-Glean-MCP-Server-Name': 'extension-glean_default', + }, + includeRootObject: true, + serverName: 'glean_default', + serverUrl: 'https://example.com/mcp/default', + transport: 'http', + }) + }) +}) diff --git a/src/hosts/index.ts b/src/hosts/index.ts index dd29036..eb49c59 100644 --- a/src/hosts/index.ts +++ b/src/hosts/index.ts @@ -4,7 +4,7 @@ import { dirname, resolve } from 'node:path' import { createGleanRegistry } from '@gleanwork/mcp-config-glean' -import { getServerUrl } from '../config.js' +import { getServerUrl, gleanMdmMetadataHeaderName, gleanMdmMetadataHeaderValue } from '../config.js' import type { McpServerEntry } from '../config.js' import { log } from '../logger.js' import { getPlatform } from '../platform.js' @@ -138,7 +138,10 @@ export function configureHosts(options: ConfigureHostsOptions): ConfigureResult[ for (const server of servers) { const serverUrl = getServerUrl(server) const generatedConfig = builder.buildConfiguration({ - headers: { 'X-Glean-Metadata': 'mdm' }, + headers: { + [gleanMdmMetadataHeaderName]: gleanMdmMetadataHeaderValue, + ...server.headers, + }, includeRootObject: true, serverName: server.serverName, serverUrl,