@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,258 +0,0 @@
1
- /**
2
- * MCP Server Installer
3
- */
4
-
5
- import * as fs from 'fs';
6
- import * as path from 'path';
7
- import pc from 'picocolors';
8
- import {
9
- getBuiltinServerPath,
10
- BUILTIN_MCP_SERVERS,
11
- getServerByName,
12
- type McpServerConfig,
13
- } from './registry';
14
- import {
15
- readExternalMcpRegistry,
16
- type ExternalMcpServer,
17
- } from './external-registry';
18
- import { readMcpConfig } from './mcp-config';
19
-
20
- function getMccHome(): string {
21
- return process.env.MCC_HOME ?? path.join(process.env.HOME ?? process.env.USERPROFILE ?? '~', '.mcc');
22
- }
23
-
24
- function getMcpInstallDir(): string {
25
- return path.join(getMccHome(), 'mcp');
26
- }
27
-
28
- export function installBuiltinServers(): void {
29
- const installDir = getMcpInstallDir();
30
- fs.mkdirSync(installDir, { recursive: true, mode: 0o700 });
31
- const copied: string[] = [];
32
-
33
- // Install MCP server entry points
34
- for (const server of BUILTIN_MCP_SERVERS) {
35
- const sourcePath = getBuiltinServerPath(server.name);
36
- if (!sourcePath) {
37
- console.warn(`[!] Built-in server not found: ${server.name}`);
38
- continue;
39
- }
40
- const destPath = path.join(installDir, path.basename(sourcePath));
41
- if (fs.existsSync(destPath)) {
42
- const sourceContent = fs.readFileSync(sourcePath);
43
- const destContent = fs.readFileSync(destPath);
44
- if (sourceContent.equals(destContent)) continue;
45
- }
46
- fs.copyFileSync(sourcePath, destPath);
47
- fs.chmodSync(destPath, 0o700);
48
- copied.push(server.name);
49
- }
50
-
51
- // Install mcp-hooks/ (runtime files referenced by MCP servers)
52
- const hooksSourceDir = path.join(__dirname, '..', '..', 'lib', 'mcp-hooks');
53
- const hooksInstallDir = path.join(getMccHome(), 'mcp-hooks');
54
- if (fs.existsSync(hooksSourceDir)) {
55
- fs.mkdirSync(hooksInstallDir, { recursive: true, mode: 0o700 });
56
- for (const entry of fs.readdirSync(hooksSourceDir)) {
57
- if (!entry.endsWith('.cjs')) continue;
58
- const src = path.join(hooksSourceDir, entry);
59
- const dest = path.join(hooksInstallDir, entry);
60
- if (fs.existsSync(dest)) {
61
- if (fs.readFileSync(src).equals(fs.readFileSync(dest))) continue;
62
- }
63
- fs.copyFileSync(src, dest);
64
- fs.chmodSync(dest, 0o700);
65
- copied.push(`hooks/${entry}`);
66
- }
67
- }
68
-
69
- // Install shared logger to ~/.mcc/shared/logger.cjs
70
- const sharedSource = path.join(__dirname, '..', '..', 'lib', 'shared', 'logger.cjs');
71
- const sharedDestDir = path.join(getMccHome(), 'shared');
72
- if (fs.existsSync(sharedSource)) {
73
- fs.mkdirSync(sharedDestDir, { recursive: true, mode: 0o700 });
74
- const sharedDest = path.join(sharedDestDir, 'logger.cjs');
75
- if (!fs.existsSync(sharedDest) || !fs.readFileSync(sharedSource).equals(fs.readFileSync(sharedDest))) {
76
- fs.copyFileSync(sharedSource, sharedDest);
77
- fs.chmodSync(sharedDest, 0o700);
78
- copied.push('shared/logger.cjs');
79
- }
80
- }
81
-
82
- if (copied.length > 0) {
83
- console.log(` ${pc.dim('install')} ${pc.dim(copied.join(', '))}`);
84
- }
85
- }
86
-
87
- export function readInstanceMcpConfig(instancePath: string): Record<string, McpServerConfig> {
88
- // Read from .claude.json (where Claude Code actually reads MCP config)
89
- const claudeJsonPath = path.join(instancePath, '.claude.json');
90
- if (fs.existsSync(claudeJsonPath)) {
91
- try {
92
- const data = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
93
- if (data.mcpServers) return data.mcpServers;
94
- } catch { /* ignore */ }
95
- }
96
- return {};
97
- }
98
-
99
- function readClaudeJson(instancePath: string): Record<string, unknown> {
100
- const claudeJsonPath = path.join(instancePath, '.claude.json');
101
- if (fs.existsSync(claudeJsonPath)) {
102
- try {
103
- return JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
104
- } catch { /* ignore */ }
105
- }
106
- return {};
107
- }
108
-
109
- function writeClaudeJson(instancePath: string, data: Record<string, unknown>): void {
110
- const claudeJsonPath = path.join(instancePath, '.claude.json');
111
- fs.writeFileSync(claudeJsonPath, JSON.stringify(data, null, 2) + '\n', {
112
- encoding: 'utf8',
113
- mode: 0o600,
114
- });
115
- }
116
-
117
- /**
118
- * Read which external MCPs are enabled for a given instance.
119
- */
120
- export function readInstanceExternalEnabled(instancePath: string): string[] {
121
- const filePath = path.join(instancePath, 'external-mcp-enabled.json');
122
- if (!fs.existsSync(filePath)) return [];
123
- try {
124
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
125
- } catch {
126
- return [];
127
- }
128
- }
129
-
130
- /**
131
- * Write which external MCPs are enabled for a given instance.
132
- */
133
- function writeInstanceExternalEnabled(instancePath: string, names: string[]): void {
134
- const filePath = path.join(instancePath, 'external-mcp-enabled.json');
135
- fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
136
- fs.writeFileSync(filePath, JSON.stringify(names, null, 2) + '\n', {
137
- encoding: 'utf8',
138
- mode: 0o600,
139
- });
140
- }
141
-
142
- /**
143
- * Resolve ${MCC_PROVIDER_KEY:<providerId>} placeholders from mcp-config.json.
144
- */
145
- function resolveEnvVar(value: string): string {
146
- const match = value.match(/^\$\{MCC_PROVIDER_KEY:([^}]+)\}$/);
147
- if (!match) return value;
148
-
149
- const providerId = match[1];
150
- const mcpConfig = readMcpConfig();
151
-
152
- // Check imageAnalysis providers first
153
- const iaProvider = mcpConfig.imageAnalysis.providers[providerId];
154
- if (iaProvider?.apiKey) return iaProvider.apiKey;
155
-
156
- // Check websearch providers
157
- const wsProvider = mcpConfig.websearch.providers[providerId];
158
- if (wsProvider?.apiKey) return wsProvider.apiKey;
159
-
160
- return value; // Return placeholder if not resolved
161
- }
162
-
163
- /**
164
- * Resolve all env vars for an external MCP server.
165
- */
166
- function resolveExternalServerEnv(server: ExternalMcpServer): Record<string, string> {
167
- const resolved: Record<string, string> = {};
168
- for (const [key, value] of Object.entries(server.envVars)) {
169
- resolved[key] = resolveEnvVar(value);
170
- }
171
- return resolved;
172
- }
173
-
174
- /**
175
- * Sync MCP servers for an instance into .claude.json.
176
- *
177
- * @param instancePath - Path to the instance directory
178
- * @param enabledServerNames - Names of servers to enable (from built-in + external)
179
- * @param profileName - Profile name, used to look up provider API keys
180
- */
181
- export function syncInstanceMcpServers(
182
- instancePath: string,
183
- enabledServerNames: string[],
184
- _profileName?: string,
185
- ): void {
186
- const mcpServers: Record<string, McpServerConfig> = {};
187
- const instanceExternalEnabled = readInstanceExternalEnabled(instancePath);
188
-
189
- // Collect all enabled names: explicit list + per-instance external enables
190
- const allEnabled = new Set([...enabledServerNames, ...instanceExternalEnabled]);
191
-
192
- for (const name of allEnabled) {
193
- const server = getServerByName(name);
194
- if (!server) continue;
195
-
196
- if ('config' in server) {
197
- // Built-in server (has config field)
198
- mcpServers[name] = server.config;
199
- } else {
200
- // External server (ExternalMcpServer)
201
- const external = server as ExternalMcpServer;
202
- mcpServers[name] = {
203
- type: 'stdio',
204
- command: external.command,
205
- args: external.args,
206
- env: resolveExternalServerEnv(external),
207
- };
208
- }
209
- }
210
-
211
- // Write to .claude.json (Claude Code reads MCP config from here)
212
- const claudeJson = readClaudeJson(instancePath);
213
- claudeJson.mcpServers = mcpServers;
214
- writeClaudeJson(instancePath, claudeJson);
215
-
216
- // Also write to .mcp/mcpServers.json (for reference/compatibility)
217
- const mcpDir = path.join(instancePath, '.mcp');
218
- fs.mkdirSync(mcpDir, { recursive: true, mode: 0o700 });
219
- const configPath = path.join(mcpDir, 'mcpServers.json');
220
- fs.writeFileSync(configPath, JSON.stringify(mcpServers, null, 2), {
221
- encoding: 'utf8',
222
- mode: 0o600,
223
- });
224
-
225
- const count = Object.keys(mcpServers).length;
226
- console.log(` ${pc.dim('mcp')} ${pc.dim(`${count} server(s) synced`)}`);
227
- }
228
-
229
- /**
230
- * Enable an external MCP for a specific instance.
231
- */
232
- export function enableInstanceExternalMcp(instancePath: string, name: string): void {
233
- const enabled = readInstanceExternalEnabled(instancePath);
234
- if (!enabled.includes(name)) {
235
- enabled.push(name);
236
- writeInstanceExternalEnabled(instancePath, enabled);
237
- }
238
- }
239
-
240
- /**
241
- * Disable an external MCP for a specific instance.
242
- */
243
- export function disableInstanceExternalMcp(instancePath: string, name: string): void {
244
- const enabled = readInstanceExternalEnabled(instancePath);
245
- writeInstanceExternalEnabled(
246
- instancePath,
247
- enabled.filter((n) => n !== name),
248
- );
249
- }
250
-
251
- /**
252
- * Get all known server names (built-in + external).
253
- */
254
- export function getAllServerNames(): string[] {
255
- const builtinNames = BUILTIN_MCP_SERVERS.map((s) => s.name);
256
- const externalNames = readExternalMcpRegistry().map((s) => s.name);
257
- return [...builtinNames, ...externalNames];
258
- }
@@ -1,168 +0,0 @@
1
- /**
2
- * MCP Config - Provider configuration for WebSearch and ImageAnalysis
3
- *
4
- * Storage: ~/.mcc/mcp-config.json
5
- */
6
-
7
- import * as fs from 'fs';
8
- import * as path from 'path';
9
-
10
- function getMccHome(): string {
11
- return process.env.MCC_HOME ?? path.join(process.env.HOME ?? process.env.USERPROFILE ?? '~', '.mcc');
12
- }
13
-
14
- export function getMcpConfigPath(): string {
15
- return path.join(getMccHome(), 'mcp-config.json');
16
- }
17
-
18
- export interface WebSearchProvider {
19
- enabled: boolean;
20
- apiKey?: string;
21
- }
22
-
23
- export interface ImageAnalysisProvider {
24
- enabled: boolean;
25
- baseUrl: string;
26
- apiKey: string;
27
- model: string;
28
- format: 'anthropic' | 'openai';
29
- }
30
-
31
- export interface McpConfig {
32
- websearch: {
33
- enabled: boolean;
34
- providers: Record<string, WebSearchProvider>;
35
- };
36
- imageAnalysis: {
37
- enabled: boolean;
38
- providers: Record<string, ImageAnalysisProvider>;
39
- };
40
- }
41
-
42
- const DEFAULT_CONFIG: McpConfig = {
43
- websearch: {
44
- enabled: true,
45
- providers: {
46
- duckduckgo: { enabled: true },
47
- exa: { enabled: false, apiKey: '' },
48
- tavily: { enabled: false, apiKey: '' },
49
- brave: { enabled: false, apiKey: '' },
50
- },
51
- },
52
- imageAnalysis: {
53
- enabled: true,
54
- providers: {
55
- ali: {
56
- enabled: false,
57
- baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
58
- apiKey: '',
59
- model: 'qwen-vl-plus',
60
- format: 'openai',
61
- },
62
- kimi: {
63
- enabled: false,
64
- baseUrl: 'https://api.moonshot.cn/v1',
65
- apiKey: '',
66
- model: 'moonshot-v1-128k-vision-preview',
67
- format: 'openai',
68
- },
69
- minimax: {
70
- enabled: false,
71
- baseUrl: 'https://api.minimaxi.com/anthropic',
72
- apiKey: '',
73
- model: 'MiniMax-VL-01',
74
- format: 'anthropic',
75
- },
76
- deepseek: {
77
- enabled: false,
78
- baseUrl: 'https://api.deepseek.com/anthropic',
79
- apiKey: '',
80
- model: 'deepseek-v4-pro[1m]',
81
- format: 'anthropic',
82
- },
83
- },
84
- },
85
- };
86
-
87
- export function readMcpConfig(): McpConfig {
88
- const configPath = getMcpConfigPath();
89
- if (!fs.existsSync(configPath)) {
90
- return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
91
- }
92
- try {
93
- const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
94
- // Merge with defaults to ensure all fields exist
95
- return {
96
- websearch: {
97
- enabled: raw.websearch?.enabled ?? DEFAULT_CONFIG.websearch.enabled,
98
- providers: { ...DEFAULT_CONFIG.websearch.providers, ...raw.websearch?.providers },
99
- },
100
- imageAnalysis: {
101
- enabled: raw.imageAnalysis?.enabled ?? DEFAULT_CONFIG.imageAnalysis.enabled,
102
- providers: { ...DEFAULT_CONFIG.imageAnalysis.providers, ...raw.imageAnalysis?.providers },
103
- },
104
- };
105
- } catch {
106
- return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
107
- }
108
- }
109
-
110
- export function writeMcpConfig(config: McpConfig): void {
111
- const configPath = getMcpConfigPath();
112
- fs.mkdirSync(path.dirname(configPath), { recursive: true, mode: 0o700 });
113
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
114
- }
115
-
116
- export function getProviderPresets() {
117
- return {
118
- websearch: {
119
- duckduckgo: { name: 'DuckDuckGo', needsApiKey: false, description: 'Free, no API key needed' },
120
- exa: { name: 'Exa', needsApiKey: true, description: 'AI-powered search' },
121
- tavily: { name: 'Tavily', needsApiKey: true, description: 'Search API for AI' },
122
- brave: { name: 'Brave Search', needsApiKey: true, description: 'Privacy-focused search' },
123
- },
124
- imageAnalysis: {
125
- ali: {
126
- name: '阿里 DashScope',
127
- format: 'openai' as const,
128
- baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
129
- models: ['qwen-vl-plus', 'qwen3-vl-flash', 'qwen-vl-max', 'qwen-vl-ocr', 'qvq-max', 'qvq-plus'],
130
- },
131
- kimi: {
132
- name: 'Kimi (Moonshot)',
133
- format: 'openai' as const,
134
- baseUrl: 'https://api.moonshot.cn/v1',
135
- models: ['moonshot-v1-128k-vision-preview', 'moonshot-v1-32k-vision-preview'],
136
- },
137
- minimax: {
138
- name: 'MiniMax',
139
- format: 'anthropic' as const,
140
- baseUrl: 'https://api.minimaxi.com/anthropic',
141
- models: ['MiniMax-VL-01'],
142
- },
143
- deepseek: {
144
- name: 'DeepSeek',
145
- format: 'anthropic' as const,
146
- baseUrl: 'https://api.deepseek.com/anthropic',
147
- models: ['deepseek-v4-pro[1m]', 'deepseek-v4-pro'],
148
- },
149
- },
150
- };
151
- }
152
-
153
- /** Get the first enabled image analysis provider config, or null */
154
- export function getActiveImageAnalysisProvider(config: McpConfig): (ImageAnalysisProvider & { id: string }) | null {
155
- for (const [id, provider] of Object.entries(config.imageAnalysis.providers)) {
156
- if (provider.enabled && provider.apiKey && provider.baseUrl) {
157
- return { id, ...provider };
158
- }
159
- }
160
- return null;
161
- }
162
-
163
- /** Get all enabled websearch provider IDs */
164
- export function getEnabledWebSearchProviders(config: McpConfig): string[] {
165
- return Object.entries(config.websearch.providers)
166
- .filter(([, p]) => p.enabled)
167
- .map(([id]) => id);
168
- }
@@ -1,89 +0,0 @@
1
- /**
2
- * MCP Server Registry
3
- */
4
-
5
- import * as fs from 'fs';
6
- import * as path from 'path';
7
- import type { ExternalMcpServer } from './external-registry';
8
-
9
- export interface McpServerConfig {
10
- type: 'stdio' | 'http';
11
- command: string;
12
- args?: string[];
13
- env?: Record<string, string>;
14
- builtin?: boolean;
15
- }
16
-
17
- export interface McpRegistryEntry {
18
- name: string;
19
- displayName: string;
20
- description: string;
21
- config: McpServerConfig;
22
- enabledByDefault: boolean;
23
- }
24
-
25
- function getMccHome(): string {
26
- return process.env.MCC_HOME ?? path.join(process.env.HOME ?? process.env.USERPROFILE ?? '~', '.mcc');
27
- }
28
-
29
- export const BUILTIN_MCP_SERVERS: readonly McpRegistryEntry[] = [
30
- {
31
- name: 'mcc-websearch',
32
- displayName: 'WebSearch',
33
- description:
34
- 'Multi-provider web search (Exa, Tavily, Brave Search, DuckDuckGo).',
35
- config: {
36
- type: 'stdio',
37
- command: 'node',
38
- args: [path.join(getMccHome(), 'mcp', 'mcc-websearch-server.cjs')],
39
- env: {},
40
- builtin: true,
41
- },
42
- enabledByDefault: true,
43
- },
44
- {
45
- name: 'mcc-image-analysis',
46
- displayName: 'Image Analysis',
47
- description: 'Image and PDF analysis via vision models.',
48
- config: {
49
- type: 'stdio',
50
- command: 'node',
51
- args: [path.join(getMccHome(), 'mcp', 'mcc-image-analysis-server.cjs')],
52
- env: {},
53
- builtin: true,
54
- },
55
- enabledByDefault: true,
56
- },
57
- ];
58
-
59
- export function getBuiltinServerPath(serverName: string): string | null {
60
- const libDir = path.join(__dirname, '..', '..', 'lib', 'mcp');
61
- const fileName =
62
- serverName === 'mcc-websearch'
63
- ? 'mcc-websearch-server.cjs'
64
- : serverName === 'mcc-image-analysis'
65
- ? 'mcc-image-analysis-server.cjs'
66
- : null;
67
- if (!fileName) return null;
68
- const candidate = path.join(libDir, fileName);
69
- return fs.existsSync(candidate) ? candidate : null;
70
- }
71
-
72
- export function getServerByName(
73
- name: string,
74
- ): McpRegistryEntry | ExternalMcpServer | undefined {
75
- const builtin = BUILTIN_MCP_SERVERS.find((s) => s.name === name);
76
- if (builtin) return builtin;
77
- // Avoid circular import at module level — require at runtime
78
- const { readExternalMcpRegistry } = require('./external-registry') as {
79
- readExternalMcpRegistry: () => ExternalMcpServer[];
80
- };
81
- return readExternalMcpRegistry().find((s) => s.name === name);
82
- }
83
-
84
- export function getAllServers(): Array<McpRegistryEntry | ExternalMcpServer> {
85
- const { readExternalMcpRegistry } = require('./external-registry') as {
86
- readExternalMcpRegistry: () => ExternalMcpServer[];
87
- };
88
- return [...BUILTIN_MCP_SERVERS, ...readExternalMcpRegistry()];
89
- }
@@ -1,184 +0,0 @@
1
- /**
2
- * Proxy Daemon - Lifecycle management for the translation proxy
3
- *
4
- * Manages starting/stopping the proxy as a detached child process.
5
- * Ported from CCS proxy/proxy-daemon.js (simplified for single-profile MCC).
6
- */
7
-
8
- import * as crypto from 'crypto';
9
- import * as http from 'http';
10
- import { spawn } from 'child_process';
11
- import * as path from 'path';
12
- import {
13
- resolveAdaptivePort,
14
- readProxyPid,
15
- writeProxyPid,
16
- removeProxyPid,
17
- readProxySession,
18
- writeProxySession,
19
- removeProxySession,
20
- PROXY_SERVICE_NAME,
21
- type ProxySession,
22
- } from './proxy-paths';
23
-
24
- const PROXY_ENTRY_CANDIDATES = [
25
- path.join(__dirname, 'proxy-entry.js'),
26
- path.join(__dirname, 'proxy-entry.cjs'),
27
- ];
28
-
29
- function generateAuthToken(): string {
30
- return crypto.randomBytes(24).toString('hex');
31
- }
32
-
33
- async function isProxyRunning(port: number): Promise<boolean> {
34
- return new Promise((resolve) => {
35
- const req = http.request(
36
- { hostname: '127.0.0.1', port, path: '/health', method: 'GET', timeout: 3000 },
37
- (res) => {
38
- let body = '';
39
- res.setEncoding('utf8');
40
- res.on('data', (chunk) => (body += chunk));
41
- res.on('end', () => {
42
- if (res.statusCode !== 200) { resolve(false); return; }
43
- try {
44
- const payload = JSON.parse(body);
45
- resolve(payload.service === PROXY_SERVICE_NAME);
46
- } catch {
47
- resolve(false);
48
- }
49
- });
50
- },
51
- );
52
- req.on('error', () => resolve(false));
53
- req.on('timeout', () => { req.destroy(); resolve(false); });
54
- req.end();
55
- });
56
- }
57
-
58
- async function terminateProcess(pid: number): Promise<void> {
59
- try {
60
- process.kill(pid, 'SIGTERM');
61
- let attempts = 0;
62
- while (attempts < 10) {
63
- await new Promise((resolve) => setTimeout(resolve, 200));
64
- try {
65
- process.kill(pid, 0);
66
- attempts++;
67
- } catch {
68
- return; // Process exited
69
- }
70
- }
71
- process.kill(pid, 'SIGKILL');
72
- } catch {
73
- // Best-effort
74
- }
75
- }
76
-
77
- export interface ProxyStartResult {
78
- port: number;
79
- authToken: string;
80
- }
81
-
82
- /**
83
- * Start the translation proxy for a profile.
84
- * If a proxy is already running for this profile, returns its existing session.
85
- */
86
- export async function startProxy(
87
- profileName: string,
88
- baseUrl: string,
89
- apiKey: string,
90
- model?: string,
91
- ): Promise<ProxyStartResult> {
92
- // Check if already running
93
- const existingSession = readProxySession(profileName);
94
- if (existingSession && await isProxyRunning(existingSession.port)) {
95
- return { port: existingSession.port, authToken: existingSession.authToken };
96
- }
97
-
98
- // Clean up stale state
99
- stopProxy(profileName).catch(() => {});
100
-
101
- const port = resolveAdaptivePort(profileName);
102
- const authToken = generateAuthToken();
103
-
104
- // Find the entry script
105
- let entryScript: string | null = null;
106
- for (const candidate of PROXY_ENTRY_CANDIDATES) {
107
- try {
108
- const fs = await import('fs');
109
- fs.accessSync(candidate, fs.constants.R_OK);
110
- entryScript = candidate;
111
- break;
112
- } catch {
113
- // Try next
114
- }
115
- }
116
- if (!entryScript) {
117
- throw new Error('Proxy entry script not found. Run: npm run build');
118
- }
119
-
120
- // Spawn the proxy as a detached child process
121
- const child = spawn(process.execPath, [
122
- entryScript,
123
- '--port', String(port),
124
- '--host', '127.0.0.1',
125
- '--base-url', baseUrl,
126
- '--api-key', apiKey,
127
- '--auth-token', authToken,
128
- ...(model ? ['--model', model] : []),
129
- ], {
130
- detached: true,
131
- stdio: 'ignore',
132
- env: { ...process.env },
133
- });
134
- child.unref();
135
-
136
- const pid = child.pid;
137
- if (!pid) {
138
- throw new Error('Failed to spawn proxy process');
139
- }
140
-
141
- // Write PID and session files
142
- writeProxyPid(profileName, pid);
143
- const session: ProxySession = {
144
- profileName,
145
- port,
146
- host: '127.0.0.1',
147
- authToken,
148
- baseUrl,
149
- startedAt: new Date().toISOString(),
150
- };
151
- writeProxySession(session);
152
-
153
- // Wait for the proxy to be ready (up to 5 seconds)
154
- for (let i = 0; i < 25; i++) {
155
- await new Promise((resolve) => setTimeout(resolve, 200));
156
- if (await isProxyRunning(port)) {
157
- return { port, authToken };
158
- }
159
- }
160
-
161
- throw new Error(`Proxy failed to start within 5 seconds (PID: ${pid}, port: ${port})`);
162
- }
163
-
164
- /**
165
- * Stop the translation proxy for a profile.
166
- */
167
- export async function stopProxy(profileName: string): Promise<void> {
168
- const pid = readProxyPid(profileName);
169
- if (pid) {
170
- await terminateProcess(pid);
171
- }
172
- removeProxyPid(profileName);
173
- removeProxySession(profileName);
174
- }
175
-
176
- /**
177
- * Check if a proxy is running for a profile.
178
- */
179
- export async function getProxyStatus(profileName: string): Promise<{ running: boolean; port?: number }> {
180
- const session = readProxySession(profileName);
181
- if (!session) return { running: false };
182
- const running = await isProxyRunning(session.port);
183
- return { running, port: running ? session.port : undefined };
184
- }