111 lines
4.7 KiB
TypeScript
111 lines
4.7 KiB
TypeScript
|
|
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']);
|
||
|
|
});
|
||
|
|
});
|