@hileeon/mcc 0.1.4 → 0.1.6

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 (74) hide show
  1. package/dist/accounts/store.d.ts +1 -0
  2. package/dist/accounts/store.d.ts.map +1 -1
  3. package/dist/accounts/store.js.map +1 -1
  4. package/dist/dashboard-server.js +5 -4
  5. package/dist/dashboard-server.js.map +1 -1
  6. package/dist/mcc.js +8 -2
  7. package/dist/mcc.js.map +1 -1
  8. package/dist/proxy/proxy-daemon.d.ts +1 -1
  9. package/dist/proxy/proxy-daemon.d.ts.map +1 -1
  10. package/dist/proxy/proxy-daemon.js +2 -1
  11. package/dist/proxy/proxy-daemon.js.map +1 -1
  12. package/dist/proxy/proxy-entry.js +6 -1
  13. package/dist/proxy/proxy-entry.js.map +1 -1
  14. package/dist/proxy/proxy-server.d.ts +1 -0
  15. package/dist/proxy/proxy-server.d.ts.map +1 -1
  16. package/dist/proxy/proxy-server.js +1 -1
  17. package/dist/proxy/proxy-server.js.map +1 -1
  18. package/dist/proxy/upstream-url.d.ts +1 -1
  19. package/dist/proxy/upstream-url.d.ts.map +1 -1
  20. package/dist/proxy/upstream-url.js +20 -2
  21. package/dist/proxy/upstream-url.js.map +1 -1
  22. package/dist/shared/provider-preset-catalog.d.ts +3 -1
  23. package/dist/shared/provider-preset-catalog.d.ts.map +1 -1
  24. package/dist/shared/provider-preset-catalog.js +16 -0
  25. package/dist/shared/provider-preset-catalog.js.map +1 -1
  26. package/{ui/dist → dist/ui}/index.html +2 -2
  27. package/package.json +6 -2
  28. package/.claude/CLAUDE.md +0 -204
  29. package/.claude/agents/.gitkeep +0 -0
  30. package/.claude/settings.json +0 -9
  31. package/.claude/skills/.gitkeep +0 -0
  32. package/docs/decisions.md +0 -33
  33. package/docs/lessons.md +0 -8
  34. package/docs/product.md +0 -37
  35. package/src/accounts/instance-manager.ts +0 -58
  36. package/src/accounts/shared-manager.ts +0 -154
  37. package/src/accounts/store.ts +0 -111
  38. package/src/core/model-router.ts +0 -82
  39. package/src/dashboard-server.ts +0 -427
  40. package/src/mcc.ts +0 -482
  41. package/src/mcp/external-registry.ts +0 -73
  42. package/src/mcp/installer.ts +0 -258
  43. package/src/mcp/mcp-config.ts +0 -168
  44. package/src/mcp/registry.ts +0 -89
  45. package/src/proxy/proxy-daemon.ts +0 -184
  46. package/src/proxy/proxy-entry.ts +0 -63
  47. package/src/proxy/proxy-paths.ts +0 -97
  48. package/src/proxy/proxy-server.ts +0 -278
  49. package/src/proxy/upstream-url.ts +0 -38
  50. package/src/shared/logger.ts +0 -140
  51. package/src/shared/provider-preset-catalog.ts +0 -340
  52. package/tsconfig.json +0 -33
  53. package/ui/.prettierrc +0 -9
  54. package/ui/index.html +0 -12
  55. package/ui/package.json +0 -33
  56. package/ui/postcss.config.js +0 -6
  57. package/ui/src/App.tsx +0 -753
  58. package/ui/src/components/ui/button.tsx +0 -48
  59. package/ui/src/components/ui/card.tsx +0 -50
  60. package/ui/src/components/ui/input.tsx +0 -21
  61. package/ui/src/components/ui/label.tsx +0 -20
  62. package/ui/src/components/ui/select.tsx +0 -80
  63. package/ui/src/components/ui/switch.tsx +0 -26
  64. package/ui/src/components/ui/tabs.tsx +0 -52
  65. package/ui/src/index.css +0 -33
  66. package/ui/src/lib/api.ts +0 -185
  67. package/ui/src/lib/utils.ts +0 -6
  68. package/ui/src/main.tsx +0 -10
  69. package/ui/src/vite-env.d.ts +0 -1
  70. package/ui/tailwind.config.js +0 -49
  71. package/ui/tsconfig.json +0 -25
  72. package/ui/vite.config.ts +0 -20
  73. /package/{ui/dist → dist/ui}/assets/index-B16lhKZ6.js +0 -0
  74. /package/{ui/dist → dist/ui}/assets/index-jEfiB6-h.css +0 -0
@@ -1,111 +0,0 @@
1
- /**
2
- * Profile Store - File-based profile management
3
- *
4
- * Storage structure:
5
- * ~/.mcc/
6
- * ├── profiles.json # Profile metadata + tiered model config
7
- * └── profiles/
8
- * └── <profile-name>/
9
- * └── .key # API key (plain text)
10
- */
11
-
12
- import * as fs from 'fs';
13
- import * as path from 'path';
14
-
15
- function getMccHome(): string {
16
- return process.env.MCC_HOME ?? path.join(process.env.HOME ?? process.env.USERPROFILE ?? '~', '.mcc');
17
- }
18
-
19
- function getProfilesDir(): string {
20
- return path.join(getMccHome(), 'profiles');
21
- }
22
-
23
- function getProfilesJsonPath(): string {
24
- return path.join(getMccHome(), 'profiles.json');
25
- }
26
-
27
- export interface Profile {
28
- name: string;
29
- baseUrl: string;
30
- model: string; // Default model
31
- opusModel?: string; // Tier 1 (Opus)
32
- sonnetModel?: string; // Tier 2 (Sonnet)
33
- haikuModel?: string; // Tier 3 (Haiku / small-fast)
34
- protocol?: 'anthropic' | 'openai'; // 'anthropic' = direct, 'openai' = needs translation proxy
35
- createdAt: string;
36
- lastUsedAt?: string;
37
- }
38
-
39
- interface ProfilesJson {
40
- version: number;
41
- profiles: Record<string, Profile>;
42
- defaultProfile?: string;
43
- }
44
-
45
- function readProfilesJson(): ProfilesJson {
46
- const jsonPath = getProfilesJsonPath();
47
- if (!fs.existsSync(jsonPath)) {
48
- return { version: 1, profiles: {} };
49
- }
50
- try {
51
- return JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
52
- } catch {
53
- return { version: 1, profiles: {} };
54
- }
55
- }
56
-
57
- function writeProfilesJson(data: ProfilesJson): void {
58
- const jsonPath = getProfilesJsonPath();
59
- fs.mkdirSync(path.dirname(jsonPath), { recursive: true, mode: 0o700 });
60
- fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
61
- }
62
-
63
- export function listProfiles(): Profile[] {
64
- return Object.values(readProfilesJson().profiles);
65
- }
66
-
67
- export function getProfile(name: string): Profile | undefined {
68
- return readProfilesJson().profiles[name];
69
- }
70
-
71
- export function getDefaultProfile(): string | undefined {
72
- return readProfilesJson().defaultProfile;
73
- }
74
-
75
- export function saveProfile(profile: Profile, apiKey: string): void {
76
- const data = readProfilesJson();
77
- const profileDir = path.join(getProfilesDir(), profile.name);
78
- fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
79
- fs.writeFileSync(path.join(profileDir, '.key'), apiKey, { encoding: 'utf8', mode: 0o600 });
80
- data.profiles[profile.name] = { ...profile, lastUsedAt: new Date().toISOString() };
81
- writeProfilesJson(data);
82
- }
83
-
84
- export function getProfileApiKey(name: string): string | undefined {
85
- const keyPath = path.join(getProfilesDir(), name, '.key');
86
- if (!fs.existsSync(keyPath)) return undefined;
87
- return fs.readFileSync(keyPath, 'utf8').trim();
88
- }
89
-
90
- export function deleteProfile(name: string): void {
91
- const data = readProfilesJson();
92
- if (!data.profiles[name]) return;
93
- delete data.profiles[name];
94
- if (data.defaultProfile === name) {
95
- data.defaultProfile = Object.keys(data.profiles)[0];
96
- }
97
- writeProfilesJson(data);
98
- const profileDir = path.join(getProfilesDir(), name);
99
- if (fs.existsSync(profileDir)) fs.rmSync(profileDir, { recursive: true, force: true });
100
- }
101
-
102
- export function setDefaultProfile(name: string): void {
103
- const data = readProfilesJson();
104
- if (!data.profiles[name]) throw new Error(`Profile not found: ${name}`);
105
- data.defaultProfile = name;
106
- writeProfilesJson(data);
107
- }
108
-
109
- export function hasProfile(name: string): boolean {
110
- return name in readProfilesJson().profiles;
111
- }
@@ -1,82 +0,0 @@
1
- /**
2
- * Model Router - Builds Claude Code env vars from a Profile
3
- *
4
- * Mirrors CCS's tiered model mapping:
5
- * ANTHROPIC_MODEL = Default model
6
- * ANTHROPIC_DEFAULT_OPUS_MODEL = Opus tier
7
- * ANTHROPIC_DEFAULT_SONNET_MODEL = Sonnet tier
8
- * ANTHROPIC_DEFAULT_HAIKU_MODEL = Haiku tier
9
- * ANTHROPIC_SMALL_FAST_MODEL = Alias for Haiku tier
10
- */
11
-
12
- import type { Profile } from '../accounts/store';
13
- import { readMcpConfig, getActiveImageAnalysisProvider, getEnabledWebSearchProviders } from '../mcp/mcp-config';
14
-
15
- export interface ProfileEnv {
16
- ANTHROPIC_BASE_URL: string;
17
- ANTHROPIC_AUTH_TOKEN: string;
18
- ANTHROPIC_MODEL: string;
19
- ANTHROPIC_DEFAULT_OPUS_MODEL: string;
20
- ANTHROPIC_DEFAULT_SONNET_MODEL: string;
21
- ANTHROPIC_DEFAULT_HAIKU_MODEL: string;
22
- ANTHROPIC_SMALL_FAST_MODEL: string;
23
- DISABLE_TELEMETRY: '1';
24
- DISABLE_COST_WARNINGS: '1';
25
- CLAUDE_CONFIG_DIR: string;
26
- [key: string]: string; // Allow MCP env vars
27
- }
28
-
29
- /**
30
- * Build env vars for launching Claude Code with a profile.
31
- * Falls back to profile.model for any missing tier.
32
- */
33
- export function buildProfileEnv(profile: Profile, apiKey: string, claudeConfigDir: string): ProfileEnv {
34
- const model = profile.model;
35
-
36
- // Read MCP config for provider settings
37
- const mcpConfig = readMcpConfig();
38
- const wsProviders = getEnabledWebSearchProviders(mcpConfig);
39
- const iaProvider = getActiveImageAnalysisProvider(mcpConfig);
40
-
41
- const env: Record<string, string> = {
42
- ANTHROPIC_BASE_URL: profile.baseUrl,
43
- ANTHROPIC_AUTH_TOKEN: apiKey,
44
- ANTHROPIC_MODEL: model,
45
- ANTHROPIC_DEFAULT_OPUS_MODEL: profile.opusModel ?? model,
46
- ANTHROPIC_DEFAULT_SONNET_MODEL: profile.sonnetModel ?? model,
47
- ANTHROPIC_DEFAULT_HAIKU_MODEL: profile.haikuModel ?? model,
48
- ANTHROPIC_SMALL_FAST_MODEL: profile.haikuModel ?? model,
49
- DISABLE_TELEMETRY: '1',
50
- DISABLE_COST_WARNINGS: '1',
51
- CLAUDE_CONFIG_DIR: claudeConfigDir,
52
- };
53
-
54
- // MCP: WebSearch
55
- env.MCC_WEBSEARCH_ENABLED = mcpConfig.websearch.enabled ? '1' : '0';
56
- for (const p of wsProviders) {
57
- env[`MCC_WEBSEARCH_${p.toUpperCase()}`] = '1';
58
- // Set API key env vars for providers that need them
59
- const providerConfig = mcpConfig.websearch.providers[p];
60
- if (providerConfig?.apiKey) {
61
- const keyEnvMap: Record<string, string> = {
62
- exa: 'MCC_WEBSEARCH_EXA_API_KEY',
63
- tavily: 'MCC_WEBSEARCH_TAVILY_API_KEY',
64
- brave: 'MCC_WEBSEARCH_BRAVE_API_KEY',
65
- };
66
- if (keyEnvMap[p]) {
67
- env[keyEnvMap[p]] = providerConfig.apiKey;
68
- }
69
- }
70
- }
71
-
72
- // MCP: Image Analysis
73
- if (mcpConfig.imageAnalysis.enabled && iaProvider) {
74
- env.MCC_IMAGE_ANALYSIS_ENABLED = '1';
75
- env.MCC_IMAGE_ANALYSIS_RUNTIME_BASE_URL = iaProvider.baseUrl;
76
- env.MCC_IMAGE_ANALYSIS_RUNTIME_API_KEY = iaProvider.apiKey;
77
- env.MCC_IMAGE_ANALYSIS_MODEL = iaProvider.model;
78
- env.MCC_IMAGE_ANALYSIS_FORMAT = iaProvider.format;
79
- }
80
-
81
- return env as unknown as ProfileEnv;
82
- }
@@ -1,427 +0,0 @@
1
- /**
2
- * Dashboard Server - Express API + static file server
3
- */
4
-
5
- import express from 'express';
6
- import cors from 'cors';
7
- import * as path from 'path';
8
- import * as fs from 'fs';
9
- import { spawn } from 'child_process';
10
- import type { Profile } from './accounts/store';
11
- import { BUILTIN_MCP_SERVERS, getAllServers, type McpRegistryEntry } from './mcp/registry';
12
- import {
13
- readMcpConfig,
14
- writeMcpConfig,
15
- getProviderPresets,
16
- type McpConfig,
17
- } from './mcp/mcp-config';
18
- import {
19
- readExternalMcpRegistry,
20
- addExternalMcpServer,
21
- removeExternalMcpServer,
22
- type ExternalMcpServer,
23
- } from './mcp/external-registry';
24
- import {
25
- enableInstanceExternalMcp,
26
- disableInstanceExternalMcp,
27
- readInstanceExternalEnabled,
28
- } from './mcp/installer';
29
- import { MCCInstanceManager } from './accounts/instance-manager';
30
-
31
- const PORT = 3000;
32
- const DIST_DIR = path.join(__dirname, '..', 'ui', 'dist');
33
-
34
- async function importModule<T>(modulePath: string, fn: string): Promise<T> {
35
- const mod = await import(modulePath);
36
- return (mod as Record<string, T>)[fn] as T;
37
- }
38
-
39
- async function listProfiles() {
40
- const fn = await importModule<() => Profile[]>('./accounts/store', 'listProfiles');
41
- return fn();
42
- }
43
-
44
- async function saveProfile(profile: Profile, apiKey: string) {
45
- const fn = await importModule<(profile: Profile, apiKey: string) => void>(
46
- './accounts/store',
47
- 'saveProfile'
48
- );
49
- return fn(profile, apiKey);
50
- }
51
-
52
- async function deleteProfile(name: string) {
53
- const fn = await importModule<(name: string) => void>('./accounts/store', 'deleteProfile');
54
- return fn(name);
55
- }
56
-
57
- async function setDefaultProfile(name: string) {
58
- const fn = await importModule<(name: string) => void>('./accounts/store', 'setDefaultProfile');
59
- return fn(name);
60
- }
61
-
62
- async function getDefaultProfile() {
63
- const fn = await importModule<() => string | undefined>('./accounts/store', 'getDefaultProfile');
64
- return fn();
65
- }
66
-
67
- function openBrowser(url: string) {
68
- const isWindows = process.platform === 'win32';
69
- if (isWindows) {
70
- spawn('cmd', ['/c', 'start', '""', url], { detached: true, stdio: 'ignore' }).unref();
71
- } else {
72
- spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
73
- }
74
- }
75
-
76
- async function main() {
77
- const app = express();
78
-
79
- app.use(cors());
80
- app.use(express.json());
81
-
82
- if (fs.existsSync(DIST_DIR)) {
83
- app.use(express.static(DIST_DIR));
84
- }
85
-
86
- // GET /api/profiles
87
- app.get('/api/profiles', async (_req, res) => {
88
- try {
89
- res.json(await listProfiles());
90
- } catch (e) {
91
- res.status(500).json({ error: (e as Error).message });
92
- }
93
- });
94
-
95
- // POST /api/profiles
96
- app.post('/api/profiles', async (req, res) => {
97
- try {
98
- const { name, baseUrl, apiKey, model, opusModel, sonnetModel, haikuModel, protocol } = req.body as {
99
- name: string;
100
- baseUrl: string;
101
- apiKey: string;
102
- model: string;
103
- opusModel?: string;
104
- sonnetModel?: string;
105
- haikuModel?: string;
106
- protocol?: 'anthropic' | 'openai';
107
- };
108
- if (!name || !baseUrl || !apiKey || !model) {
109
- res.status(400).json({ error: 'Missing required fields' });
110
- return;
111
- }
112
- const profile: Profile = { name, baseUrl, model, opusModel, sonnetModel, haikuModel, protocol: protocol || 'anthropic', createdAt: new Date().toISOString() };
113
- await saveProfile(profile, apiKey);
114
- console.log(`[i] Profile created: ${name} (model: ${model}, protocol: ${protocol || 'anthropic'})`);
115
- res.json({ ok: true });
116
- } catch (e) {
117
- res.status(500).json({ error: (e as Error).message });
118
- }
119
- });
120
-
121
- // PUT /api/profiles/:name — update profile
122
- app.put('/api/profiles/:name', async (req, res) => {
123
- try {
124
- const { baseUrl, apiKey, model, opusModel, sonnetModel, haikuModel, protocol } = req.body as {
125
- baseUrl?: string;
126
- apiKey?: string;
127
- model?: string;
128
- opusModel?: string;
129
- sonnetModel?: string;
130
- haikuModel?: string;
131
- protocol?: 'anthropic' | 'openai';
132
- };
133
- const profileName = req.params.name;
134
- const getProfileApiKey = await importModule<(name: string) => string | undefined>(
135
- './accounts/store',
136
- 'getProfileApiKey'
137
- );
138
- const existingKey = getProfileApiKey(profileName);
139
- const profiles = await listProfiles();
140
- const existing = profiles.find((p) => p.name === profileName);
141
- if (!existing) {
142
- res.status(404).json({ error: 'Profile not found' });
143
- return;
144
- }
145
- const updated: Profile = {
146
- ...existing,
147
- baseUrl: baseUrl ?? existing.baseUrl,
148
- model: model ?? existing.model,
149
- opusModel: opusModel !== undefined ? (opusModel || undefined) : existing.opusModel,
150
- sonnetModel: sonnetModel !== undefined ? (sonnetModel || undefined) : existing.sonnetModel,
151
- haikuModel: haikuModel !== undefined ? (haikuModel || undefined) : existing.haikuModel,
152
- protocol: protocol ?? existing.protocol,
153
- };
154
- // Only update API key if a new one is provided
155
- await saveProfile(updated, apiKey ?? existingKey ?? '');
156
- console.log(`[i] Profile updated: ${profileName} (model: ${updated.model}, protocol: ${updated.protocol || 'anthropic'})`);
157
- res.json({ ok: true });
158
- } catch (e) {
159
- res.status(500).json({ error: (e as Error).message });
160
- }
161
- });
162
-
163
- // DELETE /api/profiles/:name
164
- app.delete('/api/profiles/:name', async (req, res) => {
165
- try {
166
- const name = req.params.name;
167
- await deleteProfile(name);
168
- console.log(`[i] Profile deleted: ${name}`);
169
- res.json({ ok: true });
170
- } catch (e) {
171
- res.status(500).json({ error: (e as Error).message });
172
- }
173
- });
174
-
175
- // PUT /api/profiles/:name/default
176
- app.put('/api/profiles/:name/default', async (req, res) => {
177
- try {
178
- await setDefaultProfile(req.params.name);
179
- res.json({ ok: true });
180
- } catch (e) {
181
- res.status(500).json({ error: (e as Error).message });
182
- }
183
- });
184
-
185
- // GET /api/ping - connection health check
186
- app.get('/api/ping', (_req, res) => {
187
- res.json({ ok: true });
188
- });
189
-
190
- // GET /api/status
191
- app.get('/api/status', async (_req, res) => {
192
- try {
193
- const defaultProfile = await getDefaultProfile();
194
- let currentProfile = defaultProfile;
195
- res.json({ currentProfile });
196
- } catch (e) {
197
- res.status(500).json({ error: (e as Error).message });
198
- }
199
- });
200
-
201
- // GET /api/mcp
202
- app.get('/api/mcp', (_req, res) => {
203
- const servers = BUILTIN_MCP_SERVERS.map((s) => ({
204
- name: s.name,
205
- displayName: s.displayName,
206
- description: s.description,
207
- enabled: s.enabledByDefault,
208
- }));
209
- res.json(servers);
210
- });
211
-
212
- // PUT /api/mcp/:name/:action
213
- app.put('/api/mcp/:name/:action', (req, res) => {
214
- const { name, action } = req.params;
215
- if (action !== 'enable' && action !== 'disable') {
216
- res.status(400).json({ error: `Invalid action: ${action}` });
217
- return;
218
- }
219
- // Built-in servers: no per-instance tracking yet
220
- const builtin = BUILTIN_MCP_SERVERS.find((s) => s.name === name);
221
- if (builtin) {
222
- res.json({ ok: true });
223
- return;
224
- }
225
- // External servers: require instance param
226
- const instanceName = req.query.instance as string | undefined;
227
- if (!instanceName) {
228
- res.status(400).json({ error: 'instance query param required for external MCPs' });
229
- return;
230
- }
231
- const instanceMgr = new MCCInstanceManager();
232
- const instancePath = instanceMgr.getInstancePath(instanceName);
233
- if (action === 'enable') {
234
- enableInstanceExternalMcp(instancePath, name);
235
- } else {
236
- disableInstanceExternalMcp(instancePath, name);
237
- }
238
- res.json({ ok: true });
239
- });
240
-
241
- // GET /api/mcp/all - all servers (built-in + external) with enabled state
242
- app.get('/api/mcp/all', (req, res) => {
243
- try {
244
- const instanceName = req.query.instance as string | undefined;
245
- const instanceMgr = new MCCInstanceManager();
246
- let instanceExternalEnabled: string[] = [];
247
- if (instanceName) {
248
- const instancePath = instanceMgr.getInstancePath(instanceName);
249
- instanceExternalEnabled = readInstanceExternalEnabled(instancePath);
250
- }
251
- const servers = getAllServers();
252
- const result = servers.map((s: McpRegistryEntry | ExternalMcpServer) => {
253
- const isBuiltin = 'config' in s;
254
- const isEnabled = isBuiltin
255
- ? s.enabledByDefault
256
- : instanceExternalEnabled.includes(s.name);
257
- return {
258
- name: s.name,
259
- displayName: s.displayName,
260
- description: s.description,
261
- builtin: isBuiltin,
262
- enabledByDefault: isBuiltin ? s.enabledByDefault : (s as ExternalMcpServer).enabledByDefault,
263
- enabled: isEnabled,
264
- };
265
- });
266
- res.json(result);
267
- } catch (e) {
268
- res.status(500).json({ error: (e as Error).message });
269
- }
270
- });
271
-
272
- // GET /api/mcp/external - list external MCP servers
273
- app.get('/api/mcp/external', (_req, res) => {
274
- try {
275
- res.json(readExternalMcpRegistry());
276
- } catch (e) {
277
- res.status(500).json({ error: (e as Error).message });
278
- }
279
- });
280
-
281
- // POST /api/mcp/external - add external MCP server
282
- app.post('/api/mcp/external', (req, res) => {
283
- try {
284
- const server = req.body as ExternalMcpServer;
285
- if (!server.name || !server.command || !server.args) {
286
- res.status(400).json({ error: 'name, command, and args are required' });
287
- return;
288
- }
289
- addExternalMcpServer(server);
290
- res.json({ ok: true });
291
- } catch (e) {
292
- res.status(500).json({ error: (e as Error).message });
293
- }
294
- });
295
-
296
- // DELETE /api/mcp/external/:name - remove external MCP server
297
- app.delete('/api/mcp/external/:name', (req, res) => {
298
- try {
299
- removeExternalMcpServer(req.params.name);
300
- res.json({ ok: true });
301
- } catch (e) {
302
- res.status(500).json({ error: (e as Error).message });
303
- }
304
- });
305
-
306
- // GET /api/mcp-config
307
- app.get('/api/mcp-config', (_req, res) => {
308
- try {
309
- res.json(readMcpConfig());
310
- } catch (e) {
311
- res.status(500).json({ error: (e as Error).message });
312
- }
313
- });
314
-
315
- // PUT /api/mcp-config
316
- app.put('/api/mcp-config', (req, res) => {
317
- try {
318
- const newConfig = req.body as McpConfig;
319
- if (!newConfig || !newConfig.websearch || !newConfig.imageAnalysis) {
320
- res.status(400).json({ error: 'Invalid MCP config' });
321
- return;
322
- }
323
-
324
- const oldConfig = readMcpConfig();
325
- const changes: string[] = [];
326
-
327
- // Section-level toggles
328
- if (oldConfig.websearch.enabled !== newConfig.websearch.enabled) {
329
- changes.push(`websearch ${newConfig.websearch.enabled ? 'enabled' : 'disabled'}`);
330
- }
331
- if (oldConfig.imageAnalysis.enabled !== newConfig.imageAnalysis.enabled) {
332
- changes.push(`imageAnalysis ${newConfig.imageAnalysis.enabled ? 'enabled' : 'disabled'}`);
333
- }
334
-
335
- // WebSearch provider changes
336
- for (const [id, np] of Object.entries(newConfig.websearch.providers)) {
337
- const op = oldConfig.websearch.providers[id];
338
- if (!op) continue;
339
- if (op.enabled !== np.enabled) {
340
- changes.push(`websearch.${id} ${np.enabled ? 'on' : 'off'}`);
341
- }
342
- if (op.apiKey !== np.apiKey) {
343
- changes.push(np.apiKey ? `websearch.${id} apiKey updated` : `websearch.${id} apiKey cleared`);
344
- }
345
- }
346
-
347
- // ImageAnalysis provider changes
348
- for (const [id, np] of Object.entries(newConfig.imageAnalysis.providers)) {
349
- const op = oldConfig.imageAnalysis.providers[id];
350
- if (!op) continue;
351
- if (op.enabled !== np.enabled) {
352
- changes.push(`imageAnalysis.${id} ${np.enabled ? 'on' : 'off'}`);
353
- }
354
- if (op.apiKey !== np.apiKey) {
355
- changes.push(np.apiKey ? `imageAnalysis.${id} apiKey updated` : `imageAnalysis.${id} apiKey cleared`);
356
- }
357
- if (op.model !== np.model) {
358
- changes.push(`imageAnalysis.${id} model=${np.model}`);
359
- }
360
- if (op.baseUrl !== np.baseUrl) {
361
- changes.push(`imageAnalysis.${id} endpoint updated`);
362
- }
363
- }
364
-
365
- writeMcpConfig(newConfig);
366
-
367
- if (changes.length > 0) {
368
- console.log(`[i] MCP config updated: ${changes.join('; ')}`);
369
- } else {
370
- console.log('[i] MCP config saved (no changes detected)');
371
- }
372
-
373
- res.json({ ok: true });
374
- } catch (e) {
375
- res.status(500).json({ error: (e as Error).message });
376
- }
377
- });
378
-
379
- // GET /api/mcp-config/presets
380
- app.get('/api/mcp-config/presets', (_req, res) => {
381
- res.json(getProviderPresets());
382
- });
383
-
384
- app.get('*', (_req, res) => {
385
- const indexPath = path.join(DIST_DIR, 'index.html');
386
- if (fs.existsSync(indexPath)) {
387
- res.sendFile(indexPath);
388
- } else {
389
- res.status(404).send('Dashboard not built. Run: npm run build:ui');
390
- }
391
- });
392
-
393
- const startServer = async (): Promise<void> => {
394
- let port = PORT;
395
- while (port < PORT + 10) {
396
- try {
397
- await new Promise<void>((resolve, reject) => {
398
- const server = app.listen(port, () => resolve());
399
- server.on('error', reject);
400
- });
401
- console.log(`[OK] MCC Dashboard: http://localhost:${port}`);
402
- openBrowser(`http://localhost:${port}`);
403
- return;
404
- } catch (err: unknown) {
405
- if ((err as NodeJS.ErrnoException).code === 'EADDRINUSE') {
406
- console.log(`[!] Port ${port} in use, trying ${port + 1}...`);
407
- port++;
408
- } else {
409
- throw err;
410
- }
411
- }
412
- }
413
- console.error(`[!] Could not find an available port in range ${PORT}–${PORT + 9}`);
414
- };
415
-
416
- startServer();
417
- }
418
-
419
- process.on('SIGINT', () => {
420
- console.log('\n[i] Dashboard shutting down...');
421
- process.exit(0);
422
- });
423
-
424
- main().catch((err) => {
425
- console.error(`[!] Dashboard server error: ${err.message}`);
426
- process.exit(1);
427
- });