@cloudcli-ai/cloudcli 1.29.5 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +1 -1
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.tr.md +252 -0
- package/README.zh-CN.md +1 -1
- package/dist/assets/index-DI22MZMb.css +32 -0
- package/dist/assets/index-RX-W_g_Q.js +1262 -0
- package/dist/index.html +2 -2
- package/dist-server/server/claude-sdk.js +13 -8
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cursor-cli.js +7 -7
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/gemini-cli.js +5 -5
- package/dist-server/server/gemini-cli.js.map +1 -1
- package/dist-server/server/gemini-response-handler.js +2 -2
- package/dist-server/server/gemini-response-handler.js.map +1 -1
- package/dist-server/server/index.js +27 -25
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +103 -0
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude-mcp.provider.js +103 -0
- package/dist-server/server/modules/providers/list/claude/claude-mcp.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +266 -0
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js +13 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js +84 -0
- package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-mcp.provider.js +107 -0
- package/dist-server/server/modules/providers/list/codex/codex-mcp.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +282 -0
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +13 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +118 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-mcp.provider.js +80 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-mcp.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +369 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +13 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +131 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js +82 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +207 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +13 -0
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -0
- package/dist-server/server/modules/providers/provider.registry.js +31 -0
- package/dist-server/server/modules/providers/provider.registry.js.map +1 -0
- package/dist-server/server/modules/providers/provider.routes.js +159 -0
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -0
- package/dist-server/server/modules/providers/services/mcp.service.js +69 -0
- package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/provider-auth.service.js +25 -0
- package/dist-server/server/modules/providers/services/provider-auth.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/sessions.service.js +29 -0
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -0
- package/dist-server/server/modules/providers/shared/base/abstract.provider.js +14 -0
- package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -0
- package/dist-server/server/modules/providers/shared/mcp/mcp.provider.js +102 -0
- package/dist-server/server/modules/providers/shared/mcp/mcp.provider.js.map +1 -0
- package/dist-server/server/modules/providers/tests/mcp.test.js +250 -0
- package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -0
- package/dist-server/server/openai-codex.js +5 -5
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/routes/codex.js +2 -278
- package/dist-server/server/routes/codex.js.map +1 -1
- package/dist-server/server/routes/commands.js +0 -44
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/routes/cursor.js +9 -489
- package/dist-server/server/routes/cursor.js.map +1 -1
- package/dist-server/server/routes/mcp-utils.js +1 -18
- package/dist-server/server/routes/mcp-utils.js.map +1 -1
- package/dist-server/server/routes/messages.js +6 -6
- package/dist-server/server/routes/messages.js.map +1 -1
- package/dist-server/server/routes/settings.js +10 -0
- package/dist-server/server/routes/settings.js.map +1 -1
- package/dist-server/server/routes/taskmaster.js +0 -459
- package/dist-server/server/routes/taskmaster.js.map +1 -1
- package/dist-server/server/shared/interfaces.js +2 -0
- package/dist-server/server/shared/interfaces.js.map +1 -0
- package/dist-server/server/shared/types.js +3 -0
- package/dist-server/server/shared/types.js.map +1 -0
- package/dist-server/server/shared/utils.js +150 -0
- package/dist-server/server/shared/utils.js.map +1 -0
- package/dist-server/server/utils/mcp-detector.js +0 -49
- package/dist-server/server/utils/mcp-detector.js.map +1 -1
- package/dist-server/shared/modelConstants.js +1 -0
- package/dist-server/shared/modelConstants.js.map +1 -1
- package/package.json +5 -2
- package/server/claude-sdk.js +14 -8
- package/server/cursor-cli.js +7 -7
- package/server/gemini-cli.js +5 -5
- package/server/gemini-response-handler.js +2 -2
- package/server/index.js +33 -29
- package/server/modules/providers/list/claude/claude-auth.provider.ts +123 -0
- package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -0
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -0
- package/server/modules/providers/list/claude/claude.provider.ts +15 -0
- package/server/modules/providers/list/codex/codex-auth.provider.ts +100 -0
- package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -0
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -0
- package/server/modules/providers/list/codex/codex.provider.ts +15 -0
- package/server/modules/providers/list/cursor/cursor-auth.provider.ts +143 -0
- package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -0
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -0
- package/server/modules/providers/list/cursor/cursor.provider.ts +15 -0
- package/server/modules/providers/list/gemini/gemini-auth.provider.ts +151 -0
- package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -0
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -0
- package/server/modules/providers/list/gemini/gemini.provider.ts +15 -0
- package/server/modules/providers/provider.registry.ts +36 -0
- package/server/modules/providers/provider.routes.ts +217 -0
- package/server/modules/providers/services/mcp.service.ts +94 -0
- package/server/modules/providers/services/provider-auth.service.ts +26 -0
- package/server/modules/providers/services/sessions.service.ts +45 -0
- package/server/modules/providers/shared/base/abstract.provider.ts +20 -0
- package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -0
- package/server/modules/providers/tests/mcp.test.ts +293 -0
- package/server/openai-codex.js +5 -5
- package/server/routes/codex.js +2 -312
- package/server/routes/commands.js +0 -49
- package/server/routes/cursor.js +15 -527
- package/server/routes/mcp-utils.js +2 -19
- package/server/routes/messages.js +6 -6
- package/server/routes/settings.js +10 -0
- package/server/routes/taskmaster.js +0 -492
- package/server/shared/interfaces.ts +54 -0
- package/server/shared/types.ts +172 -0
- package/server/shared/utils.ts +193 -0
- package/server/tsconfig.json +6 -3
- package/server/utils/mcp-detector.js +0 -51
- package/shared/modelConstants.js +1 -0
- package/dist/assets/index-BBAE1OJ_.css +0 -32
- package/dist/assets/index-Ci1GOyV7.js +0 -1262
- package/dist-server/server/providers/claude/adapter.js +0 -266
- package/dist-server/server/providers/claude/adapter.js.map +0 -1
- package/dist-server/server/providers/claude/status.js +0 -121
- package/dist-server/server/providers/claude/status.js.map +0 -1
- package/dist-server/server/providers/codex/adapter.js +0 -232
- package/dist-server/server/providers/codex/adapter.js.map +0 -1
- package/dist-server/server/providers/codex/status.js +0 -70
- package/dist-server/server/providers/codex/status.js.map +0 -1
- package/dist-server/server/providers/cursor/adapter.js +0 -342
- package/dist-server/server/providers/cursor/adapter.js.map +0 -1
- package/dist-server/server/providers/cursor/status.js +0 -118
- package/dist-server/server/providers/cursor/status.js.map +0 -1
- package/dist-server/server/providers/gemini/adapter.js +0 -174
- package/dist-server/server/providers/gemini/adapter.js.map +0 -1
- package/dist-server/server/providers/gemini/status.js +0 -104
- package/dist-server/server/providers/gemini/status.js.map +0 -1
- package/dist-server/server/providers/registry.js +0 -58
- package/dist-server/server/providers/registry.js.map +0 -1
- package/dist-server/server/providers/types.js +0 -117
- package/dist-server/server/providers/types.js.map +0 -1
- package/dist-server/server/providers/utils.js +0 -28
- package/dist-server/server/providers/utils.js.map +0 -1
- package/dist-server/server/routes/cli-auth.js +0 -25
- package/dist-server/server/routes/cli-auth.js.map +0 -1
- package/dist-server/server/routes/mcp.js +0 -480
- package/dist-server/server/routes/mcp.js.map +0 -1
- package/server/providers/claude/adapter.js +0 -278
- package/server/providers/claude/status.js +0 -136
- package/server/providers/codex/adapter.js +0 -248
- package/server/providers/codex/status.js +0 -78
- package/server/providers/cursor/adapter.js +0 -348
- package/server/providers/cursor/status.js +0 -128
- package/server/providers/gemini/adapter.js +0 -186
- package/server/providers/gemini/status.js +0 -111
- package/server/providers/registry.js +0 -67
- package/server/providers/types.js +0 -132
- package/server/providers/utils.js +0 -29
- package/server/routes/cli-auth.js +0 -27
- package/server/routes/mcp.js +0 -552
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import TOML from '@iarna/toml';
|
|
8
|
+
|
|
9
|
+
import { providerMcpService } from '@/modules/providers/services/mcp.service.js';
|
|
10
|
+
import { AppError } from '@/shared/utils.js';
|
|
11
|
+
|
|
12
|
+
const patchHomeDir = (nextHomeDir: string) => {
|
|
13
|
+
const original = os.homedir;
|
|
14
|
+
(os as any).homedir = () => nextHomeDir;
|
|
15
|
+
return () => {
|
|
16
|
+
(os as any).homedir = original;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const readJson = async (filePath: string): Promise<Record<string, unknown>> => {
|
|
21
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
22
|
+
return JSON.parse(content) as Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* This test covers Claude MCP support for all scopes (user/local/project) and all transports (stdio/http/sse),
|
|
27
|
+
* including add, update/list, and remove operations.
|
|
28
|
+
*/
|
|
29
|
+
test('providerMcpService handles claude MCP scopes/transports with file-backed persistence', { concurrency: false }, async () => {
|
|
30
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-claude-'));
|
|
31
|
+
const workspacePath = path.join(tempRoot, 'workspace');
|
|
32
|
+
await fs.mkdir(workspacePath, { recursive: true });
|
|
33
|
+
|
|
34
|
+
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
35
|
+
try {
|
|
36
|
+
await providerMcpService.upsertProviderMcpServer('claude', {
|
|
37
|
+
name: 'claude-user-stdio',
|
|
38
|
+
scope: 'user',
|
|
39
|
+
transport: 'stdio',
|
|
40
|
+
command: 'npx',
|
|
41
|
+
args: ['-y', 'my-server'],
|
|
42
|
+
env: { API_KEY: 'secret' },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await providerMcpService.upsertProviderMcpServer('claude', {
|
|
46
|
+
name: 'claude-local-http',
|
|
47
|
+
scope: 'local',
|
|
48
|
+
transport: 'http',
|
|
49
|
+
url: 'https://example.com/mcp',
|
|
50
|
+
headers: { Authorization: 'Bearer token' },
|
|
51
|
+
workspacePath,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await providerMcpService.upsertProviderMcpServer('claude', {
|
|
55
|
+
name: 'claude-project-sse',
|
|
56
|
+
scope: 'project',
|
|
57
|
+
transport: 'sse',
|
|
58
|
+
url: 'https://example.com/sse',
|
|
59
|
+
headers: { 'X-API-Key': 'abc' },
|
|
60
|
+
workspacePath,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const grouped = await providerMcpService.listProviderMcpServers('claude', { workspacePath });
|
|
64
|
+
assert.ok(grouped.user.some((server) => server.name === 'claude-user-stdio' && server.transport === 'stdio'));
|
|
65
|
+
assert.ok(grouped.local.some((server) => server.name === 'claude-local-http' && server.transport === 'http'));
|
|
66
|
+
assert.ok(grouped.project.some((server) => server.name === 'claude-project-sse' && server.transport === 'sse'));
|
|
67
|
+
|
|
68
|
+
// update behavior is the same upsert route with same name
|
|
69
|
+
await providerMcpService.upsertProviderMcpServer('claude', {
|
|
70
|
+
name: 'claude-project-sse',
|
|
71
|
+
scope: 'project',
|
|
72
|
+
transport: 'sse',
|
|
73
|
+
url: 'https://example.com/sse-updated',
|
|
74
|
+
headers: { 'X-API-Key': 'updated' },
|
|
75
|
+
workspacePath,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const projectConfig = await readJson(path.join(workspacePath, '.mcp.json'));
|
|
79
|
+
const projectServers = projectConfig.mcpServers as Record<string, unknown>;
|
|
80
|
+
const projectServer = projectServers['claude-project-sse'] as Record<string, unknown>;
|
|
81
|
+
assert.equal(projectServer.url, 'https://example.com/sse-updated');
|
|
82
|
+
|
|
83
|
+
const removeResult = await providerMcpService.removeProviderMcpServer('claude', {
|
|
84
|
+
name: 'claude-local-http',
|
|
85
|
+
scope: 'local',
|
|
86
|
+
workspacePath,
|
|
87
|
+
});
|
|
88
|
+
assert.equal(removeResult.removed, true);
|
|
89
|
+
} finally {
|
|
90
|
+
restoreHomeDir();
|
|
91
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* This test covers Codex MCP support for user/project scopes, stdio/http formats,
|
|
97
|
+
* and validation for unsupported scope/transport combinations.
|
|
98
|
+
*/
|
|
99
|
+
test('providerMcpService handles codex MCP TOML config and capability validation', { concurrency: false }, async () => {
|
|
100
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-codex-'));
|
|
101
|
+
const workspacePath = path.join(tempRoot, 'workspace');
|
|
102
|
+
await fs.mkdir(workspacePath, { recursive: true });
|
|
103
|
+
|
|
104
|
+
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
105
|
+
try {
|
|
106
|
+
await providerMcpService.upsertProviderMcpServer('codex', {
|
|
107
|
+
name: 'codex-user-stdio',
|
|
108
|
+
scope: 'user',
|
|
109
|
+
transport: 'stdio',
|
|
110
|
+
command: 'python',
|
|
111
|
+
args: ['server.py'],
|
|
112
|
+
env: { API_KEY: 'x' },
|
|
113
|
+
envVars: ['API_KEY'],
|
|
114
|
+
cwd: '/tmp',
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await providerMcpService.upsertProviderMcpServer('codex', {
|
|
118
|
+
name: 'codex-project-http',
|
|
119
|
+
scope: 'project',
|
|
120
|
+
transport: 'http',
|
|
121
|
+
url: 'https://codex.example.com/mcp',
|
|
122
|
+
headers: { 'X-Custom-Header': 'value' },
|
|
123
|
+
envHttpHeaders: { 'X-API-Key': 'MY_API_KEY_ENV' },
|
|
124
|
+
bearerTokenEnvVar: 'MY_API_TOKEN',
|
|
125
|
+
workspacePath,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const userTomlPath = path.join(tempRoot, '.codex', 'config.toml');
|
|
129
|
+
const userConfig = TOML.parse(await fs.readFile(userTomlPath, 'utf8')) as Record<string, unknown>;
|
|
130
|
+
const userServers = userConfig.mcp_servers as Record<string, unknown>;
|
|
131
|
+
const userStdio = userServers['codex-user-stdio'] as Record<string, unknown>;
|
|
132
|
+
assert.equal(userStdio.command, 'python');
|
|
133
|
+
|
|
134
|
+
const projectTomlPath = path.join(workspacePath, '.codex', 'config.toml');
|
|
135
|
+
const projectConfig = TOML.parse(await fs.readFile(projectTomlPath, 'utf8')) as Record<string, unknown>;
|
|
136
|
+
const projectServers = projectConfig.mcp_servers as Record<string, unknown>;
|
|
137
|
+
const projectHttp = projectServers['codex-project-http'] as Record<string, unknown>;
|
|
138
|
+
assert.equal(projectHttp.url, 'https://codex.example.com/mcp');
|
|
139
|
+
|
|
140
|
+
await assert.rejects(
|
|
141
|
+
providerMcpService.upsertProviderMcpServer('codex', {
|
|
142
|
+
name: 'codex-local',
|
|
143
|
+
scope: 'local',
|
|
144
|
+
transport: 'stdio',
|
|
145
|
+
command: 'node',
|
|
146
|
+
}),
|
|
147
|
+
(error: unknown) =>
|
|
148
|
+
error instanceof AppError &&
|
|
149
|
+
error.code === 'MCP_SCOPE_NOT_SUPPORTED' &&
|
|
150
|
+
error.statusCode === 400,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
await assert.rejects(
|
|
154
|
+
providerMcpService.upsertProviderMcpServer('codex', {
|
|
155
|
+
name: 'codex-sse',
|
|
156
|
+
scope: 'project',
|
|
157
|
+
transport: 'sse',
|
|
158
|
+
url: 'https://example.com/sse',
|
|
159
|
+
workspacePath,
|
|
160
|
+
}),
|
|
161
|
+
(error: unknown) =>
|
|
162
|
+
error instanceof AppError &&
|
|
163
|
+
error.code === 'MCP_TRANSPORT_NOT_SUPPORTED' &&
|
|
164
|
+
error.statusCode === 400,
|
|
165
|
+
);
|
|
166
|
+
} finally {
|
|
167
|
+
restoreHomeDir();
|
|
168
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* This test covers Gemini/Cursor MCP JSON formats and user/project scope persistence.
|
|
174
|
+
*/
|
|
175
|
+
test('providerMcpService handles gemini and cursor MCP JSON config formats', { concurrency: false }, async () => {
|
|
176
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-gc-'));
|
|
177
|
+
const workspacePath = path.join(tempRoot, 'workspace');
|
|
178
|
+
await fs.mkdir(workspacePath, { recursive: true });
|
|
179
|
+
|
|
180
|
+
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
181
|
+
try {
|
|
182
|
+
await providerMcpService.upsertProviderMcpServer('gemini', {
|
|
183
|
+
name: 'gemini-stdio',
|
|
184
|
+
scope: 'user',
|
|
185
|
+
transport: 'stdio',
|
|
186
|
+
command: 'node',
|
|
187
|
+
args: ['server.js'],
|
|
188
|
+
env: { TOKEN: '$TOKEN' },
|
|
189
|
+
cwd: './server',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await providerMcpService.upsertProviderMcpServer('gemini', {
|
|
193
|
+
name: 'gemini-http',
|
|
194
|
+
scope: 'project',
|
|
195
|
+
transport: 'http',
|
|
196
|
+
url: 'https://gemini.example.com/mcp',
|
|
197
|
+
headers: { Authorization: 'Bearer token' },
|
|
198
|
+
workspacePath,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await providerMcpService.upsertProviderMcpServer('cursor', {
|
|
202
|
+
name: 'cursor-stdio',
|
|
203
|
+
scope: 'project',
|
|
204
|
+
transport: 'stdio',
|
|
205
|
+
command: 'npx',
|
|
206
|
+
args: ['-y', 'mcp-server'],
|
|
207
|
+
env: { API_KEY: 'value' },
|
|
208
|
+
workspacePath,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await providerMcpService.upsertProviderMcpServer('cursor', {
|
|
212
|
+
name: 'cursor-http',
|
|
213
|
+
scope: 'user',
|
|
214
|
+
transport: 'http',
|
|
215
|
+
url: 'http://localhost:3333/mcp',
|
|
216
|
+
headers: { API_KEY: 'value' },
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const geminiUserConfig = await readJson(path.join(tempRoot, '.gemini', 'settings.json'));
|
|
220
|
+
const geminiUserServer = (geminiUserConfig.mcpServers as Record<string, unknown>)['gemini-stdio'] as Record<string, unknown>;
|
|
221
|
+
assert.equal(geminiUserServer.command, 'node');
|
|
222
|
+
assert.equal(geminiUserServer.type, undefined);
|
|
223
|
+
|
|
224
|
+
const geminiProjectConfig = await readJson(path.join(workspacePath, '.gemini', 'settings.json'));
|
|
225
|
+
const geminiProjectServer = (geminiProjectConfig.mcpServers as Record<string, unknown>)['gemini-http'] as Record<string, unknown>;
|
|
226
|
+
assert.equal(geminiProjectServer.type, 'http');
|
|
227
|
+
|
|
228
|
+
const cursorUserConfig = await readJson(path.join(tempRoot, '.cursor', 'mcp.json'));
|
|
229
|
+
const cursorHttpServer = (cursorUserConfig.mcpServers as Record<string, unknown>)['cursor-http'] as Record<string, unknown>;
|
|
230
|
+
assert.equal(cursorHttpServer.url, 'http://localhost:3333/mcp');
|
|
231
|
+
assert.equal(cursorHttpServer.type, undefined);
|
|
232
|
+
} finally {
|
|
233
|
+
restoreHomeDir();
|
|
234
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* This test covers the global MCP adder requirement: only http/stdio are allowed and
|
|
240
|
+
* one payload is written to all providers.
|
|
241
|
+
*/
|
|
242
|
+
test('providerMcpService global adder writes to all providers and rejects unsupported transports', { concurrency: false }, async () => {
|
|
243
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-global-'));
|
|
244
|
+
const workspacePath = path.join(tempRoot, 'workspace');
|
|
245
|
+
await fs.mkdir(workspacePath, { recursive: true });
|
|
246
|
+
|
|
247
|
+
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
248
|
+
try {
|
|
249
|
+
const globalResult = await providerMcpService.addMcpServerToAllProviders({
|
|
250
|
+
name: 'global-http',
|
|
251
|
+
scope: 'project',
|
|
252
|
+
transport: 'http',
|
|
253
|
+
url: 'https://global.example.com/mcp',
|
|
254
|
+
workspacePath,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const expectCursorGlobal = process.platform !== 'win32';
|
|
258
|
+
assert.equal(globalResult.length, expectCursorGlobal ? 4 : 3);
|
|
259
|
+
assert.ok(globalResult.every((entry) => entry.created === true));
|
|
260
|
+
|
|
261
|
+
const claudeProject = await readJson(path.join(workspacePath, '.mcp.json'));
|
|
262
|
+
assert.ok((claudeProject.mcpServers as Record<string, unknown>)['global-http']);
|
|
263
|
+
|
|
264
|
+
const codexProject = TOML.parse(await fs.readFile(path.join(workspacePath, '.codex', 'config.toml'), 'utf8')) as Record<string, unknown>;
|
|
265
|
+
assert.ok((codexProject.mcp_servers as Record<string, unknown>)['global-http']);
|
|
266
|
+
|
|
267
|
+
const geminiProject = await readJson(path.join(workspacePath, '.gemini', 'settings.json'));
|
|
268
|
+
assert.ok((geminiProject.mcpServers as Record<string, unknown>)['global-http']);
|
|
269
|
+
|
|
270
|
+
if (expectCursorGlobal) {
|
|
271
|
+
const cursorProject = await readJson(path.join(workspacePath, '.cursor', 'mcp.json'));
|
|
272
|
+
assert.ok((cursorProject.mcpServers as Record<string, unknown>)['global-http']);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await assert.rejects(
|
|
276
|
+
providerMcpService.addMcpServerToAllProviders({
|
|
277
|
+
name: 'global-sse',
|
|
278
|
+
scope: 'project',
|
|
279
|
+
transport: 'sse',
|
|
280
|
+
url: 'https://example.com/sse',
|
|
281
|
+
workspacePath,
|
|
282
|
+
}),
|
|
283
|
+
(error: unknown) =>
|
|
284
|
+
error instanceof AppError &&
|
|
285
|
+
error.code === 'INVALID_GLOBAL_MCP_TRANSPORT' &&
|
|
286
|
+
error.statusCode === 400,
|
|
287
|
+
);
|
|
288
|
+
} finally {
|
|
289
|
+
restoreHomeDir();
|
|
290
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
package/server/openai-codex.js
CHANGED
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
|
|
16
16
|
import { Codex } from '@openai/codex-sdk';
|
|
17
17
|
import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
18
|
+
import { sessionsService } from './modules/providers/services/sessions.service.js';
|
|
19
|
+
import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
|
|
20
|
+
import { createNormalizedMessage } from './shared/utils.js';
|
|
21
21
|
|
|
22
22
|
// Track active sessions
|
|
23
23
|
const activeCodexSessions = new Map();
|
|
@@ -265,7 +265,7 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
265
265
|
const transformed = transformCodexEvent(event);
|
|
266
266
|
|
|
267
267
|
// Normalize the transformed event into NormalizedMessage(s) via adapter
|
|
268
|
-
const normalizedMsgs =
|
|
268
|
+
const normalizedMsgs = sessionsService.normalizeMessage('codex', transformed, currentSessionId);
|
|
269
269
|
for (const msg of normalizedMsgs) {
|
|
270
270
|
sendMessage(ws, msg);
|
|
271
271
|
}
|
|
@@ -311,7 +311,7 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
311
311
|
console.error('[Codex] Error:', error);
|
|
312
312
|
|
|
313
313
|
// Check if Codex SDK is available for a clearer error message
|
|
314
|
-
const installed =
|
|
314
|
+
const installed = await providerAuthService.isProviderInstalled('codex');
|
|
315
315
|
const errorContent = !installed
|
|
316
316
|
? 'Codex CLI is not configured. Please set up authentication first.'
|
|
317
317
|
: error.message;
|
package/server/routes/codex.js
CHANGED
|
@@ -1,73 +1,9 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import os from 'os';
|
|
6
|
-
import TOML from '@iarna/toml';
|
|
7
|
-
import { getCodexSessions, deleteCodexSession } from '../projects.js';
|
|
8
|
-
import { applyCustomSessionNames, sessionNamesDb } from '../database/db.js';
|
|
2
|
+
import { deleteCodexSession } from '../projects.js';
|
|
3
|
+
import { sessionNamesDb } from '../database/db.js';
|
|
9
4
|
|
|
10
5
|
const router = express.Router();
|
|
11
6
|
|
|
12
|
-
function createCliResponder(res) {
|
|
13
|
-
let responded = false;
|
|
14
|
-
return (status, payload) => {
|
|
15
|
-
if (responded || res.headersSent) {
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
responded = true;
|
|
19
|
-
res.status(status).json(payload);
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
router.get('/config', async (req, res) => {
|
|
24
|
-
try {
|
|
25
|
-
const configPath = path.join(os.homedir(), '.codex', 'config.toml');
|
|
26
|
-
const content = await fs.readFile(configPath, 'utf8');
|
|
27
|
-
const config = TOML.parse(content);
|
|
28
|
-
|
|
29
|
-
res.json({
|
|
30
|
-
success: true,
|
|
31
|
-
config: {
|
|
32
|
-
model: config.model || null,
|
|
33
|
-
mcpServers: config.mcp_servers || {},
|
|
34
|
-
approvalMode: config.approval_mode || 'suggest'
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
} catch (error) {
|
|
38
|
-
if (error.code === 'ENOENT') {
|
|
39
|
-
res.json({
|
|
40
|
-
success: true,
|
|
41
|
-
config: {
|
|
42
|
-
model: null,
|
|
43
|
-
mcpServers: {},
|
|
44
|
-
approvalMode: 'suggest'
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
} else {
|
|
48
|
-
console.error('Error reading Codex config:', error);
|
|
49
|
-
res.status(500).json({ success: false, error: error.message });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
router.get('/sessions', async (req, res) => {
|
|
55
|
-
try {
|
|
56
|
-
const { projectPath } = req.query;
|
|
57
|
-
|
|
58
|
-
if (!projectPath) {
|
|
59
|
-
return res.status(400).json({ success: false, error: 'projectPath query parameter required' });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const sessions = await getCodexSessions(projectPath);
|
|
63
|
-
applyCustomSessionNames(sessions, 'codex');
|
|
64
|
-
res.json({ success: true, sessions });
|
|
65
|
-
} catch (error) {
|
|
66
|
-
console.error('Error fetching Codex sessions:', error);
|
|
67
|
-
res.status(500).json({ success: false, error: error.message });
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
7
|
router.delete('/sessions/:sessionId', async (req, res) => {
|
|
72
8
|
try {
|
|
73
9
|
const { sessionId } = req.params;
|
|
@@ -80,250 +16,4 @@ router.delete('/sessions/:sessionId', async (req, res) => {
|
|
|
80
16
|
}
|
|
81
17
|
});
|
|
82
18
|
|
|
83
|
-
// MCP Server Management Routes
|
|
84
|
-
|
|
85
|
-
router.get('/mcp/cli/list', async (req, res) => {
|
|
86
|
-
try {
|
|
87
|
-
const respond = createCliResponder(res);
|
|
88
|
-
const proc = spawn('codex', ['mcp', 'list'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
89
|
-
|
|
90
|
-
let stdout = '';
|
|
91
|
-
let stderr = '';
|
|
92
|
-
|
|
93
|
-
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
94
|
-
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
95
|
-
|
|
96
|
-
proc.on('close', (code) => {
|
|
97
|
-
if (code === 0) {
|
|
98
|
-
respond(200, { success: true, output: stdout, servers: parseCodexListOutput(stdout) });
|
|
99
|
-
} else {
|
|
100
|
-
respond(500, { error: 'Codex CLI command failed', details: stderr || `Exited with code ${code}` });
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
proc.on('error', (error) => {
|
|
105
|
-
const isMissing = error?.code === 'ENOENT';
|
|
106
|
-
respond(isMissing ? 503 : 500, {
|
|
107
|
-
error: isMissing ? 'Codex CLI not installed' : 'Failed to run Codex CLI',
|
|
108
|
-
details: error.message,
|
|
109
|
-
code: error.code
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
} catch (error) {
|
|
113
|
-
res.status(500).json({ error: 'Failed to list MCP servers', details: error.message });
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
router.post('/mcp/cli/add', async (req, res) => {
|
|
118
|
-
try {
|
|
119
|
-
const { name, command, args = [], env = {} } = req.body;
|
|
120
|
-
|
|
121
|
-
if (!name || !command) {
|
|
122
|
-
return res.status(400).json({ error: 'name and command are required' });
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Build: codex mcp add <name> [-e KEY=VAL]... -- <command> [args...]
|
|
126
|
-
let cliArgs = ['mcp', 'add', name];
|
|
127
|
-
|
|
128
|
-
Object.entries(env).forEach(([key, value]) => {
|
|
129
|
-
cliArgs.push('-e', `${key}=${value}`);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
cliArgs.push('--', command);
|
|
133
|
-
|
|
134
|
-
if (args && args.length > 0) {
|
|
135
|
-
cliArgs.push(...args);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const respond = createCliResponder(res);
|
|
139
|
-
const proc = spawn('codex', cliArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
140
|
-
|
|
141
|
-
let stdout = '';
|
|
142
|
-
let stderr = '';
|
|
143
|
-
|
|
144
|
-
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
145
|
-
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
146
|
-
|
|
147
|
-
proc.on('close', (code) => {
|
|
148
|
-
if (code === 0) {
|
|
149
|
-
respond(200, { success: true, output: stdout, message: `MCP server "${name}" added successfully` });
|
|
150
|
-
} else {
|
|
151
|
-
respond(400, { error: 'Codex CLI command failed', details: stderr || `Exited with code ${code}` });
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
proc.on('error', (error) => {
|
|
156
|
-
const isMissing = error?.code === 'ENOENT';
|
|
157
|
-
respond(isMissing ? 503 : 500, {
|
|
158
|
-
error: isMissing ? 'Codex CLI not installed' : 'Failed to run Codex CLI',
|
|
159
|
-
details: error.message,
|
|
160
|
-
code: error.code
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
} catch (error) {
|
|
164
|
-
res.status(500).json({ error: 'Failed to add MCP server', details: error.message });
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
router.delete('/mcp/cli/remove/:name', async (req, res) => {
|
|
169
|
-
try {
|
|
170
|
-
const { name } = req.params;
|
|
171
|
-
|
|
172
|
-
const respond = createCliResponder(res);
|
|
173
|
-
const proc = spawn('codex', ['mcp', 'remove', name], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
174
|
-
|
|
175
|
-
let stdout = '';
|
|
176
|
-
let stderr = '';
|
|
177
|
-
|
|
178
|
-
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
179
|
-
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
180
|
-
|
|
181
|
-
proc.on('close', (code) => {
|
|
182
|
-
if (code === 0) {
|
|
183
|
-
respond(200, { success: true, output: stdout, message: `MCP server "${name}" removed successfully` });
|
|
184
|
-
} else {
|
|
185
|
-
respond(400, { error: 'Codex CLI command failed', details: stderr || `Exited with code ${code}` });
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
proc.on('error', (error) => {
|
|
190
|
-
const isMissing = error?.code === 'ENOENT';
|
|
191
|
-
respond(isMissing ? 503 : 500, {
|
|
192
|
-
error: isMissing ? 'Codex CLI not installed' : 'Failed to run Codex CLI',
|
|
193
|
-
details: error.message,
|
|
194
|
-
code: error.code
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
} catch (error) {
|
|
198
|
-
res.status(500).json({ error: 'Failed to remove MCP server', details: error.message });
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
router.get('/mcp/cli/get/:name', async (req, res) => {
|
|
203
|
-
try {
|
|
204
|
-
const { name } = req.params;
|
|
205
|
-
|
|
206
|
-
const respond = createCliResponder(res);
|
|
207
|
-
const proc = spawn('codex', ['mcp', 'get', name], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
208
|
-
|
|
209
|
-
let stdout = '';
|
|
210
|
-
let stderr = '';
|
|
211
|
-
|
|
212
|
-
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
213
|
-
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
214
|
-
|
|
215
|
-
proc.on('close', (code) => {
|
|
216
|
-
if (code === 0) {
|
|
217
|
-
respond(200, { success: true, output: stdout, server: parseCodexGetOutput(stdout) });
|
|
218
|
-
} else {
|
|
219
|
-
respond(404, { error: 'Codex CLI command failed', details: stderr || `Exited with code ${code}` });
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
proc.on('error', (error) => {
|
|
224
|
-
const isMissing = error?.code === 'ENOENT';
|
|
225
|
-
respond(isMissing ? 503 : 500, {
|
|
226
|
-
error: isMissing ? 'Codex CLI not installed' : 'Failed to run Codex CLI',
|
|
227
|
-
details: error.message,
|
|
228
|
-
code: error.code
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
} catch (error) {
|
|
232
|
-
res.status(500).json({ error: 'Failed to get MCP server details', details: error.message });
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
router.get('/mcp/config/read', async (req, res) => {
|
|
237
|
-
try {
|
|
238
|
-
const configPath = path.join(os.homedir(), '.codex', 'config.toml');
|
|
239
|
-
|
|
240
|
-
let configData = null;
|
|
241
|
-
|
|
242
|
-
try {
|
|
243
|
-
const fileContent = await fs.readFile(configPath, 'utf8');
|
|
244
|
-
configData = TOML.parse(fileContent);
|
|
245
|
-
} catch (error) {
|
|
246
|
-
// Config file doesn't exist
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (!configData) {
|
|
250
|
-
return res.json({ success: true, configPath, servers: [] }); }
|
|
251
|
-
|
|
252
|
-
const servers = [];
|
|
253
|
-
|
|
254
|
-
if (configData.mcp_servers && typeof configData.mcp_servers === 'object') {
|
|
255
|
-
for (const [name, config] of Object.entries(configData.mcp_servers)) {
|
|
256
|
-
servers.push({
|
|
257
|
-
id: name,
|
|
258
|
-
name: name,
|
|
259
|
-
type: 'stdio',
|
|
260
|
-
scope: 'user',
|
|
261
|
-
config: {
|
|
262
|
-
command: config.command || '',
|
|
263
|
-
args: config.args || [],
|
|
264
|
-
env: config.env || {}
|
|
265
|
-
},
|
|
266
|
-
raw: config
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
res.json({ success: true, configPath, servers });
|
|
272
|
-
} catch (error) {
|
|
273
|
-
res.status(500).json({ error: 'Failed to read Codex configuration', details: error.message });
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
function parseCodexListOutput(output) {
|
|
278
|
-
const servers = [];
|
|
279
|
-
const lines = output.split('\n').filter(line => line.trim());
|
|
280
|
-
|
|
281
|
-
for (const line of lines) {
|
|
282
|
-
if (line.includes(':')) {
|
|
283
|
-
const colonIndex = line.indexOf(':');
|
|
284
|
-
const name = line.substring(0, colonIndex).trim();
|
|
285
|
-
|
|
286
|
-
if (!name) continue;
|
|
287
|
-
|
|
288
|
-
const rest = line.substring(colonIndex + 1).trim();
|
|
289
|
-
let description = rest;
|
|
290
|
-
let status = 'unknown';
|
|
291
|
-
|
|
292
|
-
if (rest.includes('✓') || rest.includes('✗')) {
|
|
293
|
-
const statusMatch = rest.match(/(.*?)\s*-\s*([✓✗].*)$/);
|
|
294
|
-
if (statusMatch) {
|
|
295
|
-
description = statusMatch[1].trim();
|
|
296
|
-
status = statusMatch[2].includes('✓') ? 'connected' : 'failed';
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
servers.push({ name, type: 'stdio', status, description });
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return servers;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function parseCodexGetOutput(output) {
|
|
308
|
-
try {
|
|
309
|
-
const jsonMatch = output.match(/\{[\s\S]*\}/);
|
|
310
|
-
if (jsonMatch) {
|
|
311
|
-
return JSON.parse(jsonMatch[0]);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const server = { raw_output: output };
|
|
315
|
-
const lines = output.split('\n');
|
|
316
|
-
|
|
317
|
-
for (const line of lines) {
|
|
318
|
-
if (line.includes('Name:')) server.name = line.split(':')[1]?.trim();
|
|
319
|
-
else if (line.includes('Type:')) server.type = line.split(':')[1]?.trim();
|
|
320
|
-
else if (line.includes('Command:')) server.command = line.split(':')[1]?.trim();
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return server;
|
|
324
|
-
} catch (error) {
|
|
325
|
-
return { raw_output: output, parse_error: error.message };
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
19
|
export default router;
|