@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/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', () => cleanup(1));
230
- process.on('unhandledRejection', () => cleanup(1));
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
- const READ_KEYWORDS = ['read', 'get', 'list', 'search', 'find', 'show', 'describe', 'query', 'fetch', 'inspect', 'view', 'ls', 'cat'];
6
- const WRITE_KEYWORDS = ['write', 'set', 'create', 'update', 'put', 'save', 'modify', 'patch', 'upsert', 'insert', 'append'];
7
- const EXEC_KEYWORDS = ['exec', 'run', 'execute', 'spawn', 'shell', 'eval', 'invoke', 'call', 'process'];
8
- const DELETE_KEYWORDS = ['delete', 'remove', 'destroy', 'drop', 'purge', 'clean', 'wipe', 'rm', 'unlink'];
9
- const NET_KEYWORDS = ['http', 'request', 'download', 'upload', 'send', 'post', 'api', 'webhook', 'socket', 'connect'];
10
-
11
- export class McpApprovalPolicy {
12
- private cache = new Map<string, McpApprovalCacheEntry>();
13
-
14
- inferRiskHint(toolName: string, _args: Record<string, unknown>): McpRiskHint {
15
- const lower = toolName.toLowerCase();
16
-
17
- for (const kw of DELETE_KEYWORDS) {
18
- if (lower.includes(kw)) return 'execute';
19
- }
20
- for (const kw of EXEC_KEYWORDS) {
21
- if (lower.includes(kw)) return 'execute';
22
- }
23
- for (const kw of WRITE_KEYWORDS) {
24
- if (lower.includes(kw)) return 'write';
25
- }
26
- for (const kw of NET_KEYWORDS) {
27
- if (lower.includes(kw)) return 'network';
28
- }
29
- for (const kw of READ_KEYWORDS) {
30
- if (lower.includes(kw)) return 'read';
31
- }
32
-
33
- return 'unknown';
34
- }
35
-
36
- async requestMcpApproval(request: {
37
- serverId: string;
38
- serverName: string;
39
- toolName: string;
40
- canonicalId: string;
41
- args: Record<string, unknown>;
42
- approvalMode: McpServerConfig['approval'];
43
- }): Promise<{ approved: boolean; customResponse?: string }> {
44
- if (request.approvalMode === 'never') {
45
- return { approved: true };
46
- }
47
-
48
- const riskHint = this.inferRiskHint(request.toolName, request.args);
49
-
50
- if (this.checkCache(request.serverId, request.toolName, request.args)) {
51
- return { approved: true };
52
- }
53
-
54
- const argsStr = formatArgs(request.args);
55
- const payloadSize = JSON.stringify(request.args).length;
56
-
57
- const preview = {
58
- title: `MCP: ${request.serverName} / ${request.toolName}`,
59
- content: argsStr,
60
- details: [
61
- `Server: ${request.serverName} (${request.serverId})`,
62
- `Tool: ${request.toolName}`,
63
- `Risk: ${riskHint}`,
64
- `Payload: ${payloadSize} bytes`,
65
- ],
66
- };
67
-
68
- const mcpMeta = {
69
- serverId: request.serverId,
70
- serverName: request.serverName,
71
- canonicalId: request.canonicalId,
72
- riskHint,
73
- payloadSize,
74
- };
75
-
76
- const result = await requestApproval(
77
- request.canonicalId,
78
- { ...request.args, __mcpMeta: mcpMeta },
79
- preview
80
- );
81
-
82
- if (result.approved && request.approvalMode !== 'always') {
83
- this.addToCache(request.serverId, request.toolName, request.args, request.approvalMode);
84
- }
85
-
86
- return result;
87
- }
88
-
89
- private checkCache(serverId: string, toolName: string, args: Record<string, unknown>): boolean {
90
- const now = Date.now();
91
-
92
- const serverKey = `server:${serverId}`;
93
- const serverEntry = this.cache.get(serverKey);
94
- if (serverEntry && serverEntry.expiresAt > now) return true;
95
-
96
- const toolKey = `tool:${serverId}:${toolName}`;
97
- const toolEntry = this.cache.get(toolKey);
98
- if (toolEntry && toolEntry.expiresAt > now) return true;
99
-
100
- const argsHash = hashArgs(args);
101
- const argsKey = `toolArgs:${serverId}:${toolName}:${argsHash}`;
102
- const argsEntry = this.cache.get(argsKey);
103
- if (argsEntry && argsEntry.expiresAt > now) return true;
104
-
105
- return false;
106
- }
107
-
108
- private addToCache(serverId: string, toolName: string, _args: Record<string, unknown>, mode: McpServerConfig['approval']): void {
109
- const ttl = 300000;
110
- const expiresAt = Date.now() + ttl;
111
-
112
- switch (mode) {
113
- case 'once-per-server': {
114
- const key = `server:${serverId}`;
115
- this.cache.set(key, { scope: 'server', key, expiresAt });
116
- break;
117
- }
118
- case 'once-per-tool': {
119
- const key = `tool:${serverId}:${toolName}`;
120
- this.cache.set(key, { scope: 'tool', key, expiresAt });
121
- break;
122
- }
123
- }
124
- }
125
-
126
- clearCache(): void {
127
- this.cache.clear();
128
- }
129
- }
130
-
131
- function hashArgs(args: Record<string, unknown>): string {
132
- const str = JSON.stringify(args, Object.keys(args).sort());
133
- return createHash('sha256').update(str).digest('hex').slice(0, 12);
134
- }
135
-
136
- function formatArgs(args: Record<string, unknown>): string {
137
- const entries = Object.entries(args);
138
- if (entries.length === 0) return '(no arguments)';
139
-
140
- const lines: string[] = [];
141
- for (const [key, value] of entries) {
142
- const strValue = typeof value === 'string'
143
- ? (value.length > 100 ? value.slice(0, 100) + '...' : value)
144
- : JSON.stringify(value);
145
- lines.push(` ${key}: ${strValue}`);
146
- }
147
- return lines.join('\n');
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
  }
@@ -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