@kirosnn/mosaic 0.71.0 → 0.73.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 (75) 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 +75 -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 +1146 -954
  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 +148 -0
  32. package/src/mcp/cli/add.ts +185 -0
  33. package/src/mcp/cli/doctor.ts +77 -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 +223 -0
  41. package/src/mcp/index.ts +80 -0
  42. package/src/mcp/processManager.ts +299 -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.ts +854 -0
  47. package/src/mcp/toolCatalog.ts +169 -0
  48. package/src/mcp/types.ts +95 -0
  49. package/src/utils/approvalBridge.ts +17 -5
  50. package/src/utils/commands/compact.ts +30 -0
  51. package/src/utils/commands/echo.ts +1 -1
  52. package/src/utils/commands/index.ts +4 -6
  53. package/src/utils/commands/new.ts +15 -0
  54. package/src/utils/commands/types.ts +3 -0
  55. package/src/utils/config.ts +3 -1
  56. package/src/utils/diffRendering.tsx +1 -3
  57. package/src/utils/exploreBridge.ts +10 -0
  58. package/src/utils/markdown.tsx +163 -99
  59. package/src/utils/models.ts +31 -9
  60. package/src/utils/questionBridge.ts +36 -1
  61. package/src/utils/tokenEstimator.ts +32 -0
  62. package/src/utils/toolFormatting.ts +268 -7
  63. package/src/web/app.tsx +72 -72
  64. package/src/web/components/HomePage.tsx +7 -7
  65. package/src/web/components/MessageItem.tsx +22 -22
  66. package/src/web/components/QuestionPanel.tsx +72 -12
  67. package/src/web/components/Sidebar.tsx +0 -2
  68. package/src/web/components/ThinkingIndicator.tsx +1 -0
  69. package/src/web/server.tsx +767 -683
  70. package/src/utils/commands/redo.ts +0 -74
  71. package/src/utils/commands/sessions.ts +0 -129
  72. package/src/utils/commands/undo.ts +0 -75
  73. package/src/utils/undoRedo.ts +0 -429
  74. package/src/utils/undoRedoBridge.ts +0 -45
  75. package/src/utils/undoRedoDb.ts +0 -338
@@ -0,0 +1,80 @@
1
+ import { loadMcpConfig } from './config';
2
+ import { McpProcessManager } from './processManager';
3
+ import { McpApprovalPolicy } from './approvalPolicy';
4
+ import { McpToolCatalog } from './toolCatalog';
5
+
6
+ export type { McpServerConfig, McpServerState, McpToolInfo, McpRiskHint, McpGlobalConfig } from './types';
7
+ export { toCanonicalId, toSafeId, parseSafeId, parseCanonicalId } from './types';
8
+
9
+ let manager: McpProcessManager | null = null;
10
+ let catalog: McpToolCatalog | null = null;
11
+ let approvalPolicy: McpApprovalPolicy | null = null;
12
+ let initialized = false;
13
+
14
+ export function getMcpManager(): McpProcessManager {
15
+ if (!manager) {
16
+ manager = new McpProcessManager();
17
+ }
18
+ return manager;
19
+ }
20
+
21
+ export function getMcpCatalog(): McpToolCatalog {
22
+ if (!catalog) {
23
+ throw new Error('MCP not initialized. Call initializeMcp() first.');
24
+ }
25
+ return catalog;
26
+ }
27
+
28
+ export function getMcpApprovalPolicy(): McpApprovalPolicy {
29
+ if (!approvalPolicy) {
30
+ approvalPolicy = new McpApprovalPolicy();
31
+ }
32
+ return approvalPolicy;
33
+ }
34
+
35
+ export async function initializeMcp(): Promise<string[]> {
36
+ if (initialized) return [];
37
+
38
+ const configs = loadMcpConfig();
39
+ if (configs.length === 0) {
40
+ initialized = true;
41
+ return [];
42
+ }
43
+
44
+ manager = new McpProcessManager();
45
+ approvalPolicy = new McpApprovalPolicy();
46
+ catalog = new McpToolCatalog(manager, approvalPolicy, configs);
47
+
48
+ const startupServers = configs.filter(c => c.enabled && c.autostart === 'startup');
49
+ const failedServers: string[] = [];
50
+
51
+ for (const config of startupServers) {
52
+ try {
53
+ await manager.startServer(config);
54
+ } catch {
55
+ failedServers.push(config.id || config.command || 'unknown');
56
+ }
57
+ }
58
+
59
+ if (failedServers.length > 0) {
60
+ console.error(`MCP: failed to start servers: ${failedServers.join(', ')}`);
61
+ }
62
+
63
+ catalog.refreshTools();
64
+ initialized = true;
65
+ return failedServers;
66
+ }
67
+
68
+ export async function shutdownMcp(): Promise<void> {
69
+ if (manager) {
70
+ await manager.shutdownAll();
71
+ }
72
+ manager = null;
73
+ catalog = null;
74
+ approvalPolicy = null;
75
+ initialized = false;
76
+ }
77
+
78
+ export function isMcpInitialized(): boolean {
79
+ return initialized;
80
+ }
@@ -0,0 +1,299 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3
+ import type { McpServerConfig, McpServerState, McpToolInfo } from './types';
4
+ import { toCanonicalId, toSafeId } from './types';
5
+ import { McpRateLimiter } from './rateLimiter';
6
+
7
+ interface LogEntry {
8
+ timestamp: number;
9
+ level: 'info' | 'error' | 'debug';
10
+ message: string;
11
+ }
12
+
13
+ interface ServerInstance {
14
+ config: McpServerConfig;
15
+ client: Client;
16
+ transport: StdioClientTransport;
17
+ state: McpServerState;
18
+ tools: McpToolInfo[];
19
+ logBuffer: LogEntry[];
20
+ restartCount: number;
21
+ lastRestartAt: number;
22
+ }
23
+
24
+ const MAX_RESTART_COUNT = 5;
25
+ const BACKOFF_DELAYS = [1000, 2000, 4000, 8000, 16000, 30000];
26
+
27
+ export class McpProcessManager {
28
+ private servers = new Map<string, ServerInstance>();
29
+ private rateLimiter = new McpRateLimiter();
30
+
31
+ async startServer(config: McpServerConfig): Promise<McpServerState> {
32
+ const existing = this.servers.get(config.id);
33
+ if (existing && existing.state.status === 'running') {
34
+ return existing.state;
35
+ }
36
+
37
+ const state: McpServerState = {
38
+ status: 'starting',
39
+ toolCount: 0,
40
+ };
41
+
42
+ const logBuffer: LogEntry[] = [];
43
+
44
+ const addLog = (level: LogEntry['level'], message: string) => {
45
+ logBuffer.push({ timestamp: Date.now(), level, message });
46
+ if (logBuffer.length > config.logs.bufferSize) {
47
+ logBuffer.shift();
48
+ }
49
+ };
50
+
51
+ addLog('info', `Starting server ${config.id} (${config.command})`);
52
+
53
+ try {
54
+ const startTime = Date.now();
55
+
56
+ const transport = new StdioClientTransport({
57
+ command: config.command,
58
+ args: config.args,
59
+ env: config.env ? { ...process.env, ...config.env } as Record<string, string> : undefined,
60
+ cwd: config.cwd,
61
+ });
62
+
63
+ const client = new Client(
64
+ { name: 'mosaic', version: '1.0.0' },
65
+ { capabilities: {} }
66
+ );
67
+
68
+ await client.connect(transport);
69
+
70
+ const initLatencyMs = Date.now() - startTime;
71
+ addLog('info', `Connected in ${initLatencyMs}ms`);
72
+
73
+ const toolsResult = await client.listTools();
74
+ const tools: McpToolInfo[] = (toolsResult.tools || []).map(t => ({
75
+ serverId: config.id,
76
+ name: t.name,
77
+ description: t.description || '',
78
+ inputSchema: (t.inputSchema || {}) as Record<string, unknown>,
79
+ canonicalId: toCanonicalId(config.id, t.name),
80
+ safeId: toSafeId(config.id, t.name),
81
+ }));
82
+
83
+ state.status = 'running';
84
+ state.initLatencyMs = initLatencyMs;
85
+ state.toolCount = tools.length;
86
+
87
+ addLog('info', `Listed ${tools.length} tools`);
88
+
89
+ this.rateLimiter.configure(config.id, config.limits.maxCallsPerMinute);
90
+
91
+ const instance: ServerInstance = {
92
+ config,
93
+ client,
94
+ transport,
95
+ state,
96
+ tools,
97
+ logBuffer,
98
+ restartCount: existing?.restartCount ?? 0,
99
+ lastRestartAt: existing?.lastRestartAt ?? 0,
100
+ };
101
+
102
+ this.servers.set(config.id, instance);
103
+
104
+ transport.onclose = () => {
105
+ const srv = this.servers.get(config.id);
106
+ if (srv && srv.state.status === 'running') {
107
+ srv.state.status = 'error';
108
+ srv.state.lastError = 'Transport closed unexpectedly';
109
+ addLog('error', 'Transport closed unexpectedly');
110
+ this.attemptRestart(config.id);
111
+ }
112
+ };
113
+
114
+ transport.onerror = (error: Error) => {
115
+ addLog('error', `Transport error: ${error.message}`);
116
+ };
117
+
118
+ return state;
119
+ } catch (error) {
120
+ const message = error instanceof Error ? error.message : String(error);
121
+ state.status = 'error';
122
+ state.lastError = message;
123
+ addLog('error', `Failed to start: ${message}`);
124
+
125
+ this.servers.set(config.id, {
126
+ config,
127
+ client: null!,
128
+ transport: null!,
129
+ state,
130
+ tools: [],
131
+ logBuffer,
132
+ restartCount: existing?.restartCount ?? 0,
133
+ lastRestartAt: existing?.lastRestartAt ?? 0,
134
+ });
135
+
136
+ return state;
137
+ }
138
+ }
139
+
140
+ async stopServer(id: string): Promise<void> {
141
+ const instance = this.servers.get(id);
142
+ if (!instance) return;
143
+
144
+ instance.state.status = 'stopped';
145
+
146
+ try {
147
+ if (instance.client) {
148
+ await instance.client.close();
149
+ }
150
+ } catch {
151
+ // best effort
152
+ }
153
+
154
+ this.rateLimiter.remove(id);
155
+ }
156
+
157
+ async restartServer(id: string): Promise<McpServerState | null> {
158
+ const instance = this.servers.get(id);
159
+ if (!instance) return null;
160
+
161
+ await this.stopServer(id);
162
+ return this.startServer(instance.config);
163
+ }
164
+
165
+ async callTool(serverId: string, toolName: string, args: Record<string, unknown>): Promise<{ content: string; isError: boolean }> {
166
+ const instance = this.servers.get(serverId);
167
+ if (!instance) {
168
+ return { content: `Server ${serverId} not found`, isError: true };
169
+ }
170
+
171
+ if (instance.state.status !== 'running') {
172
+ return { content: `Server ${serverId} is not running (status: ${instance.state.status})`, isError: true };
173
+ }
174
+
175
+ const payloadSize = JSON.stringify(args).length;
176
+ if (payloadSize > instance.config.limits.maxPayloadBytes) {
177
+ return {
178
+ content: `Payload too large: ${payloadSize} bytes (max: ${instance.config.limits.maxPayloadBytes})`,
179
+ isError: true,
180
+ };
181
+ }
182
+
183
+ await this.rateLimiter.acquire(serverId);
184
+
185
+ try {
186
+ const timeout = instance.config.timeouts.call;
187
+ const controller = new AbortController();
188
+ const timer = setTimeout(() => controller.abort(), timeout);
189
+
190
+ const result = await instance.client.callTool(
191
+ { name: toolName, arguments: args },
192
+ undefined,
193
+ { signal: controller.signal }
194
+ );
195
+
196
+ clearTimeout(timer);
197
+ instance.state.lastCallAt = Date.now();
198
+
199
+ const contentParts = result.content as Array<{ type: string; text?: string }>;
200
+ const text = contentParts
201
+ .filter(p => p.type === 'text')
202
+ .map(p => p.text || '')
203
+ .join('\n');
204
+
205
+ return {
206
+ content: text || JSON.stringify(result.content),
207
+ isError: result.isError === true,
208
+ };
209
+ } catch (error) {
210
+ const message = error instanceof Error ? error.message : String(error);
211
+ instance.logBuffer.push({
212
+ timestamp: Date.now(),
213
+ level: 'error',
214
+ message: `callTool ${toolName} failed: ${message}`,
215
+ });
216
+ return { content: `Tool call failed: ${message}`, isError: true };
217
+ }
218
+ }
219
+
220
+ listTools(serverId: string): McpToolInfo[] {
221
+ const instance = this.servers.get(serverId);
222
+ if (!instance) return [];
223
+ return [...instance.tools];
224
+ }
225
+
226
+ getAllTools(): McpToolInfo[] {
227
+ const all: McpToolInfo[] = [];
228
+ for (const instance of this.servers.values()) {
229
+ if (instance.state.status === 'running') {
230
+ all.push(...instance.tools);
231
+ }
232
+ }
233
+ return all;
234
+ }
235
+
236
+ getState(serverId: string): McpServerState | null {
237
+ const instance = this.servers.get(serverId);
238
+ return instance ? { ...instance.state } : null;
239
+ }
240
+
241
+ getAllStates(): Map<string, McpServerState> {
242
+ const states = new Map<string, McpServerState>();
243
+ for (const [id, instance] of this.servers) {
244
+ states.set(id, { ...instance.state });
245
+ }
246
+ return states;
247
+ }
248
+
249
+ getLogs(serverId: string): LogEntry[] {
250
+ const instance = this.servers.get(serverId);
251
+ if (!instance) return [];
252
+ return [...instance.logBuffer];
253
+ }
254
+
255
+ getConfig(serverId: string): McpServerConfig | null {
256
+ const instance = this.servers.get(serverId);
257
+ return instance ? instance.config : null;
258
+ }
259
+
260
+ async shutdownAll(): Promise<void> {
261
+ const promises: Promise<void>[] = [];
262
+ for (const id of this.servers.keys()) {
263
+ promises.push(this.stopServer(id));
264
+ }
265
+ await Promise.allSettled(promises);
266
+ this.servers.clear();
267
+ }
268
+
269
+ private async attemptRestart(id: string): Promise<void> {
270
+ const instance = this.servers.get(id);
271
+ if (!instance) return;
272
+
273
+ if (instance.restartCount >= MAX_RESTART_COUNT) {
274
+ instance.logBuffer.push({
275
+ timestamp: Date.now(),
276
+ level: 'error',
277
+ message: `Max restart count (${MAX_RESTART_COUNT}) reached, giving up`,
278
+ });
279
+ return;
280
+ }
281
+
282
+ const delay = BACKOFF_DELAYS[Math.min(instance.restartCount, BACKOFF_DELAYS.length - 1)]!;
283
+ instance.restartCount++;
284
+ instance.lastRestartAt = Date.now();
285
+
286
+ instance.logBuffer.push({
287
+ timestamp: Date.now(),
288
+ level: 'info',
289
+ message: `Scheduling restart #${instance.restartCount} in ${delay}ms`,
290
+ });
291
+
292
+ await new Promise(resolve => setTimeout(resolve, delay));
293
+
294
+ const current = this.servers.get(id);
295
+ if (current && current.state.status !== 'running' && current.state.status !== 'stopped') {
296
+ await this.startServer(instance.config);
297
+ }
298
+ }
299
+ }
@@ -0,0 +1,50 @@
1
+ interface TokenBucket {
2
+ tokens: number;
3
+ maxTokens: number;
4
+ refillRate: number;
5
+ lastRefill: number;
6
+ }
7
+
8
+ export class McpRateLimiter {
9
+ private buckets = new Map<string, TokenBucket>();
10
+
11
+ configure(serverId: string, maxCallsPerMinute: number): void {
12
+ this.buckets.set(serverId, {
13
+ tokens: maxCallsPerMinute,
14
+ maxTokens: maxCallsPerMinute,
15
+ refillRate: maxCallsPerMinute / 60,
16
+ lastRefill: Date.now(),
17
+ });
18
+ }
19
+
20
+ tryAcquire(serverId: string): boolean {
21
+ const bucket = this.buckets.get(serverId);
22
+ if (!bucket) return true;
23
+
24
+ this.refill(bucket);
25
+
26
+ if (bucket.tokens >= 1) {
27
+ bucket.tokens -= 1;
28
+ return true;
29
+ }
30
+
31
+ return false;
32
+ }
33
+
34
+ async acquire(serverId: string): Promise<void> {
35
+ while (!this.tryAcquire(serverId)) {
36
+ await new Promise(resolve => setTimeout(resolve, 100));
37
+ }
38
+ }
39
+
40
+ remove(serverId: string): void {
41
+ this.buckets.delete(serverId);
42
+ }
43
+
44
+ private refill(bucket: TokenBucket): void {
45
+ const now = Date.now();
46
+ const elapsed = (now - bucket.lastRefill) / 1000;
47
+ bucket.tokens = Math.min(bucket.maxTokens, bucket.tokens + elapsed * bucket.refillRate);
48
+ bucket.lastRefill = now;
49
+ }
50
+ }
@@ -0,0 +1,151 @@
1
+ export interface McpRegistryEntry {
2
+ id: string;
3
+ name: string;
4
+ description: string;
5
+ command: string;
6
+ args: string[];
7
+ env?: Record<string, { description: string; required: boolean }>;
8
+ prompts?: { key: string; question: string; argIndex?: number }[];
9
+ }
10
+
11
+ export const MCP_REGISTRY: McpRegistryEntry[] = [
12
+ {
13
+ id: 'filesystem',
14
+ name: 'Filesystem',
15
+ description: 'Read/write access to the local filesystem',
16
+ command: 'npx',
17
+ args: ['-y', '@modelcontextprotocol/server-filesystem', '{path}'],
18
+ prompts: [{ key: 'path', question: 'Directory path to expose', argIndex: 2 }],
19
+ },
20
+ {
21
+ id: 'memory',
22
+ name: 'Memory',
23
+ description: 'Persistent knowledge graph memory for the agent',
24
+ command: 'npx',
25
+ args: ['-y', '@modelcontextprotocol/server-memory'],
26
+ },
27
+ {
28
+ id: 'fetch',
29
+ name: 'Fetch',
30
+ description: 'Fetch and convert web content to markdown',
31
+ command: 'npx',
32
+ args: ['-y', '@modelcontextprotocol/server-fetch'],
33
+ },
34
+ {
35
+ id: 'brave-search',
36
+ name: 'Brave Search',
37
+ description: 'Web search via Brave Search API',
38
+ command: 'npx',
39
+ args: ['-y', '@modelcontextprotocol/server-brave-search'],
40
+ env: { BRAVE_API_KEY: { description: 'Brave Search API key', required: true } },
41
+ },
42
+ {
43
+ id: 'github',
44
+ name: 'GitHub',
45
+ description: 'GitHub API access (repos, issues, PRs, etc.)',
46
+ command: 'npx',
47
+ args: ['-y', '@modelcontextprotocol/server-github'],
48
+ env: { GITHUB_PERSONAL_ACCESS_TOKEN: { description: 'GitHub personal access token', required: true } },
49
+ },
50
+ {
51
+ id: 'gitlab',
52
+ name: 'GitLab',
53
+ description: 'GitLab API access (repos, issues, MRs, etc.)',
54
+ command: 'npx',
55
+ args: ['-y', '@modelcontextprotocol/server-gitlab'],
56
+ env: {
57
+ GITLAB_PERSONAL_ACCESS_TOKEN: { description: 'GitLab personal access token', required: true },
58
+ GITLAB_API_URL: { description: 'GitLab API URL (for self-hosted)', required: false },
59
+ },
60
+ },
61
+ {
62
+ id: 'google-maps',
63
+ name: 'Google Maps',
64
+ description: 'Google Maps geocoding, directions, places, and elevation',
65
+ command: 'npx',
66
+ args: ['-y', '@modelcontextprotocol/server-google-maps'],
67
+ env: { GOOGLE_MAPS_API_KEY: { description: 'Google Maps API key', required: true } },
68
+ },
69
+ {
70
+ id: 'slack',
71
+ name: 'Slack',
72
+ description: 'Slack workspace access (channels, messages, users)',
73
+ command: 'npx',
74
+ args: ['-y', '@modelcontextprotocol/server-slack'],
75
+ env: {
76
+ SLACK_BOT_TOKEN: { description: 'Slack bot token (xoxb-...)', required: true },
77
+ SLACK_TEAM_ID: { description: 'Slack workspace/team ID', required: true },
78
+ },
79
+ },
80
+ {
81
+ id: 'postgres',
82
+ name: 'PostgreSQL',
83
+ description: 'Read-only access to a PostgreSQL database',
84
+ command: 'npx',
85
+ args: ['-y', '@modelcontextprotocol/server-postgres', '{connection_string}'],
86
+ prompts: [{ key: 'connection_string', question: 'PostgreSQL connection string (postgresql://...)', argIndex: 2 }],
87
+ },
88
+ {
89
+ id: 'sqlite',
90
+ name: 'SQLite',
91
+ description: 'Read/write access to a SQLite database',
92
+ command: 'npx',
93
+ args: ['-y', '@modelcontextprotocol/server-sqlite', '--db-path', '{db_path}'],
94
+ prompts: [{ key: 'db_path', question: 'Path to the SQLite database file', argIndex: 3 }],
95
+ },
96
+ {
97
+ id: 'puppeteer',
98
+ name: 'Puppeteer',
99
+ description: 'Browser automation and web scraping via Puppeteer',
100
+ command: 'npx',
101
+ args: ['-y', '@modelcontextprotocol/server-puppeteer'],
102
+ },
103
+ {
104
+ id: 'sequential-thinking',
105
+ name: 'Sequential Thinking',
106
+ description: 'Dynamic problem-solving through structured sequential thinking',
107
+ command: 'npx',
108
+ args: ['-y', '@modelcontextprotocol/server-sequential-thinking'],
109
+ },
110
+ {
111
+ id: 'everything',
112
+ name: 'Everything',
113
+ description: 'MCP test server with sample tools, resources, and prompts',
114
+ command: 'npx',
115
+ args: ['-y', '@modelcontextprotocol/server-everything'],
116
+ },
117
+ {
118
+ id: 'browser-use',
119
+ name: 'Browser Use',
120
+ description: 'AI-powered browser automation, web search, and data extraction',
121
+ command: 'npx',
122
+ args: ['-y', 'browser-use-mcp'],
123
+ env: { BROWSER_USE_API_KEY: { description: 'Browser Use API key (from cloud.browser-use.com)', required: true } },
124
+ },
125
+ {
126
+ id: 'sentry',
127
+ name: 'Sentry',
128
+ description: 'Sentry error tracking access',
129
+ command: 'npx',
130
+ args: ['-y', '@modelcontextprotocol/server-sentry'],
131
+ env: { SENTRY_AUTH_TOKEN: { description: 'Sentry auth token', required: true } },
132
+ },
133
+ ];
134
+
135
+ export function findRegistryEntry(nameOrId: string): McpRegistryEntry | null {
136
+ const lower = nameOrId.toLowerCase().replace(/\s+/g, '-');
137
+ return MCP_REGISTRY.find(e =>
138
+ e.id === lower ||
139
+ e.name.toLowerCase() === nameOrId.toLowerCase() ||
140
+ e.name.toLowerCase().replace(/\s+/g, '-') === lower
141
+ ) || null;
142
+ }
143
+
144
+ export function searchRegistry(query: string): McpRegistryEntry[] {
145
+ const lower = query.toLowerCase();
146
+ return MCP_REGISTRY.filter(e =>
147
+ e.id.includes(lower) ||
148
+ e.name.toLowerCase().includes(lower) ||
149
+ e.description.toLowerCase().includes(lower)
150
+ );
151
+ }
@@ -0,0 +1,100 @@
1
+ import { z, type ZodTypeAny } from 'zod';
2
+
3
+ export function jsonSchemaToZod(schema: Record<string, unknown>): ZodTypeAny {
4
+ if (!schema || typeof schema !== 'object') {
5
+ return z.unknown();
6
+ }
7
+
8
+ if (schema.enum && Array.isArray(schema.enum)) {
9
+ const values = schema.enum as [string, ...string[]];
10
+ if (values.length > 0) {
11
+ return z.enum(values as [string, ...string[]]);
12
+ }
13
+ return z.string();
14
+ }
15
+
16
+ if (schema.oneOf && Array.isArray(schema.oneOf)) {
17
+ const schemas = (schema.oneOf as Record<string, unknown>[]).map(s => jsonSchemaToZod(s));
18
+ if (schemas.length === 0) return z.unknown();
19
+ if (schemas.length === 1) return schemas[0]!;
20
+ return z.union([schemas[0]!, schemas[1]!, ...schemas.slice(2)]);
21
+ }
22
+
23
+ if (schema.anyOf && Array.isArray(schema.anyOf)) {
24
+ const schemas = (schema.anyOf as Record<string, unknown>[]).map(s => jsonSchemaToZod(s));
25
+ if (schemas.length === 0) return z.unknown();
26
+ if (schemas.length === 1) return schemas[0]!;
27
+ return z.union([schemas[0]!, schemas[1]!, ...schemas.slice(2)]);
28
+ }
29
+
30
+ const type = schema.type as string | undefined;
31
+ const description = schema.description as string | undefined;
32
+
33
+ let result: ZodTypeAny;
34
+
35
+ switch (type) {
36
+ case 'string': {
37
+ let s = z.string();
38
+ if (typeof schema.minLength === 'number') s = s.min(schema.minLength);
39
+ if (typeof schema.maxLength === 'number') s = s.max(schema.maxLength);
40
+ result = s;
41
+ break;
42
+ }
43
+
44
+ case 'number':
45
+ case 'integer': {
46
+ let n = type === 'integer' ? z.number().int() : z.number();
47
+ if (typeof schema.minimum === 'number') n = n.min(schema.minimum);
48
+ if (typeof schema.maximum === 'number') n = n.max(schema.maximum);
49
+ result = n;
50
+ break;
51
+ }
52
+
53
+ case 'boolean':
54
+ result = z.boolean();
55
+ break;
56
+
57
+ case 'array': {
58
+ const items = schema.items as Record<string, unknown> | undefined;
59
+ const itemSchema = items ? jsonSchemaToZod(items) : z.unknown();
60
+ result = z.array(itemSchema);
61
+ break;
62
+ }
63
+
64
+ case 'object': {
65
+ result = jsonSchemaObjectToZodObject(schema);
66
+ break;
67
+ }
68
+
69
+ case 'null':
70
+ result = z.null();
71
+ break;
72
+
73
+ default:
74
+ result = z.unknown();
75
+ break;
76
+ }
77
+
78
+ if (description) {
79
+ result = result.describe(description);
80
+ }
81
+
82
+ return result;
83
+ }
84
+
85
+ export function jsonSchemaObjectToZodObject(schema: Record<string, unknown>): z.ZodObject<any> {
86
+ const properties = (schema.properties || {}) as Record<string, Record<string, unknown>>;
87
+ const required = (schema.required || []) as string[];
88
+
89
+ const shape: Record<string, ZodTypeAny> = {};
90
+
91
+ for (const [key, propSchema] of Object.entries(properties)) {
92
+ let zodProp = jsonSchemaToZod(propSchema);
93
+ if (!required.includes(key)) {
94
+ zodProp = zodProp.optional();
95
+ }
96
+ shape[key] = zodProp;
97
+ }
98
+
99
+ return z.object(shape).passthrough();
100
+ }