@kirosnn/mosaic 0.71.0 → 0.74.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/README.md +1 -5
- package/package.json +4 -2
- package/src/agent/Agent.ts +353 -131
- package/src/agent/context.ts +4 -4
- package/src/agent/prompts/systemPrompt.ts +15 -6
- package/src/agent/prompts/toolsPrompt.ts +136 -10
- package/src/agent/provider/anthropic.ts +100 -100
- package/src/agent/provider/google.ts +102 -102
- package/src/agent/provider/mistral.ts +95 -95
- package/src/agent/provider/ollama.ts +77 -60
- package/src/agent/provider/openai.ts +42 -38
- package/src/agent/provider/rateLimit.ts +178 -0
- package/src/agent/provider/xai.ts +99 -99
- package/src/agent/tools/definitions.ts +19 -9
- package/src/agent/tools/executor.ts +95 -85
- package/src/agent/tools/exploreExecutor.ts +8 -10
- package/src/agent/tools/grep.ts +30 -29
- package/src/agent/tools/question.ts +7 -1
- package/src/agent/types.ts +9 -8
- package/src/components/App.tsx +45 -45
- package/src/components/CustomInput.tsx +214 -36
- package/src/components/Main.tsx +552 -339
- package/src/components/Setup.tsx +1 -1
- package/src/components/Welcome.tsx +1 -1
- package/src/components/main/ApprovalPanel.tsx +4 -3
- package/src/components/main/ChatPage.tsx +858 -675
- package/src/components/main/HomePage.tsx +53 -38
- package/src/components/main/QuestionPanel.tsx +52 -7
- package/src/components/main/ThinkingIndicator.tsx +2 -1
- package/src/index.tsx +50 -20
- package/src/mcp/approvalPolicy.ts +156 -0
- package/src/mcp/cli/add.ts +185 -0
- package/src/mcp/cli/doctor.ts +74 -0
- package/src/mcp/cli/index.ts +85 -0
- package/src/mcp/cli/list.ts +50 -0
- package/src/mcp/cli/logs.ts +24 -0
- package/src/mcp/cli/manage.ts +99 -0
- package/src/mcp/cli/show.ts +53 -0
- package/src/mcp/cli/tools.ts +77 -0
- package/src/mcp/config.ts +234 -0
- package/src/mcp/index.ts +80 -0
- package/src/mcp/processManager.ts +304 -0
- package/src/mcp/rateLimiter.ts +50 -0
- package/src/mcp/registry.ts +151 -0
- package/src/mcp/schemaConverter.ts +100 -0
- package/src/mcp/servers/navigation/browser.ts +151 -0
- package/src/mcp/servers/navigation/index.ts +23 -0
- package/src/mcp/servers/navigation/tools.ts +263 -0
- package/src/mcp/servers/navigation/types.ts +17 -0
- package/src/mcp/servers/navigation/utils.ts +20 -0
- package/src/mcp/toolCatalog.ts +182 -0
- package/src/mcp/types.ts +116 -0
- package/src/utils/approvalBridge.ts +17 -5
- package/src/utils/commands/compact.ts +30 -0
- package/src/utils/commands/echo.ts +1 -1
- package/src/utils/commands/index.ts +4 -6
- package/src/utils/commands/new.ts +15 -0
- package/src/utils/commands/types.ts +3 -0
- package/src/utils/config.ts +3 -1
- package/src/utils/diffRendering.tsx +1 -3
- package/src/utils/exploreBridge.ts +10 -0
- package/src/utils/markdown.tsx +220 -122
- package/src/utils/models.ts +31 -9
- package/src/utils/questionBridge.ts +36 -1
- package/src/utils/tokenEstimator.ts +32 -0
- package/src/utils/toolFormatting.ts +317 -7
- package/src/web/app.tsx +72 -72
- package/src/web/components/HomePage.tsx +7 -7
- package/src/web/components/MessageItem.tsx +66 -35
- package/src/web/components/QuestionPanel.tsx +72 -12
- package/src/web/components/Sidebar.tsx +0 -2
- package/src/web/components/ThinkingIndicator.tsx +1 -0
- package/src/web/server.tsx +767 -683
- package/src/utils/commands/redo.ts +0 -74
- package/src/utils/commands/sessions.ts +0 -129
- package/src/utils/commands/undo.ts +0 -75
- package/src/utils/undoRedo.ts +0 -429
- package/src/utils/undoRedoBridge.ts +0 -45
- package/src/utils/undoRedoDb.ts +0 -338
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { loadMcpConfig } from '../config';
|
|
2
|
+
import { McpProcessManager } from '../processManager';
|
|
3
|
+
import { platform } from 'os';
|
|
4
|
+
import { exec } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
|
|
9
|
+
export async function mcpDoctor(): Promise<void> {
|
|
10
|
+
const configs = loadMcpConfig();
|
|
11
|
+
|
|
12
|
+
if (configs.length === 0) {
|
|
13
|
+
console.log('No MCP servers configured. Nothing to diagnose.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log(`Diagnosing ${configs.length} MCP server(s)...\n`);
|
|
18
|
+
|
|
19
|
+
const manager = new McpProcessManager();
|
|
20
|
+
let pass = 0;
|
|
21
|
+
let fail = 0;
|
|
22
|
+
|
|
23
|
+
for (const config of configs) {
|
|
24
|
+
console.log(`--- ${config.id} (${config.name}) ---`);
|
|
25
|
+
|
|
26
|
+
console.log(' [config] OK');
|
|
27
|
+
|
|
28
|
+
const commandExists = await checkCommand(config.command);
|
|
29
|
+
if (commandExists) {
|
|
30
|
+
console.log(` [command] "${config.command}" found`);
|
|
31
|
+
} else {
|
|
32
|
+
console.log(` [command] WARNING: "${config.command}" not found on PATH`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!config.enabled) {
|
|
36
|
+
console.log(' [status] DISABLED - skipping connectivity test');
|
|
37
|
+
console.log('');
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const state = await manager.startServer(config);
|
|
43
|
+
|
|
44
|
+
if (state.status === 'running') {
|
|
45
|
+
console.log(` [connect] OK (${state.initLatencyMs}ms)`);
|
|
46
|
+
console.log(` [tools] ${state.toolCount} tool(s) discovered`);
|
|
47
|
+
pass++;
|
|
48
|
+
} else {
|
|
49
|
+
console.log(` [connect] FAILED: ${state.lastError || 'unknown error'}`);
|
|
50
|
+
fail++;
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
+
console.log(` [connect] FAILED: ${message}`);
|
|
55
|
+
fail++;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log('');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await manager.shutdownAll();
|
|
62
|
+
|
|
63
|
+
console.log(`\nResults: ${pass} passed, ${fail} failed, ${configs.length - pass - fail} skipped`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function checkCommand(command: string): Promise<boolean> {
|
|
67
|
+
const which = platform() === 'win32' ? 'where' : 'which';
|
|
68
|
+
try {
|
|
69
|
+
await execAsync(`${which} ${command}`);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export async function runMcpCli(args: string[]): Promise<void> {
|
|
2
|
+
const command = args[0] || 'help';
|
|
3
|
+
const rest = args.slice(1);
|
|
4
|
+
|
|
5
|
+
switch (command) {
|
|
6
|
+
case 'list':
|
|
7
|
+
case 'ls': {
|
|
8
|
+
const { mcpList } = await import('./list');
|
|
9
|
+
await mcpList();
|
|
10
|
+
break;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
case 'tools': {
|
|
14
|
+
const { mcpTools } = await import('./tools');
|
|
15
|
+
await mcpTools(rest[0]);
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
case 'doctor': {
|
|
20
|
+
const { mcpDoctor } = await import('./doctor');
|
|
21
|
+
await mcpDoctor();
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
case 'logs': {
|
|
26
|
+
const { mcpLogs } = await import('./logs');
|
|
27
|
+
await mcpLogs(rest[0]);
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
case 'show': {
|
|
32
|
+
const { mcpShow } = await import('./show');
|
|
33
|
+
await mcpShow(rest[0]);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
case 'add': {
|
|
38
|
+
const { mcpAdd } = await import('./add');
|
|
39
|
+
await mcpAdd(rest[0]);
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
case 'remove':
|
|
44
|
+
case 'enable':
|
|
45
|
+
case 'disable':
|
|
46
|
+
case 'restart':
|
|
47
|
+
case 'start':
|
|
48
|
+
case 'stop':
|
|
49
|
+
case 'refresh': {
|
|
50
|
+
const { mcpManage } = await import('./manage');
|
|
51
|
+
await mcpManage(command, rest[0]);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case 'help':
|
|
56
|
+
default:
|
|
57
|
+
showMcpHelp();
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function showMcpHelp(): void {
|
|
63
|
+
console.log(`
|
|
64
|
+
Mosaic MCP - Model Context Protocol client
|
|
65
|
+
|
|
66
|
+
Usage:
|
|
67
|
+
mosaic mcp <command> [options]
|
|
68
|
+
|
|
69
|
+
Commands:
|
|
70
|
+
list List configured MCP servers
|
|
71
|
+
tools [serverId] List available MCP tools
|
|
72
|
+
doctor Run diagnostics on MCP servers
|
|
73
|
+
logs <serverId> Show server logs
|
|
74
|
+
show <serverId> Show server config and state
|
|
75
|
+
add [name] Add an MCP server (by name or from the list)
|
|
76
|
+
remove <serverId> Remove a server
|
|
77
|
+
enable <serverId> Enable a server
|
|
78
|
+
disable <serverId> Disable a server
|
|
79
|
+
start <serverId> Start a server
|
|
80
|
+
stop <serverId> Stop a server
|
|
81
|
+
restart <serverId> Restart a server
|
|
82
|
+
refresh [serverId] Refresh tool catalog
|
|
83
|
+
help Show this help
|
|
84
|
+
`);
|
|
85
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { loadMcpConfig } from '../config';
|
|
2
|
+
import { getMcpManager } from '../index';
|
|
3
|
+
|
|
4
|
+
export async function mcpList(): Promise<void> {
|
|
5
|
+
const configs = loadMcpConfig();
|
|
6
|
+
|
|
7
|
+
if (configs.length === 0) {
|
|
8
|
+
console.log('No MCP servers configured.');
|
|
9
|
+
console.log('Use "mosaic mcp add" to add a server.');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const manager = getMcpManager();
|
|
14
|
+
|
|
15
|
+
const header = [
|
|
16
|
+
pad('ID', 20),
|
|
17
|
+
pad('Name', 20),
|
|
18
|
+
pad('Enabled', 8),
|
|
19
|
+
pad('Autostart', 10),
|
|
20
|
+
pad('Status', 10),
|
|
21
|
+
pad('Tools', 6),
|
|
22
|
+
'Last Error',
|
|
23
|
+
].join(' | ');
|
|
24
|
+
|
|
25
|
+
console.log(header);
|
|
26
|
+
console.log('-'.repeat(header.length));
|
|
27
|
+
|
|
28
|
+
for (const config of configs) {
|
|
29
|
+
const state = manager.getState(config.id);
|
|
30
|
+
const status = state?.status || 'stopped';
|
|
31
|
+
const toolCount = state?.toolCount ?? 0;
|
|
32
|
+
const lastError = state?.lastError || '';
|
|
33
|
+
|
|
34
|
+
const row = [
|
|
35
|
+
pad(config.id, 20),
|
|
36
|
+
pad(config.name, 20),
|
|
37
|
+
pad(config.enabled ? 'yes' : 'no', 8),
|
|
38
|
+
pad(config.autostart, 10),
|
|
39
|
+
pad(status, 10),
|
|
40
|
+
pad(String(toolCount), 6),
|
|
41
|
+
lastError.length > 40 ? lastError.slice(0, 40) + '...' : lastError,
|
|
42
|
+
].join(' | ');
|
|
43
|
+
|
|
44
|
+
console.log(row);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pad(str: string, len: number): string {
|
|
49
|
+
return str.length >= len ? str.slice(0, len) : str + ' '.repeat(len - str.length);
|
|
50
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getMcpManager } from '../index';
|
|
2
|
+
|
|
3
|
+
export async function mcpLogs(serverId?: string): Promise<void> {
|
|
4
|
+
if (!serverId) {
|
|
5
|
+
console.log('Usage: mosaic mcp logs <serverId>');
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const manager = getMcpManager();
|
|
10
|
+
const logs = manager.getLogs(serverId);
|
|
11
|
+
|
|
12
|
+
if (logs.length === 0) {
|
|
13
|
+
console.log(`No logs for server "${serverId}".`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log(`Logs for ${serverId} (${logs.length} entries):\n`);
|
|
18
|
+
|
|
19
|
+
for (const entry of logs) {
|
|
20
|
+
const time = new Date(entry.timestamp).toISOString().slice(11, 23);
|
|
21
|
+
const level = entry.level.toUpperCase().padEnd(5);
|
|
22
|
+
console.log(`[${time}] ${level} ${entry.message}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { loadMcpConfig, removeServerConfig, updateServerConfig } from '../config';
|
|
2
|
+
import { getMcpManager, initializeMcp } from '../index';
|
|
3
|
+
|
|
4
|
+
export async function mcpManage(command: string, serverId?: string): Promise<void> {
|
|
5
|
+
if (!serverId && command !== 'refresh') {
|
|
6
|
+
console.log(`Usage: mosaic mcp ${command} <serverId>`);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
switch (command) {
|
|
11
|
+
case 'remove': {
|
|
12
|
+
const removed = removeServerConfig(serverId!);
|
|
13
|
+
if (removed) {
|
|
14
|
+
console.log(`Server "${serverId}" removed.`);
|
|
15
|
+
} else {
|
|
16
|
+
console.log(`Server "${serverId}" not found.`);
|
|
17
|
+
}
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
case 'enable': {
|
|
22
|
+
const result = updateServerConfig(serverId!, { enabled: true });
|
|
23
|
+
if (result) {
|
|
24
|
+
console.log(`Server "${serverId}" enabled.`);
|
|
25
|
+
} else {
|
|
26
|
+
console.log(`Server "${serverId}" not found.`);
|
|
27
|
+
}
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
case 'disable': {
|
|
32
|
+
const result = updateServerConfig(serverId!, { enabled: false });
|
|
33
|
+
if (result) {
|
|
34
|
+
console.log(`Server "${serverId}" disabled.`);
|
|
35
|
+
} else {
|
|
36
|
+
console.log(`Server "${serverId}" not found.`);
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
case 'start': {
|
|
42
|
+
const configs = loadMcpConfig();
|
|
43
|
+
const config = configs.find(c => c.id === serverId);
|
|
44
|
+
if (!config) {
|
|
45
|
+
console.log(`Server "${serverId}" not found.`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const manager = getMcpManager();
|
|
50
|
+
console.log(`Starting server "${serverId}"...`);
|
|
51
|
+
const state = await manager.startServer(config);
|
|
52
|
+
console.log(`Status: ${state.status}`);
|
|
53
|
+
if (state.lastError) console.log(`Error: ${state.lastError}`);
|
|
54
|
+
if (state.toolCount > 0) console.log(`Tools: ${state.toolCount}`);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
case 'stop': {
|
|
59
|
+
const manager = getMcpManager();
|
|
60
|
+
console.log(`Stopping server "${serverId}"...`);
|
|
61
|
+
await manager.stopServer(serverId!);
|
|
62
|
+
console.log(`Server "${serverId}" stopped.`);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
case 'restart': {
|
|
67
|
+
const manager = getMcpManager();
|
|
68
|
+
console.log(`Restarting server "${serverId}"...`);
|
|
69
|
+
const state = await manager.restartServer(serverId!);
|
|
70
|
+
if (state) {
|
|
71
|
+
console.log(`Status: ${state.status}`);
|
|
72
|
+
if (state.toolCount > 0) console.log(`Tools: ${state.toolCount}`);
|
|
73
|
+
} else {
|
|
74
|
+
console.log(`Server "${serverId}" not found.`);
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
case 'refresh': {
|
|
80
|
+
await initializeMcp();
|
|
81
|
+
const { getMcpCatalog } = await import('../index');
|
|
82
|
+
try {
|
|
83
|
+
const catalog = getMcpCatalog();
|
|
84
|
+
catalog.refreshTools(serverId);
|
|
85
|
+
const tools = catalog.getMcpToolInfos();
|
|
86
|
+
const count = serverId
|
|
87
|
+
? tools.filter(t => t.serverId === serverId).length
|
|
88
|
+
: tools.length;
|
|
89
|
+
console.log(`Refreshed. ${count} MCP tool(s) available.`);
|
|
90
|
+
} catch {
|
|
91
|
+
console.log('MCP not initialized. No servers configured or all failed.');
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
default:
|
|
97
|
+
console.log(`Unknown command: ${command}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { loadMcpConfig } from '../config';
|
|
2
|
+
import { getMcpManager } from '../index';
|
|
3
|
+
|
|
4
|
+
export async function mcpShow(serverId?: string): Promise<void> {
|
|
5
|
+
if (!serverId) {
|
|
6
|
+
console.log('Usage: mosaic mcp show <serverId>');
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const configs = loadMcpConfig();
|
|
11
|
+
const config = configs.find(c => c.id === serverId);
|
|
12
|
+
|
|
13
|
+
if (!config) {
|
|
14
|
+
console.log(`Server "${serverId}" not found.`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log(`Server: ${config.id}`);
|
|
19
|
+
console.log(` Name: ${config.name}`);
|
|
20
|
+
console.log(` Enabled: ${config.enabled}`);
|
|
21
|
+
console.log(` Command: ${config.command} ${config.args.join(' ')}`);
|
|
22
|
+
if (config.cwd) console.log(` CWD: ${config.cwd}`);
|
|
23
|
+
console.log(` Autostart: ${config.autostart}`);
|
|
24
|
+
console.log(` Approval: ${config.approval}`);
|
|
25
|
+
console.log(` Timeouts: init=${config.timeouts.initialize}ms, call=${config.timeouts.call}ms`);
|
|
26
|
+
console.log(` Limits: ${config.limits.maxCallsPerMinute} calls/min, ${config.limits.maxPayloadBytes} bytes max`);
|
|
27
|
+
console.log(` Logs: persist=${config.logs.persist}, buffer=${config.logs.bufferSize}`);
|
|
28
|
+
|
|
29
|
+
if (config.tools.allow) console.log(` Allow: ${config.tools.allow.join(', ')}`);
|
|
30
|
+
if (config.tools.deny) console.log(` Deny: ${config.tools.deny.join(', ')}`);
|
|
31
|
+
|
|
32
|
+
if (config.env) {
|
|
33
|
+
console.log(` Env:`);
|
|
34
|
+
for (const [key, value] of Object.entries(config.env)) {
|
|
35
|
+
console.log(` ${key}=${value.length > 30 ? value.slice(0, 30) + '...' : value}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const manager = getMcpManager();
|
|
40
|
+
const state = manager.getState(serverId);
|
|
41
|
+
|
|
42
|
+
if (state) {
|
|
43
|
+
console.log(`\nRuntime State:`);
|
|
44
|
+
console.log(` Status: ${state.status}`);
|
|
45
|
+
if (state.pid) console.log(` PID: ${state.pid}`);
|
|
46
|
+
if (state.initLatencyMs) console.log(` Init: ${state.initLatencyMs}ms`);
|
|
47
|
+
console.log(` Tools: ${state.toolCount}`);
|
|
48
|
+
if (state.lastError) console.log(` Last Error: ${state.lastError}`);
|
|
49
|
+
if (state.lastCallAt) console.log(` Last Call: ${new Date(state.lastCallAt).toISOString()}`);
|
|
50
|
+
} else {
|
|
51
|
+
console.log(`\nRuntime State: not started`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { loadMcpConfig } from '../config';
|
|
2
|
+
import { getMcpManager } from '../index';
|
|
3
|
+
|
|
4
|
+
export async function mcpTools(serverId?: string): Promise<void> {
|
|
5
|
+
const configs = loadMcpConfig();
|
|
6
|
+
|
|
7
|
+
if (configs.length === 0) {
|
|
8
|
+
console.log('No MCP servers configured.');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const manager = getMcpManager();
|
|
13
|
+
const targetConfigs = serverId ? configs.filter(c => c.id === serverId) : configs;
|
|
14
|
+
|
|
15
|
+
if (serverId && targetConfigs.length === 0) {
|
|
16
|
+
console.log(`Server "${serverId}" not found.`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let totalTools = 0;
|
|
21
|
+
|
|
22
|
+
for (const config of targetConfigs) {
|
|
23
|
+
const state = manager.getState(config.id);
|
|
24
|
+
if (!state || state.status !== 'running') {
|
|
25
|
+
console.log(`\n[${config.id}] (${state?.status || 'not started'})`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tools = manager.listTools(config.id);
|
|
30
|
+
console.log(`\n[${config.id}] ${config.name} - ${tools.length} tools`);
|
|
31
|
+
|
|
32
|
+
if (tools.length === 0) {
|
|
33
|
+
console.log(' (no tools)');
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const deny = config.tools.deny || [];
|
|
38
|
+
const allow = config.tools.allow || [];
|
|
39
|
+
|
|
40
|
+
for (const t of tools) {
|
|
41
|
+
let status = 'exposed';
|
|
42
|
+
if (deny.length > 0 && deny.some(p => matchPattern(t.name, p))) {
|
|
43
|
+
status = 'denied';
|
|
44
|
+
} else if (allow.length > 0 && !allow.some(p => matchPattern(t.name, p))) {
|
|
45
|
+
status = 'denied';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const desc = t.description ? ` - ${t.description.slice(0, 60)}` : '';
|
|
49
|
+
console.log(` ${status === 'denied' ? 'x' : '+'} ${t.canonicalId}${desc}`);
|
|
50
|
+
|
|
51
|
+
if (t.inputSchema && typeof t.inputSchema === 'object') {
|
|
52
|
+
const props = (t.inputSchema as any).properties;
|
|
53
|
+
if (props && typeof props === 'object') {
|
|
54
|
+
const required = ((t.inputSchema as any).required || []) as string[];
|
|
55
|
+
for (const [key, schema] of Object.entries(props as Record<string, any>)) {
|
|
56
|
+
const type = schema.type || 'unknown';
|
|
57
|
+
const req = required.includes(key) ? 'required' : 'optional';
|
|
58
|
+
console.log(` - ${key} (${type}, ${req})`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
totalTools++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(`\nTotal: ${totalTools} tools`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function matchPattern(name: string, pattern: string): boolean {
|
|
71
|
+
if (pattern === '*') return true;
|
|
72
|
+
if (pattern === name) return true;
|
|
73
|
+
const regex = new RegExp(
|
|
74
|
+
'^' + pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$'
|
|
75
|
+
);
|
|
76
|
+
return regex.test(name);
|
|
77
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import type { McpServerConfig, McpGlobalConfig } from './types';
|
|
6
|
+
|
|
7
|
+
const MCP_DIR = join(homedir(), '.mosaic', 'mcp');
|
|
8
|
+
const CONFIG_FILE = join(MCP_DIR, 'config.json');
|
|
9
|
+
const SERVERS_DIR = join(MCP_DIR, 'servers');
|
|
10
|
+
|
|
11
|
+
function ensureDirs(): void {
|
|
12
|
+
if (!existsSync(MCP_DIR)) mkdirSync(MCP_DIR, { recursive: true });
|
|
13
|
+
if (!existsSync(SERVERS_DIR)) mkdirSync(SERVERS_DIR, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getDefaultServerConfig(): Partial<McpServerConfig> {
|
|
17
|
+
return {
|
|
18
|
+
enabled: true,
|
|
19
|
+
transport: { type: 'stdio' },
|
|
20
|
+
args: [],
|
|
21
|
+
autostart: 'startup',
|
|
22
|
+
timeouts: {
|
|
23
|
+
initialize: 30000,
|
|
24
|
+
call: 60000,
|
|
25
|
+
},
|
|
26
|
+
limits: {
|
|
27
|
+
maxCallsPerMinute: 60,
|
|
28
|
+
maxPayloadBytes: 1024 * 1024,
|
|
29
|
+
},
|
|
30
|
+
logs: {
|
|
31
|
+
persist: false,
|
|
32
|
+
bufferSize: 200,
|
|
33
|
+
},
|
|
34
|
+
tools: {},
|
|
35
|
+
approval: 'always',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getNavigationServerConfig(): Partial<McpServerConfig> {
|
|
40
|
+
const serverPath = fileURLToPath(new URL('./servers/navigation/index.ts', import.meta.url));
|
|
41
|
+
return {
|
|
42
|
+
id: 'navigation',
|
|
43
|
+
name: 'Navigation',
|
|
44
|
+
native: true,
|
|
45
|
+
command: 'npx',
|
|
46
|
+
args: ['tsx', serverPath],
|
|
47
|
+
enabled: true,
|
|
48
|
+
autostart: 'startup',
|
|
49
|
+
approval: 'never',
|
|
50
|
+
toolApproval: {
|
|
51
|
+
navigation_cookies: 'always',
|
|
52
|
+
navigation_headers: 'always',
|
|
53
|
+
},
|
|
54
|
+
timeouts: {
|
|
55
|
+
initialize: 30000,
|
|
56
|
+
call: 60000,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function validateServerConfig(config: Partial<McpServerConfig>): string[] {
|
|
62
|
+
const errors: string[] = [];
|
|
63
|
+
|
|
64
|
+
if (!config.id || typeof config.id !== 'string') {
|
|
65
|
+
errors.push('Server id is required and must be a string');
|
|
66
|
+
} else if (!/^[a-zA-Z0-9_-]+$/.test(config.id)) {
|
|
67
|
+
errors.push('Server id must contain only alphanumeric characters, hyphens, and underscores');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!config.name || typeof config.name !== 'string') {
|
|
71
|
+
errors.push('Server name is required and must be a string');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!config.command || typeof config.command !== 'string') {
|
|
75
|
+
errors.push('Server command is required and must be a string');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (config.args && !Array.isArray(config.args)) {
|
|
79
|
+
errors.push('Server args must be an array of strings');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (config.autostart && !['startup', 'on-demand', 'never'].includes(config.autostart)) {
|
|
83
|
+
errors.push('Server autostart must be "startup", "on-demand", or "never"');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (config.approval && !['always', 'once-per-tool', 'once-per-server', 'never'].includes(config.approval)) {
|
|
87
|
+
errors.push('Server approval must be "always", "once-per-tool", "once-per-server", or "never"');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return errors;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function mergeWithDefaults(partial: Partial<McpServerConfig>): McpServerConfig {
|
|
94
|
+
const defaults = getDefaultServerConfig();
|
|
95
|
+
return {
|
|
96
|
+
id: partial.id!,
|
|
97
|
+
name: partial.name || partial.id!,
|
|
98
|
+
enabled: partial.enabled ?? defaults.enabled!,
|
|
99
|
+
native: partial.native,
|
|
100
|
+
transport: partial.transport || defaults.transport!,
|
|
101
|
+
command: partial.command!,
|
|
102
|
+
args: partial.args || defaults.args!,
|
|
103
|
+
cwd: partial.cwd,
|
|
104
|
+
env: partial.env,
|
|
105
|
+
autostart: partial.autostart || defaults.autostart!,
|
|
106
|
+
timeouts: { ...defaults.timeouts!, ...partial.timeouts },
|
|
107
|
+
limits: { ...defaults.limits!, ...partial.limits },
|
|
108
|
+
logs: { ...defaults.logs!, ...partial.logs },
|
|
109
|
+
tools: { ...defaults.tools, ...partial.tools },
|
|
110
|
+
approval: partial.approval || defaults.approval!,
|
|
111
|
+
toolApproval: partial.toolApproval,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function loadGlobalConfigFile(): Partial<McpGlobalConfig> {
|
|
116
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
117
|
+
try {
|
|
118
|
+
const content = readFileSync(CONFIG_FILE, 'utf-8');
|
|
119
|
+
return JSON.parse(content);
|
|
120
|
+
} catch {
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function loadServerFiles(): Partial<McpServerConfig>[] {
|
|
126
|
+
if (!existsSync(SERVERS_DIR)) return [];
|
|
127
|
+
const files = readdirSync(SERVERS_DIR).filter(f => f.endsWith('.json'));
|
|
128
|
+
const configs: Partial<McpServerConfig>[] = [];
|
|
129
|
+
|
|
130
|
+
for (const file of files) {
|
|
131
|
+
try {
|
|
132
|
+
const content = readFileSync(join(SERVERS_DIR, file), 'utf-8');
|
|
133
|
+
const parsed = JSON.parse(content);
|
|
134
|
+
if (!parsed.id) {
|
|
135
|
+
parsed.id = file.replace(/\.json$/, '');
|
|
136
|
+
}
|
|
137
|
+
configs.push(parsed);
|
|
138
|
+
} catch {
|
|
139
|
+
// skip invalid files
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return configs;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function loadMcpConfig(): McpServerConfig[] {
|
|
147
|
+
ensureDirs();
|
|
148
|
+
|
|
149
|
+
const globalConfig = loadGlobalConfigFile();
|
|
150
|
+
const serverFiles = loadServerFiles();
|
|
151
|
+
|
|
152
|
+
const configMap = new Map<string, Partial<McpServerConfig>>();
|
|
153
|
+
|
|
154
|
+
if (globalConfig.servers) {
|
|
155
|
+
for (const server of globalConfig.servers) {
|
|
156
|
+
if (server.id) {
|
|
157
|
+
configMap.set(server.id, server);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const server of serverFiles) {
|
|
163
|
+
if (server.id) {
|
|
164
|
+
const existing = configMap.get(server.id);
|
|
165
|
+
if (existing) {
|
|
166
|
+
configMap.set(server.id, { ...existing, ...server });
|
|
167
|
+
} else {
|
|
168
|
+
configMap.set(server.id, server);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!configMap.has('navigation')) {
|
|
174
|
+
configMap.set('navigation', getNavigationServerConfig());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const results: McpServerConfig[] = [];
|
|
178
|
+
for (const [, partial] of configMap) {
|
|
179
|
+
const errors = validateServerConfig(partial);
|
|
180
|
+
if (errors.length === 0) {
|
|
181
|
+
results.push(mergeWithDefaults(partial));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return results;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function saveServerConfig(config: Partial<McpServerConfig>): void {
|
|
189
|
+
ensureDirs();
|
|
190
|
+
const errors = validateServerConfig(config);
|
|
191
|
+
if (errors.length > 0) {
|
|
192
|
+
throw new Error(`Invalid server config: ${errors.join(', ')}`);
|
|
193
|
+
}
|
|
194
|
+
const filePath = join(SERVERS_DIR, `${config.id}.json`);
|
|
195
|
+
writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf-8');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function removeServerConfig(id: string): boolean {
|
|
199
|
+
const filePath = join(SERVERS_DIR, `${id}.json`);
|
|
200
|
+
if (existsSync(filePath)) {
|
|
201
|
+
unlinkSync(filePath);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const globalConfig = loadGlobalConfigFile();
|
|
206
|
+
if (globalConfig.servers) {
|
|
207
|
+
const idx = globalConfig.servers.findIndex(s => s.id === id);
|
|
208
|
+
if (idx !== -1) {
|
|
209
|
+
globalConfig.servers.splice(idx, 1);
|
|
210
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(globalConfig, null, 2), 'utf-8');
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function updateServerConfig(id: string, updates: Partial<McpServerConfig>): McpServerConfig | null {
|
|
219
|
+
const configs = loadMcpConfig();
|
|
220
|
+
const existing = configs.find(c => c.id === id);
|
|
221
|
+
if (!existing) return null;
|
|
222
|
+
|
|
223
|
+
const updated = { ...existing, ...updates, id };
|
|
224
|
+
saveServerConfig(updated);
|
|
225
|
+
return updated;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function getMcpConfigDir(): string {
|
|
229
|
+
return MCP_DIR;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function getServersDir(): string {
|
|
233
|
+
return SERVERS_DIR;
|
|
234
|
+
}
|