mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-19 03:51:31 +00:00
Remove legacy backend routes that no longer have frontend or internal callers, including the old Claude/Codex MCP APIs, unused Cursor and Codex helper endpoints, stale TaskMaster detection/next/initialize routes, and unused command/project helpers. This reduces duplicated MCP behavior now handled by the provider-based MCP API, shrinks the exposed backend surface, and removes probe/service code that only existed for deleted endpoints. Add an MCP settings API audit document to capture the route-usage analysis and explain why the legacy MCP endpoints were considered safe to remove.
294 lines
11 KiB
TypeScript
294 lines
11 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
import TOML from '@iarna/toml';
|
|
|
|
import { providerMcpService } from '@/modules/providers/services/mcp.service.js';
|
|
import { AppError } from '@/shared/utils.js';
|
|
|
|
const patchHomeDir = (nextHomeDir: string) => {
|
|
const original = os.homedir;
|
|
(os as any).homedir = () => nextHomeDir;
|
|
return () => {
|
|
(os as any).homedir = original;
|
|
};
|
|
};
|
|
|
|
const readJson = async (filePath: string): Promise<Record<string, unknown>> => {
|
|
const content = await fs.readFile(filePath, 'utf8');
|
|
return JSON.parse(content) as Record<string, unknown>;
|
|
};
|
|
|
|
/**
|
|
* This test covers Claude MCP support for all scopes (user/local/project) and all transports (stdio/http/sse),
|
|
* including add, update/list, and remove operations.
|
|
*/
|
|
test('providerMcpService handles claude MCP scopes/transports with file-backed persistence', { concurrency: false }, async () => {
|
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-claude-'));
|
|
const workspacePath = path.join(tempRoot, 'workspace');
|
|
await fs.mkdir(workspacePath, { recursive: true });
|
|
|
|
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
try {
|
|
await providerMcpService.upsertProviderMcpServer('claude', {
|
|
name: 'claude-user-stdio',
|
|
scope: 'user',
|
|
transport: 'stdio',
|
|
command: 'npx',
|
|
args: ['-y', 'my-server'],
|
|
env: { API_KEY: 'secret' },
|
|
});
|
|
|
|
await providerMcpService.upsertProviderMcpServer('claude', {
|
|
name: 'claude-local-http',
|
|
scope: 'local',
|
|
transport: 'http',
|
|
url: 'https://example.com/mcp',
|
|
headers: { Authorization: 'Bearer token' },
|
|
workspacePath,
|
|
});
|
|
|
|
await providerMcpService.upsertProviderMcpServer('claude', {
|
|
name: 'claude-project-sse',
|
|
scope: 'project',
|
|
transport: 'sse',
|
|
url: 'https://example.com/sse',
|
|
headers: { 'X-API-Key': 'abc' },
|
|
workspacePath,
|
|
});
|
|
|
|
const grouped = await providerMcpService.listProviderMcpServers('claude', { workspacePath });
|
|
assert.ok(grouped.user.some((server) => server.name === 'claude-user-stdio' && server.transport === 'stdio'));
|
|
assert.ok(grouped.local.some((server) => server.name === 'claude-local-http' && server.transport === 'http'));
|
|
assert.ok(grouped.project.some((server) => server.name === 'claude-project-sse' && server.transport === 'sse'));
|
|
|
|
// update behavior is the same upsert route with same name
|
|
await providerMcpService.upsertProviderMcpServer('claude', {
|
|
name: 'claude-project-sse',
|
|
scope: 'project',
|
|
transport: 'sse',
|
|
url: 'https://example.com/sse-updated',
|
|
headers: { 'X-API-Key': 'updated' },
|
|
workspacePath,
|
|
});
|
|
|
|
const projectConfig = await readJson(path.join(workspacePath, '.mcp.json'));
|
|
const projectServers = projectConfig.mcpServers as Record<string, unknown>;
|
|
const projectServer = projectServers['claude-project-sse'] as Record<string, unknown>;
|
|
assert.equal(projectServer.url, 'https://example.com/sse-updated');
|
|
|
|
const removeResult = await providerMcpService.removeProviderMcpServer('claude', {
|
|
name: 'claude-local-http',
|
|
scope: 'local',
|
|
workspacePath,
|
|
});
|
|
assert.equal(removeResult.removed, true);
|
|
} finally {
|
|
restoreHomeDir();
|
|
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* This test covers Codex MCP support for user/project scopes, stdio/http formats,
|
|
* and validation for unsupported scope/transport combinations.
|
|
*/
|
|
test('providerMcpService handles codex MCP TOML config and capability validation', { concurrency: false }, async () => {
|
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-codex-'));
|
|
const workspacePath = path.join(tempRoot, 'workspace');
|
|
await fs.mkdir(workspacePath, { recursive: true });
|
|
|
|
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
try {
|
|
await providerMcpService.upsertProviderMcpServer('codex', {
|
|
name: 'codex-user-stdio',
|
|
scope: 'user',
|
|
transport: 'stdio',
|
|
command: 'python',
|
|
args: ['server.py'],
|
|
env: { API_KEY: 'x' },
|
|
envVars: ['API_KEY'],
|
|
cwd: '/tmp',
|
|
});
|
|
|
|
await providerMcpService.upsertProviderMcpServer('codex', {
|
|
name: 'codex-project-http',
|
|
scope: 'project',
|
|
transport: 'http',
|
|
url: 'https://codex.example.com/mcp',
|
|
headers: { 'X-Custom-Header': 'value' },
|
|
envHttpHeaders: { 'X-API-Key': 'MY_API_KEY_ENV' },
|
|
bearerTokenEnvVar: 'MY_API_TOKEN',
|
|
workspacePath,
|
|
});
|
|
|
|
const userTomlPath = path.join(tempRoot, '.codex', 'config.toml');
|
|
const userConfig = TOML.parse(await fs.readFile(userTomlPath, 'utf8')) as Record<string, unknown>;
|
|
const userServers = userConfig.mcp_servers as Record<string, unknown>;
|
|
const userStdio = userServers['codex-user-stdio'] as Record<string, unknown>;
|
|
assert.equal(userStdio.command, 'python');
|
|
|
|
const projectTomlPath = path.join(workspacePath, '.codex', 'config.toml');
|
|
const projectConfig = TOML.parse(await fs.readFile(projectTomlPath, 'utf8')) as Record<string, unknown>;
|
|
const projectServers = projectConfig.mcp_servers as Record<string, unknown>;
|
|
const projectHttp = projectServers['codex-project-http'] as Record<string, unknown>;
|
|
assert.equal(projectHttp.url, 'https://codex.example.com/mcp');
|
|
|
|
await assert.rejects(
|
|
providerMcpService.upsertProviderMcpServer('codex', {
|
|
name: 'codex-local',
|
|
scope: 'local',
|
|
transport: 'stdio',
|
|
command: 'node',
|
|
}),
|
|
(error: unknown) =>
|
|
error instanceof AppError &&
|
|
error.code === 'MCP_SCOPE_NOT_SUPPORTED' &&
|
|
error.statusCode === 400,
|
|
);
|
|
|
|
await assert.rejects(
|
|
providerMcpService.upsertProviderMcpServer('codex', {
|
|
name: 'codex-sse',
|
|
scope: 'project',
|
|
transport: 'sse',
|
|
url: 'https://example.com/sse',
|
|
workspacePath,
|
|
}),
|
|
(error: unknown) =>
|
|
error instanceof AppError &&
|
|
error.code === 'MCP_TRANSPORT_NOT_SUPPORTED' &&
|
|
error.statusCode === 400,
|
|
);
|
|
} finally {
|
|
restoreHomeDir();
|
|
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* This test covers Gemini/Cursor MCP JSON formats and user/project scope persistence.
|
|
*/
|
|
test('providerMcpService handles gemini and cursor MCP JSON config formats', { concurrency: false }, async () => {
|
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-gc-'));
|
|
const workspacePath = path.join(tempRoot, 'workspace');
|
|
await fs.mkdir(workspacePath, { recursive: true });
|
|
|
|
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
try {
|
|
await providerMcpService.upsertProviderMcpServer('gemini', {
|
|
name: 'gemini-stdio',
|
|
scope: 'user',
|
|
transport: 'stdio',
|
|
command: 'node',
|
|
args: ['server.js'],
|
|
env: { TOKEN: '$TOKEN' },
|
|
cwd: './server',
|
|
});
|
|
|
|
await providerMcpService.upsertProviderMcpServer('gemini', {
|
|
name: 'gemini-http',
|
|
scope: 'project',
|
|
transport: 'http',
|
|
url: 'https://gemini.example.com/mcp',
|
|
headers: { Authorization: 'Bearer token' },
|
|
workspacePath,
|
|
});
|
|
|
|
await providerMcpService.upsertProviderMcpServer('cursor', {
|
|
name: 'cursor-stdio',
|
|
scope: 'project',
|
|
transport: 'stdio',
|
|
command: 'npx',
|
|
args: ['-y', 'mcp-server'],
|
|
env: { API_KEY: 'value' },
|
|
workspacePath,
|
|
});
|
|
|
|
await providerMcpService.upsertProviderMcpServer('cursor', {
|
|
name: 'cursor-http',
|
|
scope: 'user',
|
|
transport: 'http',
|
|
url: 'http://localhost:3333/mcp',
|
|
headers: { API_KEY: 'value' },
|
|
});
|
|
|
|
const geminiUserConfig = await readJson(path.join(tempRoot, '.gemini', 'settings.json'));
|
|
const geminiUserServer = (geminiUserConfig.mcpServers as Record<string, unknown>)['gemini-stdio'] as Record<string, unknown>;
|
|
assert.equal(geminiUserServer.command, 'node');
|
|
assert.equal(geminiUserServer.type, undefined);
|
|
|
|
const geminiProjectConfig = await readJson(path.join(workspacePath, '.gemini', 'settings.json'));
|
|
const geminiProjectServer = (geminiProjectConfig.mcpServers as Record<string, unknown>)['gemini-http'] as Record<string, unknown>;
|
|
assert.equal(geminiProjectServer.type, 'http');
|
|
|
|
const cursorUserConfig = await readJson(path.join(tempRoot, '.cursor', 'mcp.json'));
|
|
const cursorHttpServer = (cursorUserConfig.mcpServers as Record<string, unknown>)['cursor-http'] as Record<string, unknown>;
|
|
assert.equal(cursorHttpServer.url, 'http://localhost:3333/mcp');
|
|
assert.equal(cursorHttpServer.type, undefined);
|
|
} finally {
|
|
restoreHomeDir();
|
|
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* This test covers the global MCP adder requirement: only http/stdio are allowed and
|
|
* one payload is written to all providers.
|
|
*/
|
|
test('providerMcpService global adder writes to all providers and rejects unsupported transports', { concurrency: false }, async () => {
|
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-global-'));
|
|
const workspacePath = path.join(tempRoot, 'workspace');
|
|
await fs.mkdir(workspacePath, { recursive: true });
|
|
|
|
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
try {
|
|
const globalResult = await providerMcpService.addMcpServerToAllProviders({
|
|
name: 'global-http',
|
|
scope: 'project',
|
|
transport: 'http',
|
|
url: 'https://global.example.com/mcp',
|
|
workspacePath,
|
|
});
|
|
|
|
const expectCursorGlobal = process.platform !== 'win32';
|
|
assert.equal(globalResult.length, expectCursorGlobal ? 4 : 3);
|
|
assert.ok(globalResult.every((entry) => entry.created === true));
|
|
|
|
const claudeProject = await readJson(path.join(workspacePath, '.mcp.json'));
|
|
assert.ok((claudeProject.mcpServers as Record<string, unknown>)['global-http']);
|
|
|
|
const codexProject = TOML.parse(await fs.readFile(path.join(workspacePath, '.codex', 'config.toml'), 'utf8')) as Record<string, unknown>;
|
|
assert.ok((codexProject.mcp_servers as Record<string, unknown>)['global-http']);
|
|
|
|
const geminiProject = await readJson(path.join(workspacePath, '.gemini', 'settings.json'));
|
|
assert.ok((geminiProject.mcpServers as Record<string, unknown>)['global-http']);
|
|
|
|
if (expectCursorGlobal) {
|
|
const cursorProject = await readJson(path.join(workspacePath, '.cursor', 'mcp.json'));
|
|
assert.ok((cursorProject.mcpServers as Record<string, unknown>)['global-http']);
|
|
}
|
|
|
|
await assert.rejects(
|
|
providerMcpService.addMcpServerToAllProviders({
|
|
name: 'global-sse',
|
|
scope: 'project',
|
|
transport: 'sse',
|
|
url: 'https://example.com/sse',
|
|
workspacePath,
|
|
}),
|
|
(error: unknown) =>
|
|
error instanceof AppError &&
|
|
error.code === 'INVALID_GLOBAL_MCP_TRANSPORT' &&
|
|
error.statusCode === 400,
|
|
);
|
|
} finally {
|
|
restoreHomeDir();
|
|
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|