@kirosnn/mosaic 0.73.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.
@@ -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