@kirosnn/mosaic 0.73.0 → 0.75.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 -1
- package/package.json +2 -2
- package/src/agent/prompts/systemPrompt.ts +1 -1
- package/src/agent/prompts/toolsPrompt.ts +75 -5
- package/src/agent/tools/explore.ts +4 -4
- package/src/agent/tools/exploreExecutor.ts +56 -4
- package/src/components/Main.tsx +1480 -1459
- package/src/components/main/ChatPage.tsx +858 -858
- package/src/index.tsx +32 -5
- package/src/mcp/approvalPolicy.ts +155 -147
- package/src/mcp/cli/doctor.ts +0 -3
- package/src/mcp/config.ts +234 -223
- package/src/mcp/processManager.ts +303 -298
- 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 +181 -168
- package/src/mcp/types.ts +115 -94
- package/src/utils/history.ts +82 -82
- package/src/utils/markdown.tsx +60 -26
- package/src/utils/toolFormatting.ts +55 -6
- package/src/web/components/MessageItem.tsx +44 -13
- package/src/mcp/servers/navigation.ts +0 -854
package/src/index.tsx
CHANGED
|
@@ -208,14 +208,27 @@ if (parsed.directory) {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
import { addRecentProject } from './utils/config';
|
|
211
|
+
import { appendFileSync } from 'fs';
|
|
212
|
+
import { join } from 'path';
|
|
213
|
+
import { homedir } from 'os';
|
|
214
|
+
|
|
215
|
+
const DEBUG_LOG = join(homedir(), '.mosaic', 'debug.log');
|
|
216
|
+
const debugLog = (msg: string) => {
|
|
217
|
+
try { appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] ${msg}\n`); } catch {}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
debugLog('--- Mosaic starting ---');
|
|
211
221
|
addRecentProject(process.cwd());
|
|
212
222
|
|
|
223
|
+
debugLog('MCP init...');
|
|
213
224
|
const { initializeMcp } = await import('./mcp/index');
|
|
214
|
-
await initializeMcp().catch(() => { });
|
|
225
|
+
await initializeMcp().catch((e) => { debugLog(`MCP init error: ${e}`); });
|
|
226
|
+
debugLog('MCP init done');
|
|
215
227
|
|
|
216
228
|
process.title = '⁘ Mosaic';
|
|
217
229
|
|
|
218
230
|
const cleanup = async (code = 0) => {
|
|
231
|
+
debugLog(`cleanup called with code=${code}`);
|
|
219
232
|
try {
|
|
220
233
|
const { shutdownMcp } = await import('./mcp/index');
|
|
221
234
|
await shutdownMcp();
|
|
@@ -224,17 +237,31 @@ const cleanup = async (code = 0) => {
|
|
|
224
237
|
process.exit(code);
|
|
225
238
|
};
|
|
226
239
|
|
|
227
|
-
process.on('SIGINT', () => cleanup(0));
|
|
228
|
-
process.on('SIGTERM', () => cleanup(0));
|
|
229
|
-
process.on('uncaughtException', () =>
|
|
230
|
-
|
|
240
|
+
process.on('SIGINT', () => { debugLog('SIGINT received'); cleanup(0); });
|
|
241
|
+
process.on('SIGTERM', () => { debugLog('SIGTERM received'); cleanup(0); });
|
|
242
|
+
process.on('uncaughtException', (err) => {
|
|
243
|
+
const msg = `Uncaught exception: ${err?.stack ?? err}`;
|
|
244
|
+
debugLog(msg);
|
|
245
|
+
originalStderrWrite(msg + '\n');
|
|
246
|
+
cleanup(1);
|
|
247
|
+
});
|
|
248
|
+
process.on('unhandledRejection', (reason) => {
|
|
249
|
+
const msg = `Unhandled rejection: ${reason instanceof Error ? reason.stack : reason}`;
|
|
250
|
+
debugLog(msg);
|
|
251
|
+
originalStderrWrite(msg + '\n');
|
|
252
|
+
cleanup(1);
|
|
253
|
+
});
|
|
231
254
|
|
|
232
255
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
233
256
|
|
|
257
|
+
debugLog('Creating renderer...');
|
|
234
258
|
try {
|
|
235
259
|
const renderer = await createCliRenderer();
|
|
260
|
+
debugLog('Renderer created, mounting React...');
|
|
236
261
|
createRoot(renderer).render(<App initialMessage={parsed.initialMessage} />);
|
|
262
|
+
debugLog('React mounted');
|
|
237
263
|
} catch (error) {
|
|
264
|
+
debugLog(`Renderer/React error: ${error}`);
|
|
238
265
|
console.error(error);
|
|
239
266
|
cleanup(1);
|
|
240
267
|
}
|
|
@@ -1,148 +1,156 @@
|
|
|
1
|
-
import { createHash } from 'crypto';
|
|
2
|
-
import { requestApproval } from '../utils/approvalBridge';
|
|
3
|
-
import type { McpApprovalCacheEntry, McpRiskHint, McpServerConfig } from './types';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
request.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { requestApproval } from '../utils/approvalBridge';
|
|
3
|
+
import type { McpApprovalCacheEntry, McpRiskHint, McpServerConfig } from './types';
|
|
4
|
+
import { isNativeMcpServer } from './types';
|
|
5
|
+
|
|
6
|
+
const READ_KEYWORDS = ['read', 'get', 'list', 'search', 'find', 'show', 'describe', 'query', 'fetch', 'inspect', 'view', 'ls', 'cat'];
|
|
7
|
+
const WRITE_KEYWORDS = ['write', 'set', 'create', 'update', 'put', 'save', 'modify', 'patch', 'upsert', 'insert', 'append'];
|
|
8
|
+
const EXEC_KEYWORDS = ['exec', 'run', 'execute', 'spawn', 'shell', 'eval', 'invoke', 'call', 'process'];
|
|
9
|
+
const DELETE_KEYWORDS = ['delete', 'remove', 'destroy', 'drop', 'purge', 'clean', 'wipe', 'rm', 'unlink'];
|
|
10
|
+
const NET_KEYWORDS = ['http', 'request', 'download', 'upload', 'send', 'post', 'api', 'webhook', 'socket', 'connect'];
|
|
11
|
+
|
|
12
|
+
export class McpApprovalPolicy {
|
|
13
|
+
private cache = new Map<string, McpApprovalCacheEntry>();
|
|
14
|
+
|
|
15
|
+
inferRiskHint(toolName: string, _args: Record<string, unknown>): McpRiskHint {
|
|
16
|
+
const lower = toolName.toLowerCase();
|
|
17
|
+
|
|
18
|
+
for (const kw of DELETE_KEYWORDS) {
|
|
19
|
+
if (lower.includes(kw)) return 'execute';
|
|
20
|
+
}
|
|
21
|
+
for (const kw of EXEC_KEYWORDS) {
|
|
22
|
+
if (lower.includes(kw)) return 'execute';
|
|
23
|
+
}
|
|
24
|
+
for (const kw of WRITE_KEYWORDS) {
|
|
25
|
+
if (lower.includes(kw)) return 'write';
|
|
26
|
+
}
|
|
27
|
+
for (const kw of NET_KEYWORDS) {
|
|
28
|
+
if (lower.includes(kw)) return 'network';
|
|
29
|
+
}
|
|
30
|
+
for (const kw of READ_KEYWORDS) {
|
|
31
|
+
if (lower.includes(kw)) return 'read';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return 'unknown';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async requestMcpApproval(request: {
|
|
38
|
+
serverId: string;
|
|
39
|
+
serverName: string;
|
|
40
|
+
toolName: string;
|
|
41
|
+
canonicalId: string;
|
|
42
|
+
args: Record<string, unknown>;
|
|
43
|
+
approvalMode: McpServerConfig['approval'];
|
|
44
|
+
}): Promise<{ approved: boolean; customResponse?: string }> {
|
|
45
|
+
if (request.approvalMode === 'never') {
|
|
46
|
+
return { approved: true };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const riskHint = this.inferRiskHint(request.toolName, request.args);
|
|
50
|
+
|
|
51
|
+
if (this.checkCache(request.serverId, request.toolName, request.args)) {
|
|
52
|
+
return { approved: true };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const argsStr = formatArgs(request.args);
|
|
56
|
+
const payloadSize = JSON.stringify(request.args).length;
|
|
57
|
+
|
|
58
|
+
const isNative = isNativeMcpServer(request.serverId);
|
|
59
|
+
const preview = {
|
|
60
|
+
title: isNative ? request.toolName : `MCP: ${request.serverName} / ${request.toolName}`,
|
|
61
|
+
content: argsStr,
|
|
62
|
+
details: isNative
|
|
63
|
+
? [
|
|
64
|
+
`Tool: ${request.toolName}`,
|
|
65
|
+
`Risk: ${riskHint}`,
|
|
66
|
+
`Payload: ${payloadSize} bytes`,
|
|
67
|
+
]
|
|
68
|
+
: [
|
|
69
|
+
`Server: ${request.serverName} (${request.serverId})`,
|
|
70
|
+
`Tool: ${request.toolName}`,
|
|
71
|
+
`Risk: ${riskHint}`,
|
|
72
|
+
`Payload: ${payloadSize} bytes`,
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const mcpMeta = {
|
|
77
|
+
serverId: request.serverId,
|
|
78
|
+
serverName: request.serverName,
|
|
79
|
+
canonicalId: request.canonicalId,
|
|
80
|
+
riskHint,
|
|
81
|
+
payloadSize,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const result = await requestApproval(
|
|
85
|
+
request.canonicalId,
|
|
86
|
+
{ ...request.args, __mcpMeta: mcpMeta },
|
|
87
|
+
preview
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (result.approved && request.approvalMode !== 'always') {
|
|
91
|
+
this.addToCache(request.serverId, request.toolName, request.args, request.approvalMode);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private checkCache(serverId: string, toolName: string, args: Record<string, unknown>): boolean {
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
|
|
100
|
+
const serverKey = `server:${serverId}`;
|
|
101
|
+
const serverEntry = this.cache.get(serverKey);
|
|
102
|
+
if (serverEntry && serverEntry.expiresAt > now) return true;
|
|
103
|
+
|
|
104
|
+
const toolKey = `tool:${serverId}:${toolName}`;
|
|
105
|
+
const toolEntry = this.cache.get(toolKey);
|
|
106
|
+
if (toolEntry && toolEntry.expiresAt > now) return true;
|
|
107
|
+
|
|
108
|
+
const argsHash = hashArgs(args);
|
|
109
|
+
const argsKey = `toolArgs:${serverId}:${toolName}:${argsHash}`;
|
|
110
|
+
const argsEntry = this.cache.get(argsKey);
|
|
111
|
+
if (argsEntry && argsEntry.expiresAt > now) return true;
|
|
112
|
+
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private addToCache(serverId: string, toolName: string, _args: Record<string, unknown>, mode: McpServerConfig['approval']): void {
|
|
117
|
+
const ttl = 300000;
|
|
118
|
+
const expiresAt = Date.now() + ttl;
|
|
119
|
+
|
|
120
|
+
switch (mode) {
|
|
121
|
+
case 'once-per-server': {
|
|
122
|
+
const key = `server:${serverId}`;
|
|
123
|
+
this.cache.set(key, { scope: 'server', key, expiresAt });
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case 'once-per-tool': {
|
|
127
|
+
const key = `tool:${serverId}:${toolName}`;
|
|
128
|
+
this.cache.set(key, { scope: 'tool', key, expiresAt });
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
clearCache(): void {
|
|
135
|
+
this.cache.clear();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function hashArgs(args: Record<string, unknown>): string {
|
|
140
|
+
const str = JSON.stringify(args, Object.keys(args).sort());
|
|
141
|
+
return createHash('sha256').update(str).digest('hex').slice(0, 12);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function formatArgs(args: Record<string, unknown>): string {
|
|
145
|
+
const entries = Object.entries(args);
|
|
146
|
+
if (entries.length === 0) return '(no arguments)';
|
|
147
|
+
|
|
148
|
+
const lines: string[] = [];
|
|
149
|
+
for (const [key, value] of entries) {
|
|
150
|
+
const strValue = typeof value === 'string'
|
|
151
|
+
? (value.length > 100 ? value.slice(0, 100) + '...' : value)
|
|
152
|
+
: JSON.stringify(value);
|
|
153
|
+
lines.push(` ${key}: ${strValue}`);
|
|
154
|
+
}
|
|
155
|
+
return lines.join('\n');
|
|
148
156
|
}
|
package/src/mcp/cli/doctor.ts
CHANGED
|
@@ -23,10 +23,8 @@ export async function mcpDoctor(): Promise<void> {
|
|
|
23
23
|
for (const config of configs) {
|
|
24
24
|
console.log(`--- ${config.id} (${config.name}) ---`);
|
|
25
25
|
|
|
26
|
-
// 1. Config validation
|
|
27
26
|
console.log(' [config] OK');
|
|
28
27
|
|
|
29
|
-
// 2. Check command resolves
|
|
30
28
|
const commandExists = await checkCommand(config.command);
|
|
31
29
|
if (commandExists) {
|
|
32
30
|
console.log(` [command] "${config.command}" found`);
|
|
@@ -40,7 +38,6 @@ export async function mcpDoctor(): Promise<void> {
|
|
|
40
38
|
continue;
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
// 3. Try start + init + list tools
|
|
44
41
|
try {
|
|
45
42
|
const state = await manager.startServer(config);
|
|
46
43
|
|