Files
mcpctl/src/cli/tests/config/claude-mcp.test.ts

111 lines
4.7 KiB
TypeScript
Raw Normal View History

import { describe, it, expect } from 'vitest';
import {
MCPCTL_SERVER_NAME,
mergeMcpctlServers,
projectOfEntry,
isLegacyMcpctlEntry,
activeProjectIn,
} from '../../src/config/claude-mcp.js';
const bridge = (project: string): Record<string, unknown> => ({
command: 'mcpctl',
args: ['mcp', '-p', project],
});
describe('projectOfEntry', () => {
it('reads the project out of the bridge args', () => {
expect(projectOfEntry(bridge('docmost'))).toBe('docmost');
expect(projectOfEntry({ command: 'mcpctl', args: ['mcp', '--project', 'sre'] })).toBe('sre');
});
it('ignores anything that is not our stdio bridge', () => {
expect(projectOfEntry({ command: 'echo', args: ['mcp', '-p', 'x'] })).toBeNull();
expect(projectOfEntry({ command: 'mcpctl', args: ['console', '--stdin-mcp'] })).toBeNull();
expect(projectOfEntry({ type: 'remote', url: 'https://x/projects/y/mcp' })).toBeNull();
expect(projectOfEntry({ command: 'mcpctl', args: ['mcp', '-p'] })).toBeNull();
expect(projectOfEntry(null)).toBeNull();
expect(projectOfEntry('nope')).toBeNull();
});
});
describe('isLegacyMcpctlEntry', () => {
it('recognises an entry named after the very project it bridges to', () => {
expect(isLegacyMcpctlEntry('docmost', bridge('docmost'))).toBe(true);
});
it('never claims the canonical entry', () => {
expect(isLegacyMcpctlEntry(MCPCTL_SERVER_NAME, bridge('docmost'))).toBe(false);
});
it('leaves a hand-configured server alone', () => {
// Same name, different command — someone else's server.
expect(isLegacyMcpctlEntry('docmost', { command: 'docker', args: ['run', 'docmost'] })).toBe(false);
// Our command, but the name does not match the project: not something this
// CLI ever wrote, so it is the user's to keep.
expect(isLegacyMcpctlEntry('my-shortcut', bridge('docmost'))).toBe(false);
});
});
describe('activeProjectIn', () => {
it('prefers the canonical entry', () => {
expect(activeProjectIn({ mcpServers: { [MCPCTL_SERVER_NAME]: bridge('sre') } })).toBe('sre');
});
it('falls back to a legacy entry so pre-migration installs still report', () => {
expect(activeProjectIn({ mcpServers: { docmost: bridge('docmost') } })).toBe('docmost');
});
it('is null when nothing of ours is mounted', () => {
expect(activeProjectIn({ mcpServers: { other: { command: 'echo' } } })).toBeNull();
expect(activeProjectIn(null)).toBeNull();
expect(activeProjectIn({ mcpServers: {} })).toBeNull();
});
});
describe('mergeMcpctlServers', () => {
it('writes one constant entry regardless of project', () => {
const { config } = mergeMcpctlServers(null, { project: 'my-fancy-project' });
expect(Object.keys(config.mcpServers)).toEqual([MCPCTL_SERVER_NAME]);
expect(config.mcpServers[MCPCTL_SERVER_NAME]).toEqual(bridge('my-fancy-project'));
});
it('re-points rather than stacking on a second project', () => {
const first = mergeMcpctlServers(null, { project: 'a' }).config;
const { config } = mergeMcpctlServers(first, { project: 'b' });
expect(Object.keys(config.mcpServers)).toEqual([MCPCTL_SERVER_NAME]);
expect(config.mcpServers[MCPCTL_SERVER_NAME]).toEqual(bridge('b'));
});
it('retires legacy per-project entries and reports them', () => {
const existing = { mcpServers: { homeautomation: bridge('homeautomation'), sre: bridge('sre') } };
const { config, retired } = mergeMcpctlServers(existing, { project: 'docmost' });
expect(Object.keys(config.mcpServers)).toEqual([MCPCTL_SERVER_NAME]);
expect(retired.sort()).toEqual(['homeautomation', 'sre']);
});
it('preserves servers the user configured, and other top-level keys', () => {
const existing = {
mcpServers: { 'my-own': { command: 'echo', args: [] } },
someOtherKey: { keep: true },
};
const { config, retired } = mergeMcpctlServers(existing, { project: 'p' });
expect(config.mcpServers['my-own']).toEqual({ command: 'echo', args: [] });
expect(config['someOtherKey']).toEqual({ keep: true });
expect(retired).toEqual([]);
});
it('--inspect alone does not unmount the project you are working in', () => {
const existing = mergeMcpctlServers(null, { project: 'p' }).config;
const { config, retired } = mergeMcpctlServers(existing, { inspect: true });
expect(config.mcpServers[MCPCTL_SERVER_NAME]).toEqual(bridge('p'));
expect(config.mcpServers['mcpctl-inspect']).toEqual({ command: 'mcpctl', args: ['console', '--stdin-mcp'] });
expect(retired).toEqual([]);
});
it('does not mutate the config it was handed', () => {
const existing = { mcpServers: { homeautomation: bridge('homeautomation') } };
mergeMcpctlServers(existing, { project: 'docmost' });
expect(Object.keys(existing.mcpServers)).toEqual(['homeautomation']);
});
});