@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.
- package/.claude/CLAUDE.md +204 -0
- package/.claude/agents/.gitkeep +0 -0
- package/.claude/settings.json +9 -0
- package/.claude/skills/.gitkeep +0 -0
- package/README.md +127 -0
- package/dist/accounts/instance-manager.d.ts +11 -0
- package/dist/accounts/instance-manager.d.ts.map +1 -0
- package/dist/accounts/instance-manager.js +89 -0
- package/dist/accounts/instance-manager.js.map +1 -0
- package/dist/accounts/shared-manager.d.ts +25 -0
- package/dist/accounts/shared-manager.d.ts.map +1 -0
- package/dist/accounts/shared-manager.js +186 -0
- package/dist/accounts/shared-manager.js.map +1 -0
- package/dist/accounts/store.d.ts +30 -0
- package/dist/accounts/store.d.ts.map +1 -0
- package/dist/accounts/store.js +128 -0
- package/dist/accounts/store.js.map +1 -0
- package/dist/core/model-router.d.ts +30 -0
- package/dist/core/model-router.d.ts.map +1 -0
- package/dist/core/model-router.js +64 -0
- package/dist/core/model-router.js.map +1 -0
- package/dist/dashboard-server.d.ts +5 -0
- package/dist/dashboard-server.d.ts.map +1 -0
- package/dist/dashboard-server.js +387 -0
- package/dist/dashboard-server.js.map +1 -0
- package/dist/mcc.d.ts +8 -0
- package/dist/mcc.d.ts.map +1 -0
- package/dist/mcc.js +474 -0
- package/dist/mcc.js.map +1 -0
- package/dist/mcp/external-registry.d.ts +24 -0
- package/dist/mcp/external-registry.d.ts.map +1 -0
- package/dist/mcp/external-registry.js +99 -0
- package/dist/mcp/external-registry.js.map +1 -0
- package/dist/mcp/installer.d.ts +31 -0
- package/dist/mcp/installer.d.ts.map +1 -0
- package/dist/mcp/installer.js +273 -0
- package/dist/mcp/installer.js.map +1 -0
- package/dist/mcp/mcp-config.d.ts +86 -0
- package/dist/mcp/mcp-config.d.ts.map +1 -0
- package/dist/mcp/mcp-config.js +178 -0
- package/dist/mcp/mcp-config.js.map +1 -0
- package/dist/mcp/registry.d.ts +23 -0
- package/dist/mcp/registry.d.ts.map +1 -0
- package/dist/mcp/registry.js +100 -0
- package/dist/mcp/registry.js.map +1 -0
- package/dist/proxy/proxy-daemon.d.ts +27 -0
- package/dist/proxy/proxy-daemon.d.ts.map +1 -0
- package/dist/proxy/proxy-daemon.js +192 -0
- package/dist/proxy/proxy-daemon.js.map +1 -0
- package/dist/proxy/proxy-entry.d.ts +11 -0
- package/dist/proxy/proxy-entry.d.ts.map +1 -0
- package/dist/proxy/proxy-entry.js +74 -0
- package/dist/proxy/proxy-entry.js.map +1 -0
- package/dist/proxy/proxy-paths.d.ts +27 -0
- package/dist/proxy/proxy-paths.d.ts.map +1 -0
- package/dist/proxy/proxy-paths.js +125 -0
- package/dist/proxy/proxy-paths.js.map +1 -0
- package/dist/proxy/proxy-server.d.ts +20 -0
- package/dist/proxy/proxy-server.d.ts.map +1 -0
- package/dist/proxy/proxy-server.js +280 -0
- package/dist/proxy/proxy-server.js.map +1 -0
- package/dist/proxy/upstream-url.d.ts +7 -0
- package/dist/proxy/upstream-url.d.ts.map +1 -0
- package/dist/proxy/upstream-url.js +38 -0
- package/dist/proxy/upstream-url.js.map +1 -0
- package/dist/shared/logger.d.ts +23 -0
- package/dist/shared/logger.d.ts.map +1 -0
- package/dist/shared/logger.js +184 -0
- package/dist/shared/logger.js.map +1 -0
- package/dist/shared/provider-preset-catalog.d.ts +41 -0
- package/dist/shared/provider-preset-catalog.d.ts.map +1 -0
- package/dist/shared/provider-preset-catalog.js +299 -0
- package/dist/shared/provider-preset-catalog.js.map +1 -0
- package/docs/decisions.md +33 -0
- package/docs/lessons.md +8 -0
- package/docs/product.md +37 -0
- package/lib/mcp/mcc-image-analysis-server.cjs +454 -0
- package/lib/mcp/mcc-websearch-server.cjs +339 -0
- package/lib/mcp-hooks/image-analysis-runtime.cjs +510 -0
- package/lib/mcp-hooks/image-analyzer-transformer.cjs +526 -0
- package/lib/mcp-hooks/websearch-transformer.cjs +1421 -0
- package/lib/proxy/config/config-loader-facade.js +24 -0
- package/lib/proxy/glmt/delta-accumulator.js +363 -0
- package/lib/proxy/glmt/glmt-transformer.js +204 -0
- package/lib/proxy/glmt/index.js +41 -0
- package/lib/proxy/glmt/locale-enforcer.js +69 -0
- package/lib/proxy/glmt/pipeline/content-transformer.js +162 -0
- package/lib/proxy/glmt/pipeline/index.js +20 -0
- package/lib/proxy/glmt/pipeline/request-transformer.js +116 -0
- package/lib/proxy/glmt/pipeline/response-builder.js +205 -0
- package/lib/proxy/glmt/pipeline/stream-parser.js +234 -0
- package/lib/proxy/glmt/pipeline/tool-call-handler.js +78 -0
- package/lib/proxy/glmt/pipeline/types.js +6 -0
- package/lib/proxy/glmt/reasoning-enforcer.js +151 -0
- package/lib/proxy/glmt/sse-parser.js +102 -0
- package/lib/proxy/services/logging.js +13 -0
- package/lib/proxy/transformers/request-transformer.js +452 -0
- package/lib/proxy/transformers/sse-stream-transformer.js +199 -0
- package/lib/shared/logger.cjs +138 -0
- package/package.json +35 -0
- package/src/accounts/instance-manager.ts +58 -0
- package/src/accounts/shared-manager.ts +154 -0
- package/src/accounts/store.ts +111 -0
- package/src/core/model-router.ts +82 -0
- package/src/dashboard-server.ts +407 -0
- package/src/mcc.ts +474 -0
- package/src/mcp/external-registry.ts +73 -0
- package/src/mcp/installer.ts +258 -0
- package/src/mcp/mcp-config.ts +168 -0
- package/src/mcp/registry.ts +89 -0
- package/src/proxy/proxy-daemon.ts +184 -0
- package/src/proxy/proxy-entry.ts +63 -0
- package/src/proxy/proxy-paths.ts +97 -0
- package/src/proxy/proxy-server.ts +278 -0
- package/src/proxy/upstream-url.ts +38 -0
- package/src/shared/logger.ts +140 -0
- package/src/shared/provider-preset-catalog.ts +340 -0
- package/tsconfig.json +33 -0
- package/ui/.prettierrc +9 -0
- package/ui/index.html +12 -0
- package/ui/package.json +33 -0
- package/ui/postcss.config.js +6 -0
- package/ui/src/App.tsx +753 -0
- package/ui/src/components/ui/button.tsx +48 -0
- package/ui/src/components/ui/card.tsx +50 -0
- package/ui/src/components/ui/input.tsx +21 -0
- package/ui/src/components/ui/label.tsx +20 -0
- package/ui/src/components/ui/select.tsx +80 -0
- package/ui/src/components/ui/switch.tsx +26 -0
- package/ui/src/components/ui/tabs.tsx +52 -0
- package/ui/src/index.css +33 -0
- package/ui/src/lib/api.ts +185 -0
- package/ui/src/lib/utils.ts +6 -0
- package/ui/src/main.tsx +10 -0
- package/ui/src/vite-env.d.ts +1 -0
- package/ui/tailwind.config.js +49 -0
- package/ui/tsconfig.json +25 -0
- package/ui/vite.config.ts +20 -0
|
@@ -0,0 +1,407 @@
|
|
|
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
|
+
app.listen(PORT, () => {
|
|
394
|
+
console.log(`[OK] MCC Dashboard: http://localhost:${PORT}`);
|
|
395
|
+
openBrowser(`http://localhost:${PORT}`);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
process.on('SIGINT', () => {
|
|
400
|
+
console.log('\n[i] Dashboard shutting down...');
|
|
401
|
+
process.exit(0);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
main().catch((err) => {
|
|
405
|
+
console.error(`[!] Dashboard server error: ${err.message}`);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
});
|