@hileeon/mcc 0.1.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.
Files changed (138) hide show
  1. package/.claude/CLAUDE.md +204 -0
  2. package/.claude/agents/.gitkeep +0 -0
  3. package/.claude/settings.json +9 -0
  4. package/.claude/skills/.gitkeep +0 -0
  5. package/README.md +127 -0
  6. package/dist/accounts/instance-manager.d.ts +11 -0
  7. package/dist/accounts/instance-manager.d.ts.map +1 -0
  8. package/dist/accounts/instance-manager.js +89 -0
  9. package/dist/accounts/instance-manager.js.map +1 -0
  10. package/dist/accounts/shared-manager.d.ts +25 -0
  11. package/dist/accounts/shared-manager.d.ts.map +1 -0
  12. package/dist/accounts/shared-manager.js +186 -0
  13. package/dist/accounts/shared-manager.js.map +1 -0
  14. package/dist/accounts/store.d.ts +30 -0
  15. package/dist/accounts/store.d.ts.map +1 -0
  16. package/dist/accounts/store.js +128 -0
  17. package/dist/accounts/store.js.map +1 -0
  18. package/dist/core/model-router.d.ts +30 -0
  19. package/dist/core/model-router.d.ts.map +1 -0
  20. package/dist/core/model-router.js +64 -0
  21. package/dist/core/model-router.js.map +1 -0
  22. package/dist/dashboard-server.d.ts +5 -0
  23. package/dist/dashboard-server.d.ts.map +1 -0
  24. package/dist/dashboard-server.js +387 -0
  25. package/dist/dashboard-server.js.map +1 -0
  26. package/dist/mcc.d.ts +8 -0
  27. package/dist/mcc.d.ts.map +1 -0
  28. package/dist/mcc.js +474 -0
  29. package/dist/mcc.js.map +1 -0
  30. package/dist/mcp/external-registry.d.ts +24 -0
  31. package/dist/mcp/external-registry.d.ts.map +1 -0
  32. package/dist/mcp/external-registry.js +99 -0
  33. package/dist/mcp/external-registry.js.map +1 -0
  34. package/dist/mcp/installer.d.ts +31 -0
  35. package/dist/mcp/installer.d.ts.map +1 -0
  36. package/dist/mcp/installer.js +273 -0
  37. package/dist/mcp/installer.js.map +1 -0
  38. package/dist/mcp/mcp-config.d.ts +86 -0
  39. package/dist/mcp/mcp-config.d.ts.map +1 -0
  40. package/dist/mcp/mcp-config.js +178 -0
  41. package/dist/mcp/mcp-config.js.map +1 -0
  42. package/dist/mcp/registry.d.ts +23 -0
  43. package/dist/mcp/registry.d.ts.map +1 -0
  44. package/dist/mcp/registry.js +100 -0
  45. package/dist/mcp/registry.js.map +1 -0
  46. package/dist/proxy/proxy-daemon.d.ts +27 -0
  47. package/dist/proxy/proxy-daemon.d.ts.map +1 -0
  48. package/dist/proxy/proxy-daemon.js +192 -0
  49. package/dist/proxy/proxy-daemon.js.map +1 -0
  50. package/dist/proxy/proxy-entry.d.ts +11 -0
  51. package/dist/proxy/proxy-entry.d.ts.map +1 -0
  52. package/dist/proxy/proxy-entry.js +74 -0
  53. package/dist/proxy/proxy-entry.js.map +1 -0
  54. package/dist/proxy/proxy-paths.d.ts +27 -0
  55. package/dist/proxy/proxy-paths.d.ts.map +1 -0
  56. package/dist/proxy/proxy-paths.js +125 -0
  57. package/dist/proxy/proxy-paths.js.map +1 -0
  58. package/dist/proxy/proxy-server.d.ts +20 -0
  59. package/dist/proxy/proxy-server.d.ts.map +1 -0
  60. package/dist/proxy/proxy-server.js +280 -0
  61. package/dist/proxy/proxy-server.js.map +1 -0
  62. package/dist/proxy/upstream-url.d.ts +7 -0
  63. package/dist/proxy/upstream-url.d.ts.map +1 -0
  64. package/dist/proxy/upstream-url.js +38 -0
  65. package/dist/proxy/upstream-url.js.map +1 -0
  66. package/dist/shared/logger.d.ts +23 -0
  67. package/dist/shared/logger.d.ts.map +1 -0
  68. package/dist/shared/logger.js +184 -0
  69. package/dist/shared/logger.js.map +1 -0
  70. package/dist/shared/provider-preset-catalog.d.ts +41 -0
  71. package/dist/shared/provider-preset-catalog.d.ts.map +1 -0
  72. package/dist/shared/provider-preset-catalog.js +299 -0
  73. package/dist/shared/provider-preset-catalog.js.map +1 -0
  74. package/docs/decisions.md +33 -0
  75. package/docs/lessons.md +8 -0
  76. package/docs/product.md +37 -0
  77. package/lib/mcp/mcc-image-analysis-server.cjs +454 -0
  78. package/lib/mcp/mcc-websearch-server.cjs +339 -0
  79. package/lib/mcp-hooks/image-analysis-runtime.cjs +510 -0
  80. package/lib/mcp-hooks/image-analyzer-transformer.cjs +526 -0
  81. package/lib/mcp-hooks/websearch-transformer.cjs +1421 -0
  82. package/lib/proxy/config/config-loader-facade.js +24 -0
  83. package/lib/proxy/glmt/delta-accumulator.js +363 -0
  84. package/lib/proxy/glmt/glmt-transformer.js +204 -0
  85. package/lib/proxy/glmt/index.js +41 -0
  86. package/lib/proxy/glmt/locale-enforcer.js +69 -0
  87. package/lib/proxy/glmt/pipeline/content-transformer.js +162 -0
  88. package/lib/proxy/glmt/pipeline/index.js +20 -0
  89. package/lib/proxy/glmt/pipeline/request-transformer.js +116 -0
  90. package/lib/proxy/glmt/pipeline/response-builder.js +205 -0
  91. package/lib/proxy/glmt/pipeline/stream-parser.js +234 -0
  92. package/lib/proxy/glmt/pipeline/tool-call-handler.js +78 -0
  93. package/lib/proxy/glmt/pipeline/types.js +6 -0
  94. package/lib/proxy/glmt/reasoning-enforcer.js +151 -0
  95. package/lib/proxy/glmt/sse-parser.js +102 -0
  96. package/lib/proxy/services/logging.js +13 -0
  97. package/lib/proxy/transformers/request-transformer.js +452 -0
  98. package/lib/proxy/transformers/sse-stream-transformer.js +199 -0
  99. package/lib/shared/logger.cjs +138 -0
  100. package/package.json +35 -0
  101. package/src/accounts/instance-manager.ts +58 -0
  102. package/src/accounts/shared-manager.ts +154 -0
  103. package/src/accounts/store.ts +111 -0
  104. package/src/core/model-router.ts +82 -0
  105. package/src/dashboard-server.ts +407 -0
  106. package/src/mcc.ts +474 -0
  107. package/src/mcp/external-registry.ts +73 -0
  108. package/src/mcp/installer.ts +258 -0
  109. package/src/mcp/mcp-config.ts +168 -0
  110. package/src/mcp/registry.ts +89 -0
  111. package/src/proxy/proxy-daemon.ts +184 -0
  112. package/src/proxy/proxy-entry.ts +63 -0
  113. package/src/proxy/proxy-paths.ts +97 -0
  114. package/src/proxy/proxy-server.ts +278 -0
  115. package/src/proxy/upstream-url.ts +38 -0
  116. package/src/shared/logger.ts +140 -0
  117. package/src/shared/provider-preset-catalog.ts +340 -0
  118. package/tsconfig.json +33 -0
  119. package/ui/.prettierrc +9 -0
  120. package/ui/index.html +12 -0
  121. package/ui/package.json +33 -0
  122. package/ui/postcss.config.js +6 -0
  123. package/ui/src/App.tsx +753 -0
  124. package/ui/src/components/ui/button.tsx +48 -0
  125. package/ui/src/components/ui/card.tsx +50 -0
  126. package/ui/src/components/ui/input.tsx +21 -0
  127. package/ui/src/components/ui/label.tsx +20 -0
  128. package/ui/src/components/ui/select.tsx +80 -0
  129. package/ui/src/components/ui/switch.tsx +26 -0
  130. package/ui/src/components/ui/tabs.tsx +52 -0
  131. package/ui/src/index.css +33 -0
  132. package/ui/src/lib/api.ts +185 -0
  133. package/ui/src/lib/utils.ts +6 -0
  134. package/ui/src/main.tsx +10 -0
  135. package/ui/src/vite-env.d.ts +1 -0
  136. package/ui/tailwind.config.js +49 -0
  137. package/ui/tsconfig.json +25 -0
  138. package/ui/vite.config.ts +20 -0
package/src/mcc.ts ADDED
@@ -0,0 +1,474 @@
1
+ /**
2
+ * MCC - My Cloud Code
3
+ *
4
+ * Simplified CLI for multi-provider profile switching.
5
+ * No OAuth, no local proxy - uses provider's direct API.
6
+ */
7
+
8
+ import { spawn } from 'child_process';
9
+ import * as path from 'path';
10
+ import pc from 'picocolors';
11
+ import {
12
+ listProfiles,
13
+ getProfile,
14
+ getProfileApiKey,
15
+ saveProfile,
16
+ deleteProfile,
17
+ setDefaultProfile,
18
+ getDefaultProfile,
19
+ hasProfile,
20
+ type Profile,
21
+ } from './accounts/store';
22
+ import { MCCInstanceManager } from './accounts/instance-manager';
23
+ import { buildProfileEnv } from './core/model-router';
24
+ import {
25
+ installBuiltinServers,
26
+ syncInstanceMcpServers,
27
+ enableInstanceExternalMcp,
28
+ disableInstanceExternalMcp,
29
+ } from './mcp/installer';
30
+ import { BUILTIN_MCP_SERVERS, getAllServers } from './mcp/registry';
31
+ import {
32
+ readExternalMcpRegistry,
33
+ addExternalMcpServer,
34
+ removeExternalMcpServer,
35
+ type ExternalMcpServer,
36
+ } from './mcp/external-registry';
37
+ import { startProxy } from './proxy/proxy-daemon';
38
+ import { log, init, makeSessionId, isDebugEnabled } from './shared/logger';
39
+ import { readMcpConfig, getEnabledWebSearchProviders, getActiveImageAnalysisProvider } from './mcp/mcp-config';
40
+
41
+ const instanceMgr = new MCCInstanceManager();
42
+
43
+ function showHelp(): void {
44
+ console.log(`
45
+ MCC - My Cloud Code
46
+
47
+ Usage: mcc [mcc-options] <profile> [claude-options...]
48
+
49
+ <profile> Profile name to launch (required)
50
+ [claude-options...] All options after profile are passed directly to Claude Code
51
+
52
+ MCC Options:
53
+ -h, --help Show this help
54
+ -- Explicit separator (everything after is for Claude Code)
55
+
56
+ Note: All flags after <profile> are passed directly to Claude Code.
57
+
58
+ Debug logging: MCC_LOG_LEVEL=debug node dist/mcc.js deepseek
59
+
60
+ Examples:
61
+ mcc deepseek Interactive session
62
+ mcc deepseek --print "hello" Non-interactive
63
+ mcc deepseek --print "hello" --verbose Claude Code verbose mode
64
+
65
+ mcc profile add prod --base-url https://api.deepseek.com/anthropic --api-key sk-xxxx --model deepseek-chat
66
+ mcc profile list
67
+ mcc dashboard
68
+ `.trim());
69
+ }
70
+
71
+ async function cmdLaunch(args: string[]): Promise<void> {
72
+ const profileName = args[0];
73
+ if (!profileName) {
74
+ console.error('[!] Usage: mcc <profile> [args...]');
75
+ process.exit(1);
76
+ }
77
+
78
+ const profile = getProfile(profileName);
79
+ if (!profile) {
80
+ console.error(`[!] Profile not found: ${profileName}`);
81
+ console.error('[i] Available profiles:');
82
+ for (const p of listProfiles()) {
83
+ console.error(` - ${p.name} (baseUrl: ${p.baseUrl})`);
84
+ }
85
+ process.exit(1);
86
+ }
87
+
88
+ const proto = profile.protocol || 'anthropic';
89
+ console.log(` ${pc.cyan(pc.bold('●'))} ${pc.bold(profileName)} ${pc.dim('·')} ${pc.cyan(profile.model)} ${pc.dim('·')} ${pc.dim(proto)}`);
90
+
91
+ const apiKey = getProfileApiKey(profileName);
92
+ if (!apiKey) {
93
+ console.error(`[!] No API key found for profile: ${profileName}`);
94
+ process.exit(1);
95
+ }
96
+
97
+ // Read MCP config early so we can display provider info
98
+ const mcpConfig = readMcpConfig();
99
+ const wsProviders = getEnabledWebSearchProviders(mcpConfig);
100
+ const iaProvider = getActiveImageAnalysisProvider(mcpConfig);
101
+
102
+ const instancePath = await instanceMgr.ensureInstance(profileName);
103
+ console.log(` ${pc.dim('instance')} ${pc.dim(instancePath)}`);
104
+
105
+ syncInstanceMcpServers(instancePath, BUILTIN_MCP_SERVERS.map((s) => s.name), profileName);
106
+
107
+ // Initialize logging session
108
+ const sessionId = makeSessionId();
109
+ const mccHome = process.env.MCC_HOME ?? path.join(process.env.HOME ?? process.env.USERPROFILE ?? '~', '.mcc');
110
+ const logDir = path.join(mccHome, 'logs', profileName, sessionId);
111
+ init(sessionId, logDir);
112
+ log.info('MCC', `Session starting: ${profileName} | log: ${logDir}`);
113
+ console.log(` ${pc.dim('session')} ${pc.dim(sessionId)}`);
114
+
115
+ // MCP provider summary
116
+ if (wsProviders.length > 0) {
117
+ console.log(` ${pc.dim('websearch')} ${pc.dim(wsProviders.join(', '))}`);
118
+ } else if (mcpConfig.websearch.enabled) {
119
+ console.log(` ${pc.dim('websearch')} ${pc.dim('none enabled')}`);
120
+ }
121
+ if (mcpConfig.imageAnalysis.enabled) {
122
+ if (iaProvider) {
123
+ console.log(` ${pc.dim('image')} ${pc.dim(iaProvider.id)} ${pc.dim('·')} ${pc.cyan(iaProvider.model)}`);
124
+ } else {
125
+ console.log(` ${pc.dim('image')} ${pc.dim('no active provider')}`);
126
+ }
127
+ }
128
+
129
+ const env = buildProfileEnv(profile, apiKey, instancePath);
130
+ env.MCC_CURRENT_PROFILE = profileName;
131
+ env.MCC_LOG_SESSION_ID = sessionId;
132
+ env.MCC_LOG_DIR = logDir;
133
+
134
+ // Debug: key env vars (no secrets)
135
+ if (isDebugEnabled()) {
136
+ const debugEnvs: [string, string][] = [
137
+ ['ANTHROPIC_BASE_URL', env.ANTHROPIC_BASE_URL],
138
+ ['ANTHROPIC_MODEL', env.ANTHROPIC_MODEL],
139
+ ['CLAUDE_CONFIG_DIR', env.CLAUDE_CONFIG_DIR],
140
+ ['MCC_WEBSEARCH_ENABLED', env.MCC_WEBSEARCH_ENABLED || '0'],
141
+ ['MCC_IMAGE_ANALYSIS_ENABLED', env.MCC_IMAGE_ANALYSIS_ENABLED || '0'],
142
+ ];
143
+ if (env.ANTHROPIC_DEFAULT_OPUS_MODEL !== env.ANTHROPIC_MODEL)
144
+ debugEnvs.push(['ANTHROPIC_DEFAULT_OPUS_MODEL', env.ANTHROPIC_DEFAULT_OPUS_MODEL]);
145
+ if (env.ANTHROPIC_DEFAULT_SONNET_MODEL !== env.ANTHROPIC_MODEL)
146
+ debugEnvs.push(['ANTHROPIC_DEFAULT_SONNET_MODEL', env.ANTHROPIC_DEFAULT_SONNET_MODEL]);
147
+ if (env.ANTHROPIC_DEFAULT_HAIKU_MODEL !== env.ANTHROPIC_MODEL)
148
+ debugEnvs.push(['ANTHROPIC_DEFAULT_HAIKU_MODEL', env.ANTHROPIC_DEFAULT_HAIKU_MODEL]);
149
+
150
+ console.log(` ${pc.dim('---')}`);
151
+ for (const [k, v] of debugEnvs) {
152
+ console.log(` ${pc.dim('env')} ${pc.dim(k)}=${pc.dim(v)}`);
153
+ }
154
+ }
155
+
156
+ // Start translation proxy for OpenAI-compatible profiles
157
+ if (profile.protocol === 'openai') {
158
+ try {
159
+ const proxyInfo = await startProxy(profileName, profile.baseUrl, apiKey, profile.model);
160
+ env.ANTHROPIC_BASE_URL = `http://127.0.0.1:${proxyInfo.port}`;
161
+ env.ANTHROPIC_AUTH_TOKEN = proxyInfo.authToken;
162
+ // MCP image analysis also needs to go through the proxy
163
+ env.MCC_IMAGE_ANALYSIS_RUNTIME_BASE_URL = `http://127.0.0.1:${proxyInfo.port}`;
164
+ env.MCC_IMAGE_ANALYSIS_RUNTIME_API_KEY = proxyInfo.authToken;
165
+ console.log(` ${pc.dim('proxy')} ${pc.cyan(`:${proxyInfo.port}`)}`);
166
+ } catch (e) {
167
+ console.error(`[!] Failed to start translation proxy: ${(e as Error).message}`);
168
+ process.exit(1);
169
+ }
170
+ }
171
+
172
+ console.log(`\n ${pc.green('✓')} ${pc.bold('launching Claude Code')}`);
173
+ const remainingArgs = args.slice(1);
174
+ const child = spawn('claude', remainingArgs, {
175
+ env: { ...process.env, ...env },
176
+ stdio: 'inherit',
177
+ });
178
+
179
+ child.on('exit', (code) => process.exit(code ?? 0));
180
+ }
181
+
182
+ async function cmdProfileAdd(args: string[]): Promise<void> {
183
+ const name = args[0];
184
+ if (!name) {
185
+ console.error('[!] Usage: mcc profile add <name> --base-url <url> --api-key <key> --model <model> [--protocol anthropic|openai] [--opus-model <m>] [--sonnet-model <m>] [--haiku-model <m>]');
186
+ process.exit(1);
187
+ }
188
+
189
+ const getArg = (flag: string) => {
190
+ const idx = args.indexOf(flag);
191
+ return idx !== -1 ? args[idx + 1] : undefined;
192
+ };
193
+
194
+ const baseUrl = getArg('--base-url');
195
+ const apiKey = getArg('--api-key');
196
+ const model = getArg('--model') ?? 'claude-sonnet-4-6';
197
+ const protocol = (getArg('--protocol') as 'anthropic' | 'openai') ?? 'anthropic';
198
+
199
+ if (!baseUrl || !apiKey) {
200
+ console.error('[!] --base-url and --api-key are required');
201
+ process.exit(1);
202
+ }
203
+
204
+ if (protocol !== 'anthropic' && protocol !== 'openai') {
205
+ console.error('[!] --protocol must be "anthropic" or "openai"');
206
+ process.exit(1);
207
+ }
208
+
209
+ const profile: Profile = {
210
+ name,
211
+ baseUrl,
212
+ model,
213
+ opusModel: getArg('--opus-model'),
214
+ sonnetModel: getArg('--sonnet-model'),
215
+ haikuModel: getArg('--haiku-model'),
216
+ protocol,
217
+ createdAt: new Date().toISOString(),
218
+ };
219
+
220
+ saveProfile(profile, apiKey);
221
+ await instanceMgr.ensureInstance(name);
222
+ const instancePath = instanceMgr.getInstancePath(name);
223
+ syncInstanceMcpServers(instancePath, BUILTIN_MCP_SERVERS.map((s) => s.name), name);
224
+
225
+ console.log(`[OK] Profile created: ${name}`);
226
+ console.log(` Base URL: ${baseUrl}`);
227
+ console.log(` Model: ${model}`);
228
+ console.log(` Protocol: ${protocol}`);
229
+ if (profile.opusModel) console.log(` Opus: ${profile.opusModel}`);
230
+ if (profile.sonnetModel) console.log(` Sonnet: ${profile.sonnetModel}`);
231
+ if (profile.haikuModel) console.log(` Haiku: ${profile.haikuModel}`);
232
+ }
233
+
234
+ async function cmdProfileList(): Promise<void> {
235
+ const profiles = listProfiles();
236
+ const defaultProfile = getDefaultProfile();
237
+
238
+ if (profiles.length === 0) {
239
+ console.log('[i] No profiles. Run: mcc profile add <name> --base-url <url> --api-key <key> --model <model>');
240
+ return;
241
+ }
242
+
243
+ console.log('Profiles:');
244
+ for (const p of profiles) {
245
+ const marker = p.name === defaultProfile ? ' (default)' : '';
246
+ console.log(` ${p.name}${marker}`);
247
+ console.log(` Base URL: ${p.baseUrl}`);
248
+ console.log(` Model: ${p.model}`);
249
+ console.log(` Protocol: ${p.protocol || 'anthropic'}`);
250
+ if (p.opusModel) console.log(` Opus: ${p.opusModel}`);
251
+ if (p.sonnetModel) console.log(` Sonnet: ${p.sonnetModel}`);
252
+ if (p.haikuModel) console.log(` Haiku: ${p.haikuModel}`);
253
+ console.log();
254
+ }
255
+ }
256
+
257
+ async function cmdProfileRemove(args: string[]): Promise<void> {
258
+ const name = args[0];
259
+ if (!name) {
260
+ console.error('[!] Usage: mcc profile remove <name>');
261
+ process.exit(1);
262
+ }
263
+ if (!hasProfile(name)) {
264
+ console.error(`[!] Profile not found: ${name}`);
265
+ process.exit(1);
266
+ }
267
+ deleteProfile(name);
268
+ await instanceMgr.deleteInstance(name);
269
+ console.log(`[OK] Profile removed: ${name}`);
270
+ }
271
+
272
+ async function cmdProfileDefault(args: string[]): Promise<void> {
273
+ const name = args[0];
274
+ if (name) {
275
+ if (!hasProfile(name)) {
276
+ console.error(`[!] Profile not found: ${name}`);
277
+ process.exit(1);
278
+ }
279
+ setDefaultProfile(name);
280
+ console.log(`[OK] Default profile set: ${name}`);
281
+ } else {
282
+ const defaultProf = getDefaultProfile();
283
+ console.log(defaultProf ? `Default profile: ${defaultProf}` : '[i] No default profile set');
284
+ }
285
+ }
286
+
287
+ async function cmdMcpList(): Promise<void> {
288
+ const servers = getAllServers();
289
+ console.log('MCP Servers:');
290
+ for (const s of servers) {
291
+ const isBuiltin = 'config' in s;
292
+ const marker = isBuiltin && s.enabledByDefault ? ' (builtin, default)' : isBuiltin ? ' (builtin)' : '';
293
+ console.log(` ${s.name}${marker}`);
294
+ console.log(` ${s.displayName}: ${s.description}`);
295
+ if (!isBuiltin) {
296
+ const ext = s as ExternalMcpServer;
297
+ console.log(` Command: ${ext.command} ${ext.args.join(' ')}`);
298
+ }
299
+ }
300
+ }
301
+
302
+ async function cmdMcpAdd(args: string[]): Promise<void> {
303
+ const getArg = (flag: string) => {
304
+ const idx = args.indexOf(flag);
305
+ return idx !== -1 ? args[idx + 1] : undefined;
306
+ };
307
+ const getBoolArg = (flag: string) => args.includes(flag);
308
+
309
+ const name = getArg('--name');
310
+ const displayName = getArg('--display-name') ?? name ?? '';
311
+ const description = getArg('--description') ?? '';
312
+ const command = getArg('--command') ?? 'uvx';
313
+ const argsStr = getArg('--args') ?? '';
314
+ const argsList = argsStr ? argsStr.split(',').map((s) => s.trim()) : [];
315
+ const providerRef = getArg('--provider-ref');
316
+ const enabledByDefault = getBoolArg('--enabled-by-default');
317
+
318
+ if (!name || !command) {
319
+ console.error('[!] --name and --command are required');
320
+ console.error(' Usage: mcc mcp add --name <id> --command <cmd> --args "<arg1>,<arg2>" [--display-name <name>] [--description <desc>] [--provider-ref <provider>] [--enabled-by-default]');
321
+ process.exit(1);
322
+ }
323
+
324
+ const envVars: Record<string, string> = {};
325
+ if (providerRef) {
326
+ envVars.MINIMAX_API_KEY = `\${MCC_PROVIDER_KEY:${providerRef}}`;
327
+ envVars.MINIMAX_API_HOST = 'https://api.minimaxi.com';
328
+ }
329
+
330
+ const server: ExternalMcpServer = {
331
+ name,
332
+ displayName,
333
+ description,
334
+ command,
335
+ args: argsList,
336
+ envVars,
337
+ enabledByDefault,
338
+ };
339
+ addExternalMcpServer(server);
340
+ console.log(`[OK] External MCP added: ${name}`);
341
+ }
342
+
343
+ async function cmdMcpRemove(args: string[]): Promise<void> {
344
+ const name = args[0];
345
+ if (!name) {
346
+ console.error('[!] Usage: mcc mcp remove <name>');
347
+ process.exit(1);
348
+ }
349
+ const existing = readExternalMcpRegistry().find((s) => s.name === name);
350
+ if (!existing) {
351
+ console.error(`[!] External MCP not found: ${name}`);
352
+ process.exit(1);
353
+ }
354
+ removeExternalMcpServer(name);
355
+ console.log(`[OK] External MCP removed: ${name}`);
356
+ }
357
+
358
+ async function cmdMcpEnable(args: string[]): Promise<void> {
359
+ const name = args[0];
360
+ const profileName = args[1];
361
+ if (!name || !profileName) {
362
+ console.error('[!] Usage: mcc mcp enable <name> <profile>');
363
+ process.exit(1);
364
+ }
365
+ if (!hasProfile(profileName)) {
366
+ console.error(`[!] Profile not found: ${profileName}`);
367
+ process.exit(1);
368
+ }
369
+ const instancePath = instanceMgr.getInstancePath(profileName);
370
+ enableInstanceExternalMcp(instancePath, name);
371
+ syncInstanceMcpServers(instancePath, BUILTIN_MCP_SERVERS.map((s) => s.name), profileName);
372
+ console.log(`[OK] External MCP '${name}' enabled for profile '${profileName}'`);
373
+ }
374
+
375
+ async function cmdMcpDisable(args: string[]): Promise<void> {
376
+ const name = args[0];
377
+ const profileName = args[1];
378
+ if (!name || !profileName) {
379
+ console.error('[!] Usage: mcc mcp disable <name> <profile>');
380
+ process.exit(1);
381
+ }
382
+ if (!hasProfile(profileName)) {
383
+ console.error(`[!] Profile not found: ${profileName}`);
384
+ process.exit(1);
385
+ }
386
+ const instancePath = instanceMgr.getInstancePath(profileName);
387
+ disableInstanceExternalMcp(instancePath, name);
388
+ syncInstanceMcpServers(instancePath, BUILTIN_MCP_SERVERS.map((s) => s.name), profileName);
389
+ console.log(`[OK] External MCP '${name}' disabled for profile '${profileName}'`);
390
+ }
391
+
392
+ async function main(): Promise<void> {
393
+ const rawArgs = process.argv.slice(2);
394
+
395
+ // -h / --help
396
+ if (rawArgs[0] === '-h' || rawArgs[0] === '--help' || rawArgs.length === 0) {
397
+ showHelp();
398
+ return;
399
+ }
400
+
401
+ // Simple parse: first arg is the profile, everything after goes to Claude
402
+ // Use -- to explicitly separate: mcc deepseek -- --print "hello"
403
+ const remaining: string[] = [];
404
+ let i = 0;
405
+ while (i < rawArgs.length) {
406
+ const arg = rawArgs[i];
407
+ if (arg === '--') {
408
+ // Explicit separator: rest goes to Claude (including --)
409
+ remaining.push(...rawArgs.slice(i));
410
+ break;
411
+ } else {
412
+ // First non-flag = profile; everything after it (incl. flags) goes to Claude
413
+ remaining.push(...rawArgs.slice(i));
414
+ break;
415
+ }
416
+ }
417
+
418
+ if (remaining.length === 0) {
419
+ showHelp();
420
+ return;
421
+ }
422
+
423
+ installBuiltinServers();
424
+
425
+ const command = remaining[0];
426
+ const args = remaining.slice(1);
427
+
428
+ switch (command) {
429
+ // mcc <profile> - launch with profile
430
+ case 'profile':
431
+ switch (args[0]) {
432
+ case 'add': await cmdProfileAdd(args.slice(1)); break;
433
+ case 'list': await cmdProfileList(); break;
434
+ case 'remove': await cmdProfileRemove(args.slice(1)); break;
435
+ case 'default': await cmdProfileDefault(args.slice(1)); break;
436
+ default:
437
+ console.error(`[!] Unknown profile command: ${args[0]}`);
438
+ showHelp();
439
+ process.exit(1);
440
+ }
441
+ break;
442
+ case 'dashboard': {
443
+ spawn('node', [path.join(__dirname, 'dashboard-server.js')], {
444
+ env: process.env,
445
+ stdio: 'inherit',
446
+ });
447
+ break;
448
+ }
449
+ case 'mcp': {
450
+ const sub = args[0];
451
+ const subArgs = args.slice(1);
452
+ switch (sub) {
453
+ case 'list': await cmdMcpList(); break;
454
+ case 'add': await cmdMcpAdd(subArgs); break;
455
+ case 'remove': await cmdMcpRemove(subArgs); break;
456
+ case 'enable': await cmdMcpEnable(subArgs); break;
457
+ case 'disable': await cmdMcpDisable(subArgs); break;
458
+ default:
459
+ console.error(`[!] Unknown mcp command: ${sub}`);
460
+ showHelp();
461
+ process.exit(1);
462
+ }
463
+ break;
464
+ }
465
+ default:
466
+ // Treat as profile launch
467
+ await cmdLaunch(rawArgs);
468
+ }
469
+ }
470
+
471
+ main().catch((err) => {
472
+ console.error(`[!] Error: ${err instanceof Error ? err.message : String(err)}`);
473
+ process.exit(1);
474
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * External MCP Server Registry
3
+ *
4
+ * Stores user-added MCP server definitions in ~/.mcc/external-mcp-servers.json.
5
+ * These are MCP servers not bundled with MCC (e.g. MiniMax Token Plan MCP).
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import type { McpRegistryEntry } from './registry';
11
+
12
+ export interface ExternalMcpServer {
13
+ name: string; // unique identifier, e.g. "minimax-plan"
14
+ displayName: string;
15
+ description: string;
16
+ command: string; // e.g. "uvx"
17
+ args: string[]; // e.g. ["minimax-coding-plan-mcp", "-y"]
18
+ envVars: Record<string, string>; // e.g. { MINIMAX_API_KEY: "${MCC_PROVIDER_KEY:minimax}" }
19
+ enabledByDefault: boolean;
20
+ }
21
+
22
+ function getMccHome(): string {
23
+ return process.env.MCC_HOME ?? path.join(process.env.HOME ?? process.env.USERPROFILE ?? '~', '.mcc');
24
+ }
25
+
26
+ export function getExternalMcpRegistryPath(): string {
27
+ return path.join(getMccHome(), 'external-mcp-servers.json');
28
+ }
29
+
30
+ export function readExternalMcpRegistry(): ExternalMcpServer[] {
31
+ const filePath = getExternalMcpRegistryPath();
32
+ if (!fs.existsSync(filePath)) return [];
33
+ try {
34
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
35
+ } catch {
36
+ return [];
37
+ }
38
+ }
39
+
40
+ export function writeExternalMcpRegistry(servers: ExternalMcpServer[]): void {
41
+ const filePath = getExternalMcpRegistryPath();
42
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
43
+ fs.writeFileSync(filePath, JSON.stringify(servers, null, 2) + '\n', {
44
+ encoding: 'utf8',
45
+ mode: 0o600,
46
+ });
47
+ }
48
+
49
+ export function getExternalMcpServer(name: string): ExternalMcpServer | undefined {
50
+ return readExternalMcpRegistry().find((s) => s.name === name);
51
+ }
52
+
53
+ export function addExternalMcpServer(server: ExternalMcpServer): void {
54
+ const servers = readExternalMcpRegistry();
55
+ const idx = servers.findIndex((s) => s.name === server.name);
56
+ if (idx >= 0) {
57
+ servers[idx] = server;
58
+ } else {
59
+ servers.push(server);
60
+ }
61
+ writeExternalMcpRegistry(servers);
62
+ }
63
+
64
+ export function removeExternalMcpServer(name: string): void {
65
+ const servers = readExternalMcpRegistry().filter((s) => s.name !== name);
66
+ writeExternalMcpRegistry(servers);
67
+ }
68
+
69
+ export function getAllServers(): Array<McpRegistryEntry | ExternalMcpServer> {
70
+ // Imported here to avoid circular dependency
71
+ const { BUILTIN_MCP_SERVERS } = require('./registry');
72
+ return [...BUILTIN_MCP_SERVERS, ...readExternalMcpRegistry()];
73
+ }