@pixelbyte-software/pixcode 1.30.2 → 1.31.1

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.
Files changed (209) hide show
  1. package/LICENSE +718 -718
  2. package/README.de.md +248 -248
  3. package/README.ja.md +240 -240
  4. package/README.ko.md +240 -240
  5. package/README.md +295 -285
  6. package/README.ru.md +248 -248
  7. package/README.tr.md +250 -250
  8. package/README.zh-CN.md +240 -240
  9. package/dist/api-docs.html +879 -879
  10. package/dist/assets/index-BtOeB3cE.js +837 -0
  11. package/dist/assets/index-CDpePeIN.css +32 -0
  12. package/dist/assets/vendor-codemirror-CzYAOTxS.js +41 -0
  13. package/dist/clear-cache.html +85 -85
  14. package/dist/convert-icons.md +52 -52
  15. package/dist/favicon.png +0 -0
  16. package/dist/favicon.svg +7 -8
  17. package/dist/generate-icons.js +48 -48
  18. package/dist/icons/codex-white.svg +3 -3
  19. package/dist/icons/codex.svg +3 -3
  20. package/dist/icons/cursor-white.svg +11 -11
  21. package/dist/icons/icon-128x128.png +0 -0
  22. package/dist/icons/icon-128x128.svg +9 -12
  23. package/dist/icons/icon-144x144.png +0 -0
  24. package/dist/icons/icon-144x144.svg +9 -12
  25. package/dist/icons/icon-152x152.png +0 -0
  26. package/dist/icons/icon-152x152.svg +9 -12
  27. package/dist/icons/icon-192x192.png +0 -0
  28. package/dist/icons/icon-192x192.svg +9 -12
  29. package/dist/icons/icon-384x384.png +0 -0
  30. package/dist/icons/icon-384x384.svg +9 -12
  31. package/dist/icons/icon-512x512.png +0 -0
  32. package/dist/icons/icon-512x512.svg +9 -12
  33. package/dist/icons/icon-72x72.png +0 -0
  34. package/dist/icons/icon-72x72.svg +9 -12
  35. package/dist/icons/icon-96x96.png +0 -0
  36. package/dist/icons/icon-96x96.svg +9 -12
  37. package/dist/icons/icon-template.svg +9 -12
  38. package/dist/icons/qwen-ai-icon.png +0 -0
  39. package/dist/index.html +60 -50
  40. package/dist/logo.png +0 -0
  41. package/dist/logo.svg +11 -16
  42. package/dist/manifest.json +60 -60
  43. package/dist/sw.js +124 -124
  44. package/dist-server/server/claude-sdk.js +28 -5
  45. package/dist-server/server/claude-sdk.js.map +1 -1
  46. package/dist-server/server/cli.js +100 -97
  47. package/dist-server/server/cli.js.map +1 -1
  48. package/dist-server/server/daemon/manager.js +33 -33
  49. package/dist-server/server/daemon-manager.js +62 -62
  50. package/dist-server/server/database/db.js +114 -22
  51. package/dist-server/server/database/db.js.map +1 -1
  52. package/dist-server/server/database/schema.js +122 -89
  53. package/dist-server/server/database/schema.js.map +1 -1
  54. package/dist-server/server/gemini-cli.js +6 -1
  55. package/dist-server/server/gemini-cli.js.map +1 -1
  56. package/dist-server/server/index.js +346 -61
  57. package/dist-server/server/index.js.map +1 -1
  58. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +29 -2
  59. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  60. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js +22 -2
  61. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js.map +1 -1
  62. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +2 -2
  63. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
  64. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +14 -2
  65. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -1
  66. package/dist-server/server/modules/providers/list/qwen/qwen-auth.provider.js +132 -0
  67. package/dist-server/server/modules/providers/list/qwen/qwen-auth.provider.js.map +1 -0
  68. package/dist-server/server/modules/providers/list/qwen/qwen-mcp.provider.js +87 -0
  69. package/dist-server/server/modules/providers/list/qwen/qwen-mcp.provider.js.map +1 -0
  70. package/dist-server/server/modules/providers/list/qwen/qwen-sessions.provider.js +201 -0
  71. package/dist-server/server/modules/providers/list/qwen/qwen-sessions.provider.js.map +1 -0
  72. package/dist-server/server/modules/providers/list/qwen/qwen.provider.js +19 -0
  73. package/dist-server/server/modules/providers/list/qwen/qwen.provider.js.map +1 -0
  74. package/dist-server/server/modules/providers/provider.registry.js +2 -0
  75. package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
  76. package/dist-server/server/modules/providers/provider.routes.js +478 -1
  77. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  78. package/dist-server/server/modules/providers/shared/provider-configs.js +105 -0
  79. package/dist-server/server/modules/providers/shared/provider-configs.js.map +1 -0
  80. package/dist-server/server/projects.js +197 -6
  81. package/dist-server/server/projects.js.map +1 -1
  82. package/dist-server/server/qwen-code-cli.js +350 -0
  83. package/dist-server/server/qwen-code-cli.js.map +1 -0
  84. package/dist-server/server/qwen-response-handler.js +70 -0
  85. package/dist-server/server/qwen-response-handler.js.map +1 -0
  86. package/dist-server/server/routes/commands.js +25 -25
  87. package/dist-server/server/routes/git.js +17 -17
  88. package/dist-server/server/routes/network.js +116 -0
  89. package/dist-server/server/routes/network.js.map +1 -0
  90. package/dist-server/server/routes/projects.js +166 -1
  91. package/dist-server/server/routes/projects.js.map +1 -1
  92. package/dist-server/server/routes/qwen.js +23 -0
  93. package/dist-server/server/routes/qwen.js.map +1 -0
  94. package/dist-server/server/routes/taskmaster.js +419 -419
  95. package/dist-server/server/routes/telegram.js +119 -0
  96. package/dist-server/server/routes/telegram.js.map +1 -0
  97. package/dist-server/server/services/external-access.js +228 -0
  98. package/dist-server/server/services/external-access.js.map +1 -0
  99. package/dist-server/server/services/install-jobs.js +552 -0
  100. package/dist-server/server/services/install-jobs.js.map +1 -0
  101. package/dist-server/server/services/notification-orchestrator.js +19 -5
  102. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  103. package/dist-server/server/services/provider-credentials.js +154 -0
  104. package/dist-server/server/services/provider-credentials.js.map +1 -0
  105. package/dist-server/server/services/provider-models.js +218 -0
  106. package/dist-server/server/services/provider-models.js.map +1 -0
  107. package/dist-server/server/services/telegram/bot.js +259 -0
  108. package/dist-server/server/services/telegram/bot.js.map +1 -0
  109. package/dist-server/server/services/telegram/translations.js +160 -0
  110. package/dist-server/server/services/telegram/translations.js.map +1 -0
  111. package/dist-server/server/utils/port-access.js +196 -0
  112. package/dist-server/server/utils/port-access.js.map +1 -0
  113. package/dist-server/shared/modelConstants.js +18 -0
  114. package/dist-server/shared/modelConstants.js.map +1 -1
  115. package/package.json +177 -168
  116. package/scripts/fix-node-pty.js +67 -67
  117. package/server/claude-sdk.js +857 -834
  118. package/server/cli.js +940 -937
  119. package/server/constants/config.js +4 -4
  120. package/server/cursor-cli.js +342 -342
  121. package/server/daemon/manager.js +564 -564
  122. package/server/daemon-manager.js +920 -920
  123. package/server/database/db.js +696 -593
  124. package/server/database/schema.js +138 -102
  125. package/server/gemini-cli.js +475 -469
  126. package/server/gemini-response-handler.js +79 -79
  127. package/server/index.js +2854 -2556
  128. package/server/load-env.js +34 -34
  129. package/server/middleware/auth.js +132 -132
  130. package/server/modules/providers/list/claude/claude-auth.provider.ts +145 -123
  131. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
  132. package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -306
  133. package/server/modules/providers/list/claude/claude.provider.ts +15 -15
  134. package/server/modules/providers/list/codex/codex-auth.provider.ts +115 -100
  135. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
  136. package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -319
  137. package/server/modules/providers/list/codex/codex.provider.ts +15 -15
  138. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +143 -143
  139. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
  140. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -421
  141. package/server/modules/providers/list/cursor/cursor.provider.ts +15 -15
  142. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +163 -151
  143. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -110
  144. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -227
  145. package/server/modules/providers/list/gemini/gemini.provider.ts +15 -15
  146. package/server/modules/providers/list/qwen/qwen-auth.provider.ts +145 -0
  147. package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -0
  148. package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +218 -0
  149. package/server/modules/providers/list/qwen/qwen.provider.ts +21 -0
  150. package/server/modules/providers/provider.registry.ts +38 -36
  151. package/server/modules/providers/provider.routes.ts +781 -217
  152. package/server/modules/providers/services/mcp.service.ts +94 -94
  153. package/server/modules/providers/services/provider-auth.service.ts +26 -26
  154. package/server/modules/providers/services/sessions.service.ts +45 -45
  155. package/server/modules/providers/shared/base/abstract.provider.ts +20 -20
  156. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
  157. package/server/modules/providers/shared/provider-configs.ts +118 -0
  158. package/server/modules/providers/tests/mcp.test.ts +293 -293
  159. package/server/openai-codex.js +426 -426
  160. package/server/projects.js +2993 -2792
  161. package/server/qwen-code-cli.js +392 -0
  162. package/server/qwen-response-handler.js +73 -0
  163. package/server/routes/agent.js +1245 -1245
  164. package/server/routes/auth.js +134 -134
  165. package/server/routes/codex.js +19 -19
  166. package/server/routes/commands.js +554 -554
  167. package/server/routes/cursor.js +52 -52
  168. package/server/routes/gemini.js +24 -24
  169. package/server/routes/git.js +1488 -1488
  170. package/server/routes/mcp-utils.js +31 -31
  171. package/server/routes/messages.js +61 -61
  172. package/server/routes/network.js +128 -0
  173. package/server/routes/plugins.js +307 -307
  174. package/server/routes/projects.js +795 -627
  175. package/server/routes/qwen.js +27 -0
  176. package/server/routes/settings.js +286 -286
  177. package/server/routes/taskmaster.js +1471 -1471
  178. package/server/routes/telegram.js +125 -0
  179. package/server/routes/user.js +123 -123
  180. package/server/services/external-access.js +240 -0
  181. package/server/services/install-jobs.js +569 -0
  182. package/server/services/notification-orchestrator.js +242 -227
  183. package/server/services/provider-credentials.js +151 -0
  184. package/server/services/provider-models.js +225 -0
  185. package/server/services/telegram/bot.js +280 -0
  186. package/server/services/telegram/translations.js +170 -0
  187. package/server/services/vapid-keys.js +35 -35
  188. package/server/sessionManager.js +225 -225
  189. package/server/shared/interfaces.ts +54 -54
  190. package/server/shared/types.ts +172 -172
  191. package/server/shared/utils.ts +193 -193
  192. package/server/tsconfig.json +36 -36
  193. package/server/utils/colors.js +21 -21
  194. package/server/utils/commandParser.js +303 -303
  195. package/server/utils/frontmatter.js +18 -18
  196. package/server/utils/gitConfig.js +34 -34
  197. package/server/utils/mcp-detector.js +147 -147
  198. package/server/utils/plugin-loader.js +457 -457
  199. package/server/utils/plugin-process-manager.js +184 -184
  200. package/server/utils/port-access.js +209 -0
  201. package/server/utils/runtime-paths.js +37 -37
  202. package/server/utils/taskmaster-websocket.js +128 -128
  203. package/server/utils/url-detection.js +71 -71
  204. package/server/vite-daemon.js +78 -78
  205. package/shared/modelConstants.js +117 -97
  206. package/shared/networkHosts.js +22 -22
  207. package/dist/assets/index-C2c9QNwK.css +0 -32
  208. package/dist/assets/index-DyXDZED-.js +0 -1277
  209. package/dist/assets/vendor-codemirror-NA4v81it.js +0 -41
@@ -1,293 +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
-
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
+