@hileeon/mcc 0.1.3 → 0.1.5
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/dist/dashboard-server.js +1 -1
- package/dist/dashboard-server.js.map +1 -1
- package/dist/mcc.js +1 -1
- package/dist/ui/assets/index-B16lhKZ6.js +40 -0
- package/dist/ui/assets/index-jEfiB6-h.css +1 -0
- package/{ui → dist/ui}/index.html +3 -2
- package/package.json +8 -2
- package/.claude/CLAUDE.md +0 -204
- package/.claude/agents/.gitkeep +0 -0
- package/.claude/settings.json +0 -9
- package/.claude/skills/.gitkeep +0 -0
- package/docs/decisions.md +0 -33
- package/docs/lessons.md +0 -8
- package/docs/product.md +0 -37
- package/src/accounts/instance-manager.ts +0 -58
- package/src/accounts/shared-manager.ts +0 -154
- package/src/accounts/store.ts +0 -111
- package/src/core/model-router.ts +0 -82
- package/src/dashboard-server.ts +0 -427
- package/src/mcc.ts +0 -482
- package/src/mcp/external-registry.ts +0 -73
- package/src/mcp/installer.ts +0 -258
- package/src/mcp/mcp-config.ts +0 -168
- package/src/mcp/registry.ts +0 -89
- package/src/proxy/proxy-daemon.ts +0 -184
- package/src/proxy/proxy-entry.ts +0 -63
- package/src/proxy/proxy-paths.ts +0 -97
- package/src/proxy/proxy-server.ts +0 -278
- package/src/proxy/upstream-url.ts +0 -38
- package/src/shared/logger.ts +0 -140
- package/src/shared/provider-preset-catalog.ts +0 -340
- package/tsconfig.json +0 -33
- package/ui/.prettierrc +0 -9
- package/ui/package.json +0 -33
- package/ui/postcss.config.js +0 -6
- package/ui/src/App.tsx +0 -753
- package/ui/src/components/ui/button.tsx +0 -48
- package/ui/src/components/ui/card.tsx +0 -50
- package/ui/src/components/ui/input.tsx +0 -21
- package/ui/src/components/ui/label.tsx +0 -20
- package/ui/src/components/ui/select.tsx +0 -80
- package/ui/src/components/ui/switch.tsx +0 -26
- package/ui/src/components/ui/tabs.tsx +0 -52
- package/ui/src/index.css +0 -33
- package/ui/src/lib/api.ts +0 -185
- package/ui/src/lib/utils.ts +0 -6
- package/ui/src/main.tsx +0 -10
- package/ui/src/vite-env.d.ts +0 -1
- package/ui/tailwind.config.js +0 -49
- package/ui/tsconfig.json +0 -25
- package/ui/vite.config.ts +0 -20
package/src/accounts/store.ts
DELETED
|
@@ -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
|
-
}
|
package/src/core/model-router.ts
DELETED
|
@@ -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
|
-
}
|
package/src/dashboard-server.ts
DELETED
|
@@ -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
|
-
});
|