@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.
Files changed (79) hide show
  1. package/README.md +1 -5
  2. package/package.json +4 -2
  3. package/src/agent/Agent.ts +353 -131
  4. package/src/agent/context.ts +4 -4
  5. package/src/agent/prompts/systemPrompt.ts +15 -6
  6. package/src/agent/prompts/toolsPrompt.ts +136 -10
  7. package/src/agent/provider/anthropic.ts +100 -100
  8. package/src/agent/provider/google.ts +102 -102
  9. package/src/agent/provider/mistral.ts +95 -95
  10. package/src/agent/provider/ollama.ts +77 -60
  11. package/src/agent/provider/openai.ts +42 -38
  12. package/src/agent/provider/rateLimit.ts +178 -0
  13. package/src/agent/provider/xai.ts +99 -99
  14. package/src/agent/tools/definitions.ts +19 -9
  15. package/src/agent/tools/executor.ts +95 -85
  16. package/src/agent/tools/exploreExecutor.ts +8 -10
  17. package/src/agent/tools/grep.ts +30 -29
  18. package/src/agent/tools/question.ts +7 -1
  19. package/src/agent/types.ts +9 -8
  20. package/src/components/App.tsx +45 -45
  21. package/src/components/CustomInput.tsx +214 -36
  22. package/src/components/Main.tsx +552 -339
  23. package/src/components/Setup.tsx +1 -1
  24. package/src/components/Welcome.tsx +1 -1
  25. package/src/components/main/ApprovalPanel.tsx +4 -3
  26. package/src/components/main/ChatPage.tsx +858 -675
  27. package/src/components/main/HomePage.tsx +53 -38
  28. package/src/components/main/QuestionPanel.tsx +52 -7
  29. package/src/components/main/ThinkingIndicator.tsx +2 -1
  30. package/src/index.tsx +50 -20
  31. package/src/mcp/approvalPolicy.ts +156 -0
  32. package/src/mcp/cli/add.ts +185 -0
  33. package/src/mcp/cli/doctor.ts +74 -0
  34. package/src/mcp/cli/index.ts +85 -0
  35. package/src/mcp/cli/list.ts +50 -0
  36. package/src/mcp/cli/logs.ts +24 -0
  37. package/src/mcp/cli/manage.ts +99 -0
  38. package/src/mcp/cli/show.ts +53 -0
  39. package/src/mcp/cli/tools.ts +77 -0
  40. package/src/mcp/config.ts +234 -0
  41. package/src/mcp/index.ts +80 -0
  42. package/src/mcp/processManager.ts +304 -0
  43. package/src/mcp/rateLimiter.ts +50 -0
  44. package/src/mcp/registry.ts +151 -0
  45. package/src/mcp/schemaConverter.ts +100 -0
  46. package/src/mcp/servers/navigation/browser.ts +151 -0
  47. package/src/mcp/servers/navigation/index.ts +23 -0
  48. package/src/mcp/servers/navigation/tools.ts +263 -0
  49. package/src/mcp/servers/navigation/types.ts +17 -0
  50. package/src/mcp/servers/navigation/utils.ts +20 -0
  51. package/src/mcp/toolCatalog.ts +182 -0
  52. package/src/mcp/types.ts +116 -0
  53. package/src/utils/approvalBridge.ts +17 -5
  54. package/src/utils/commands/compact.ts +30 -0
  55. package/src/utils/commands/echo.ts +1 -1
  56. package/src/utils/commands/index.ts +4 -6
  57. package/src/utils/commands/new.ts +15 -0
  58. package/src/utils/commands/types.ts +3 -0
  59. package/src/utils/config.ts +3 -1
  60. package/src/utils/diffRendering.tsx +1 -3
  61. package/src/utils/exploreBridge.ts +10 -0
  62. package/src/utils/markdown.tsx +220 -122
  63. package/src/utils/models.ts +31 -9
  64. package/src/utils/questionBridge.ts +36 -1
  65. package/src/utils/tokenEstimator.ts +32 -0
  66. package/src/utils/toolFormatting.ts +317 -7
  67. package/src/web/app.tsx +72 -72
  68. package/src/web/components/HomePage.tsx +7 -7
  69. package/src/web/components/MessageItem.tsx +66 -35
  70. package/src/web/components/QuestionPanel.tsx +72 -12
  71. package/src/web/components/Sidebar.tsx +0 -2
  72. package/src/web/components/ThinkingIndicator.tsx +1 -0
  73. package/src/web/server.tsx +767 -683
  74. package/src/utils/commands/redo.ts +0 -74
  75. package/src/utils/commands/sessions.ts +0 -129
  76. package/src/utils/commands/undo.ts +0 -75
  77. package/src/utils/undoRedo.ts +0 -429
  78. package/src/utils/undoRedoBridge.ts +0 -45
  79. package/src/utils/undoRedoDb.ts +0 -338
@@ -0,0 +1,151 @@
1
+ import { chromium, firefox, webkit, type Browser, type BrowserContext, type Page } from 'playwright';
2
+ import { LaunchConfig, ContextConfig } from './types.js';
3
+
4
+ const browserTypes = { chromium, firefox, webkit };
5
+
6
+ let browser: Browser | null = null;
7
+ let context: BrowserContext | null = null;
8
+
9
+ export const launchConfig: LaunchConfig = {
10
+ browserType: 'chromium',
11
+ headless: true,
12
+ args: [
13
+ '--disable-blink-features=AutomationControlled',
14
+ '--disable-infobars',
15
+ '--no-sandbox',
16
+ '--disable-setuid-sandbox',
17
+ '--ignore-certificate-errors',
18
+ '--disable-features=IsolateOrigins,site-per-process',
19
+ ],
20
+ };
21
+
22
+ const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
23
+ const DEFAULT_VIEWPORT = { width: 1280, height: 720 };
24
+ const DEFAULT_PAGE_TIMEOUT = 30000;
25
+ const DEFAULT_NAV_TIMEOUT = 60000;
26
+
27
+ export const contextConfig: ContextConfig = {
28
+ userAgent: DEFAULT_USER_AGENT,
29
+ viewport: DEFAULT_VIEWPORT,
30
+ };
31
+
32
+ const tabs = new Map<string, Page>();
33
+ const tabOrder: string[] = [];
34
+ const pageToId = new WeakMap<Page, string>();
35
+ let currentTabId: string | null = null;
36
+ let tabSeq = 0;
37
+
38
+ function configurePage(page: Page): void {
39
+ page.setDefaultTimeout(DEFAULT_PAGE_TIMEOUT);
40
+ page.setDefaultNavigationTimeout(DEFAULT_NAV_TIMEOUT);
41
+ }
42
+
43
+ export function registerPage(page: Page): string {
44
+ const existing = pageToId.get(page);
45
+ if (existing) return existing;
46
+ configurePage(page);
47
+ const id = `tab_${++tabSeq}`;
48
+ pageToId.set(page, id);
49
+ tabs.set(id, page);
50
+ tabOrder.push(id);
51
+ currentTabId = id;
52
+ page.on('close', () => {
53
+ tabs.delete(id);
54
+ const index = tabOrder.indexOf(id);
55
+ if (index >= 0) tabOrder.splice(index, 1);
56
+ if (currentTabId === id) {
57
+ currentTabId = tabOrder.length > 0 ? tabOrder[tabOrder.length - 1]! : null;
58
+ }
59
+ });
60
+ return id;
61
+ }
62
+
63
+ export async function ensureBrowser(): Promise<Browser> {
64
+ if (browser) return browser;
65
+ const launcher = browserTypes[launchConfig.browserType];
66
+ browser = await launcher.launch({
67
+ headless: launchConfig.headless,
68
+ channel: launchConfig.channel,
69
+ executablePath: launchConfig.executablePath,
70
+ args: [
71
+ ...launchConfig.args,
72
+ ...(launchConfig.args.length > 0 ? [] : [])
73
+ ],
74
+ });
75
+ return browser;
76
+ }
77
+
78
+ export async function ensureContext(): Promise<BrowserContext> {
79
+ if (context) return context;
80
+ const b = await ensureBrowser();
81
+ context = await b.newContext({
82
+ userAgent: contextConfig.userAgent,
83
+ viewport: contextConfig.viewport === null ? null : contextConfig.viewport,
84
+ extraHTTPHeaders: contextConfig.extraHTTPHeaders,
85
+ locale: 'en-US',
86
+ timezoneId: contextConfig.timezoneId,
87
+ deviceScaleFactor: 1,
88
+ isMobile: false,
89
+ hasTouch: false,
90
+ javaScriptEnabled: true,
91
+ });
92
+
93
+ await context.addInitScript(() => {
94
+ Object.defineProperty(navigator, 'webdriver', {
95
+ get: () => undefined,
96
+ });
97
+
98
+ if (!(window as any).chrome) {
99
+ (window as any).chrome = {
100
+ runtime: {},
101
+ loadTimes: function () { },
102
+ csi: function () { },
103
+ app: {},
104
+ };
105
+ }
106
+
107
+ if (navigator.plugins.length === 0) {
108
+ Object.defineProperty(navigator, 'plugins', {
109
+ get: () => {
110
+ return [
111
+ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
112
+ { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: 'Portable Document Format' },
113
+ { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' }
114
+ ];
115
+ },
116
+ });
117
+ }
118
+
119
+ Object.defineProperty(navigator, 'languages', {
120
+ get: () => ['en-US', 'en'],
121
+ });
122
+ });
123
+
124
+ context.on('page', page => {
125
+ registerPage(page);
126
+ });
127
+ return context;
128
+ }
129
+
130
+ export async function ensurePage(tabId?: string): Promise<{ page: Page; tabId: string }> {
131
+ const ctx = await ensureContext();
132
+ if (tabId) {
133
+ const existing = tabs.get(tabId);
134
+ if (!existing) {
135
+ throw new Error(`Tab not found: ${tabId}`);
136
+ }
137
+ currentTabId = tabId;
138
+ return { page: existing, tabId };
139
+ }
140
+ if (currentTabId) {
141
+ const current = tabs.get(currentTabId);
142
+ if (current) return { page: current, tabId: currentTabId };
143
+ }
144
+ const page = await ctx.newPage();
145
+ const id = registerPage(page);
146
+ return { page, tabId: id };
147
+ }
148
+
149
+ export function getTabs() {
150
+ return tabs;
151
+ }
@@ -0,0 +1,23 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { ensureContext } from './browser.js';
4
+ import { registerTools } from './tools.js';
5
+
6
+ const server = new McpServer({ name: 'navigation', version: '1.0.0' });
7
+
8
+ registerTools(server);
9
+
10
+ async function main(): Promise<void> {
11
+ const transport = new StdioServerTransport();
12
+ await server.connect(transport);
13
+ try {
14
+ await ensureContext();
15
+ } catch {
16
+ // Browser will be launched on first tool call if pre-launch fails
17
+ }
18
+ }
19
+
20
+ main().catch(error => {
21
+ console.error(error);
22
+ process.exit(1);
23
+ });
@@ -0,0 +1,263 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import type { Page } from 'playwright';
4
+ import { ensureContext, ensurePage, registerPage } from './browser.js';
5
+ import { randomDelay } from './utils.js';
6
+
7
+ type SearchEngine = 'duckduckgo' | 'google' | 'bing';
8
+
9
+ interface SearchResult {
10
+ text: string;
11
+ href: string;
12
+ snippet: string;
13
+ }
14
+
15
+ interface EngineConfig {
16
+ buildUrl: (query: string) => string;
17
+ extract: (page: Page, limit: number) => Promise<SearchResult[]>;
18
+ }
19
+
20
+ async function resolveRedirects(page: Page, results: SearchResult[]): Promise<SearchResult[]> {
21
+ return page.evaluate((results) => {
22
+ return results.map(r => {
23
+ try {
24
+ const url = new URL(r.href);
25
+ if (url.hostname.includes('bing.com') && url.pathname === '/ck/a') {
26
+ const u = url.searchParams.get('u');
27
+ if (u) {
28
+ const decoded = atob(u.replace(/^a1/, ''));
29
+ if (decoded.startsWith('http')) return { ...r, href: decoded };
30
+ }
31
+ }
32
+ } catch {}
33
+ return r;
34
+ });
35
+ }, results);
36
+ }
37
+
38
+ const engines: Record<SearchEngine, EngineConfig> = {
39
+ bing: {
40
+ buildUrl: (q) => `https://www.bing.com/search?q=${encodeURIComponent(q)}`,
41
+ extract: async (page, limit) => {
42
+ const raw = await page.evaluate((limit) => {
43
+ const items: { text: string; href: string; snippet: string }[] = [];
44
+ const results = document.querySelectorAll('.b_algo');
45
+
46
+ for (const el of Array.from(results)) {
47
+ if (items.length >= limit) break;
48
+
49
+ const linkEl = el.querySelector('h2 a');
50
+ const snippetEl = el.querySelector('.b_caption p, p');
51
+
52
+ const text = linkEl?.textContent?.trim();
53
+ const href = linkEl?.getAttribute('href');
54
+ const snippet = snippetEl?.textContent?.trim() || '';
55
+
56
+ if (text && href && href.startsWith('http')) {
57
+ items.push({ text, href, snippet });
58
+ }
59
+ }
60
+ return items;
61
+ }, limit);
62
+ return resolveRedirects(page, raw);
63
+ },
64
+ },
65
+ duckduckgo: {
66
+ buildUrl: (q) => `https://duckduckgo.com/?q=${encodeURIComponent(q)}`,
67
+ extract: async (page, limit) => {
68
+ return page.evaluate((limit) => {
69
+ const items: { text: string; href: string; snippet: string }[] = [];
70
+ const selectors = ['article[data-testid="result"]', '.result', '.web-result', '.results .result__body'];
71
+
72
+ for (const selector of selectors) {
73
+ const elements = document.querySelectorAll(selector);
74
+ if (elements.length === 0) continue;
75
+
76
+ for (const el of Array.from(elements)) {
77
+ if (items.length >= limit) break;
78
+
79
+ const titleEl = el.querySelector('h2 a, a[data-testid="result-title-a"], .result__a');
80
+ const snippetEl = el.querySelector('[data-result="snippet"], .result__snippet, span');
81
+
82
+ const text = titleEl?.textContent?.trim();
83
+ const href = titleEl?.getAttribute('href');
84
+ const snippet = snippetEl?.textContent?.trim() || '';
85
+
86
+ if (text && href && href.startsWith('http')) {
87
+ items.push({ text, href, snippet });
88
+ }
89
+ }
90
+ if (items.length > 0) break;
91
+ }
92
+ return items;
93
+ }, limit);
94
+ },
95
+ },
96
+ google: {
97
+ buildUrl: (q) => `https://www.google.com/search?q=${encodeURIComponent(q)}&udm=14`,
98
+ extract: async (page, limit) => {
99
+ return page.evaluate((limit) => {
100
+ const items: { text: string; href: string; snippet: string }[] = [];
101
+ const results = document.querySelectorAll('#search .g');
102
+
103
+ for (const el of Array.from(results)) {
104
+ if (items.length >= limit) break;
105
+
106
+ const titleEl = el.querySelector('h3');
107
+ const linkEl = el.querySelector('a');
108
+ const snippetEl = el.querySelector('[style*="-webkit-line-clamp"], .VwiC3b, div[style*="line-height"]');
109
+
110
+ const text = titleEl?.textContent?.trim();
111
+ const href = linkEl?.getAttribute('href');
112
+ const snippet = snippetEl?.textContent?.trim() || '';
113
+
114
+ if (text && href && href.startsWith('http')) {
115
+ items.push({ text, href, snippet });
116
+ }
117
+ }
118
+ return items;
119
+ }, limit);
120
+ },
121
+ },
122
+ };
123
+
124
+ async function extractFallbackResults(page: Page, limit: number): Promise<SearchResult[]> {
125
+ return page.evaluate((limit) => {
126
+ const items: { text: string; href: string; snippet: string }[] = [];
127
+ const seen = new Set<string>();
128
+ const links = document.querySelectorAll('a[href^="http"]');
129
+
130
+ for (const link of Array.from(links)) {
131
+ if (items.length >= limit) break;
132
+
133
+ const href = link.getAttribute('href');
134
+ if (!href || seen.has(href)) continue;
135
+
136
+ try {
137
+ const host = new URL(href).hostname;
138
+ if (['google.', 'bing.', 'duckduckgo.', 'yahoo.'].some(d => host.includes(d))) continue;
139
+ } catch { continue; }
140
+
141
+ const text = link.textContent?.trim();
142
+ if (!text || text.length < 3) continue;
143
+
144
+ seen.add(href);
145
+ const parent = link.closest('div, li, article, section');
146
+ const snippet = parent?.textContent?.trim().slice(0, 200) || '';
147
+
148
+ items.push({ text, href, snippet });
149
+ }
150
+ return items;
151
+ }, limit);
152
+ }
153
+
154
+ async function dismissConsentDialogs(page: Page): Promise<void> {
155
+ try {
156
+ const consentButton = page.locator(
157
+ 'button:has-text("Tout refuser"), button:has-text("Reject all"), ' +
158
+ 'button:has-text("Alle ablehnen"), button:has-text("Accept"), ' +
159
+ 'button:has-text("I agree"), button:has-text("Accepter")'
160
+ ).first();
161
+ if (await consentButton.isVisible({ timeout: 1500 })) {
162
+ await consentButton.click();
163
+ await page.waitForLoadState('domcontentloaded', { timeout: 3000 }).catch(() => {});
164
+ }
165
+ } catch {
166
+ // No consent dialog
167
+ }
168
+ }
169
+
170
+ function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
171
+ return Promise.race([
172
+ promise,
173
+ new Promise<never>((_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)),
174
+ ]);
175
+ }
176
+
177
+ export function registerTools(server: McpServer) {
178
+ server.registerTool('navigation_search', {
179
+ description: 'Search the web and return top results (titles, links, snippets). Defaults to Bing. Supports bing, duckduckgo, and google engines.',
180
+ inputSchema: {
181
+ query: z.string(),
182
+ limit: z.number().optional(),
183
+ engine: z.enum(['bing', 'duckduckgo', 'google']).optional(),
184
+ newTab: z.boolean().optional(),
185
+ },
186
+ }, async (args) => {
187
+ const engineName: SearchEngine = args.engine ?? 'bing';
188
+ const engine = engines[engineName];
189
+ const limit = args.limit ?? 8;
190
+
191
+ const doSearch = async () => {
192
+ const target = args.newTab ? await ensureContext().then(async ctx => {
193
+ const page = await ctx.newPage();
194
+ const tabId = registerPage(page);
195
+ return { page, tabId };
196
+ }) : await ensurePage();
197
+
198
+ const page = target.page;
199
+ const url = engine.buildUrl(args.query);
200
+
201
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 10000 });
202
+
203
+ await dismissConsentDialogs(page);
204
+
205
+ await randomDelay(300, 600);
206
+
207
+ let results = await engine.extract(page, limit);
208
+
209
+ if (results.length === 0) {
210
+ results = await extractFallbackResults(page, limit);
211
+ }
212
+
213
+ const currentUrl = page.url();
214
+ const pageTitle = await page.title();
215
+
216
+ if (results.length === 0) {
217
+ return {
218
+ content: [{
219
+ type: 'text' as const,
220
+ text: JSON.stringify({
221
+ tabId: target.tabId,
222
+ engine: engineName,
223
+ url: currentUrl,
224
+ pageTitle,
225
+ resultCount: 0,
226
+ results: [],
227
+ note: 'No results extracted. The page may have shown a CAPTCHA, consent wall, or unexpected layout. Check pageTitle and url for clues.',
228
+ }, null, 2),
229
+ }],
230
+ };
231
+ }
232
+
233
+ return {
234
+ content: [{
235
+ type: 'text' as const,
236
+ text: JSON.stringify({
237
+ tabId: target.tabId,
238
+ engine: engineName,
239
+ url: currentUrl,
240
+ resultCount: results.length,
241
+ results,
242
+ }, null, 2),
243
+ }],
244
+ };
245
+ };
246
+
247
+ try {
248
+ return await withTimeout(doSearch(), 45000, 'navigation_search');
249
+ } catch (error) {
250
+ const msg = error instanceof Error ? error.message : String(error);
251
+ return {
252
+ content: [{
253
+ type: 'text',
254
+ text: JSON.stringify({
255
+ engine: engineName,
256
+ error: `Search failed: ${msg}`,
257
+ }, null, 2),
258
+ }],
259
+ isError: true,
260
+ };
261
+ }
262
+ });
263
+ }
@@ -0,0 +1,17 @@
1
+ export type BrowserTypeName = 'chromium' | 'firefox' | 'webkit';
2
+
3
+ export type LaunchConfig = {
4
+ browserType: BrowserTypeName;
5
+ headless: boolean;
6
+ channel?: string;
7
+ executablePath?: string;
8
+ args: string[];
9
+ };
10
+
11
+ export type ContextConfig = {
12
+ userAgent?: string;
13
+ viewport?: { width: number; height: number } | null;
14
+ extraHTTPHeaders?: Record<string, string>;
15
+ locale?: string;
16
+ timezoneId?: string;
17
+ };
@@ -0,0 +1,20 @@
1
+ import type { Page } from 'playwright';
2
+
3
+ export const randomDelay = (min: number, max: number) => new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * (max - min + 1)) + min));
4
+
5
+ export async function humanType(page: any, selector: string, text: string) {
6
+ const element = page.locator(selector);
7
+ await element.click();
8
+ for (const char of text) {
9
+ await page.keyboard.type(char, { delay: Math.random() * 100 + 50 });
10
+ }
11
+ }
12
+
13
+ export async function waitForStability(page: Page, timeout = 10000): Promise<void> {
14
+ try {
15
+ await page.waitForLoadState('networkidle', { timeout });
16
+ } catch {
17
+ await page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {});
18
+ await randomDelay(300, 600);
19
+ }
20
+ }
@@ -0,0 +1,182 @@
1
+ import { tool, type CoreTool } from 'ai';
2
+ import type { McpToolInfo, McpServerConfig } from './types';
3
+ import { parseSafeId, isNativeMcpServer } from './types';
4
+ import { McpProcessManager } from './processManager';
5
+ import { McpApprovalPolicy } from './approvalPolicy';
6
+ import { jsonSchemaObjectToZodObject } from './schemaConverter';
7
+
8
+ function matchGlobList(name: string, patterns: string[]): boolean {
9
+ for (const pattern of patterns) {
10
+ if (pattern === '*') return true;
11
+ if (pattern === name) return true;
12
+
13
+ const regex = new RegExp(
14
+ '^' + pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$'
15
+ );
16
+ if (regex.test(name)) return true;
17
+ }
18
+ return false;
19
+ }
20
+
21
+ export class McpToolCatalog {
22
+ private processManager: McpProcessManager;
23
+ private approvalPolicy: McpApprovalPolicy;
24
+ private configs: McpServerConfig[];
25
+ private exposedTools = new Map<string, CoreTool>();
26
+ private toolInfoMap = new Map<string, McpToolInfo>();
27
+ private safeToCanonical = new Map<string, string>();
28
+ private canonicalToSafe = new Map<string, string>();
29
+
30
+ constructor(processManager: McpProcessManager, approvalPolicy: McpApprovalPolicy, configs: McpServerConfig[]) {
31
+ this.processManager = processManager;
32
+ this.approvalPolicy = approvalPolicy;
33
+ this.configs = configs;
34
+ }
35
+
36
+ refreshTools(serverId?: string): void {
37
+ if (serverId) {
38
+ this.refreshServerTools(serverId);
39
+ } else {
40
+ for (const config of this.configs) {
41
+ if (config.enabled) {
42
+ this.refreshServerTools(config.id);
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ private refreshServerTools(serverId: string): void {
49
+ for (const [safeId, info] of this.toolInfoMap) {
50
+ if (info.serverId === serverId) {
51
+ this.exposedTools.delete(safeId);
52
+ this.toolInfoMap.delete(safeId);
53
+ this.safeToCanonical.delete(safeId);
54
+ this.canonicalToSafe.delete(info.canonicalId);
55
+ }
56
+ }
57
+
58
+ const config = this.configs.find(c => c.id === serverId);
59
+ if (!config || !config.enabled) return;
60
+
61
+ const rawTools = this.processManager.listTools(serverId);
62
+
63
+ for (const toolInfo of rawTools) {
64
+ if (config.tools.deny && config.tools.deny.length > 0) {
65
+ if (matchGlobList(toolInfo.name, config.tools.deny)) continue;
66
+ }
67
+
68
+ if (config.tools.allow && config.tools.allow.length > 0) {
69
+ if (!matchGlobList(toolInfo.name, config.tools.allow)) continue;
70
+ }
71
+
72
+ const coreTool = this.convertToCoreToolDef(toolInfo, config);
73
+ this.exposedTools.set(toolInfo.safeId, coreTool);
74
+ this.toolInfoMap.set(toolInfo.safeId, toolInfo);
75
+ this.safeToCanonical.set(toolInfo.safeId, toolInfo.canonicalId);
76
+ this.canonicalToSafe.set(toolInfo.canonicalId, toolInfo.safeId);
77
+ }
78
+ }
79
+
80
+ private convertToCoreToolDef(toolInfo: McpToolInfo, config: McpServerConfig): CoreTool {
81
+ const schema = toolInfo.inputSchema || { type: 'object', properties: {} };
82
+ const zodParams = jsonSchemaObjectToZodObject(schema as Record<string, unknown>);
83
+
84
+ const pm = this.processManager;
85
+ const ap = this.approvalPolicy;
86
+ const serverConfig = config;
87
+
88
+ return tool({
89
+ description: toolInfo.description || `MCP tool: ${toolInfo.name} (${config.name})`,
90
+ parameters: zodParams,
91
+ execute: async (args: Record<string, unknown>) => {
92
+ try {
93
+ const effectiveApproval = serverConfig.toolApproval?.[toolInfo.name] ?? serverConfig.approval;
94
+ const approvalResult = await ap.requestMcpApproval({
95
+ serverId: serverConfig.id,
96
+ serverName: serverConfig.name,
97
+ toolName: toolInfo.name,
98
+ canonicalId: toolInfo.canonicalId,
99
+ args,
100
+ approvalMode: effectiveApproval,
101
+ });
102
+
103
+ if (!approvalResult.approved) {
104
+ if (approvalResult.customResponse) {
105
+ return {
106
+ error: `OPERATION REJECTED BY USER with custom instructions: "${approvalResult.customResponse}"`,
107
+ userMessage: 'Operation cancelled by user',
108
+ };
109
+ }
110
+ return {
111
+ error: `OPERATION REJECTED BY USER: calling MCP tool ${toolInfo.canonicalId}`,
112
+ userMessage: 'Operation cancelled by user',
113
+ };
114
+ }
115
+
116
+ const result = await pm.callTool(serverConfig.id, toolInfo.name, args);
117
+
118
+ if (result.isError) {
119
+ return { error: result.content };
120
+ }
121
+
122
+ return result.content;
123
+ } catch (error) {
124
+ const message = error instanceof Error ? error.message : String(error);
125
+ return { error: `MCP tool call failed: ${message}` };
126
+ }
127
+ },
128
+ });
129
+ }
130
+
131
+ getExposedTools(): Record<string, CoreTool> {
132
+ const result: Record<string, CoreTool> = {};
133
+ for (const [safeId, coreTool] of this.exposedTools) {
134
+ result[safeId] = coreTool;
135
+ }
136
+ return result;
137
+ }
138
+
139
+ getMcpToolInfos(): McpToolInfo[] {
140
+ return Array.from(this.toolInfoMap.values());
141
+ }
142
+
143
+ getToolInfo(safeId: string): McpToolInfo | null {
144
+ return this.toolInfoMap.get(safeId) || null;
145
+ }
146
+
147
+ getSafeIdFromCanonical(canonicalId: string): string | null {
148
+ return this.canonicalToSafe.get(canonicalId) || null;
149
+ }
150
+
151
+ getCanonicalFromSafeId(safeId: string): string | null {
152
+ return this.safeToCanonical.get(safeId) || null;
153
+ }
154
+
155
+ isMcpTool(toolName: string): boolean {
156
+ return toolName.startsWith('mcp__');
157
+ }
158
+
159
+ parseMcpToolName(safeId: string): { serverId: string; toolName: string } | null {
160
+ return parseSafeId(safeId);
161
+ }
162
+
163
+ isNative(safeId: string): boolean {
164
+ const parsed = parseSafeId(safeId);
165
+ if (!parsed) return false;
166
+ const config = this.configs.find(c => c.id === parsed.serverId);
167
+ if (config?.native) return true;
168
+ return isNativeMcpServer(parsed.serverId);
169
+ }
170
+
171
+ getServerConfig(serverId: string): McpServerConfig | null {
172
+ return this.configs.find(c => c.id === serverId) || null;
173
+ }
174
+
175
+ updateConfigs(configs: McpServerConfig[]): void {
176
+ this.configs = configs;
177
+ }
178
+
179
+ static mergeTools(internal: Record<string, CoreTool>, mcp: Record<string, CoreTool>): Record<string, CoreTool> {
180
+ return { ...internal, ...mcp };
181
+ }
182
+ }