@utopia-ai/cli 0.1.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 (49) hide show
  1. package/.claude/settings.json +1 -0
  2. package/.claude/settings.local.json +38 -0
  3. package/bin/utopia.js +20 -0
  4. package/package.json +46 -0
  5. package/python/README.md +34 -0
  6. package/python/instrumenter/instrument.py +1148 -0
  7. package/python/pyproject.toml +32 -0
  8. package/python/setup.py +27 -0
  9. package/python/utopia_runtime/__init__.py +30 -0
  10. package/python/utopia_runtime/__pycache__/__init__.cpython-313.pyc +0 -0
  11. package/python/utopia_runtime/__pycache__/client.cpython-313.pyc +0 -0
  12. package/python/utopia_runtime/__pycache__/probe.cpython-313.pyc +0 -0
  13. package/python/utopia_runtime/client.py +31 -0
  14. package/python/utopia_runtime/probe.py +446 -0
  15. package/python/utopia_runtime.egg-info/PKG-INFO +59 -0
  16. package/python/utopia_runtime.egg-info/SOURCES.txt +10 -0
  17. package/python/utopia_runtime.egg-info/dependency_links.txt +1 -0
  18. package/python/utopia_runtime.egg-info/top_level.txt +1 -0
  19. package/scripts/publish-npm.sh +14 -0
  20. package/scripts/publish-pypi.sh +17 -0
  21. package/src/cli/commands/codex.ts +193 -0
  22. package/src/cli/commands/context.ts +188 -0
  23. package/src/cli/commands/destruct.ts +237 -0
  24. package/src/cli/commands/easter-eggs.ts +203 -0
  25. package/src/cli/commands/init.ts +505 -0
  26. package/src/cli/commands/instrument.ts +962 -0
  27. package/src/cli/commands/mcp.ts +16 -0
  28. package/src/cli/commands/serve.ts +194 -0
  29. package/src/cli/commands/status.ts +304 -0
  30. package/src/cli/commands/validate.ts +328 -0
  31. package/src/cli/index.ts +37 -0
  32. package/src/cli/utils/config.ts +54 -0
  33. package/src/graph/index.ts +687 -0
  34. package/src/instrumenter/javascript.ts +1798 -0
  35. package/src/mcp/index.ts +886 -0
  36. package/src/runtime/js/index.ts +518 -0
  37. package/src/runtime/js/package-lock.json +30 -0
  38. package/src/runtime/js/package.json +30 -0
  39. package/src/runtime/js/tsconfig.json +16 -0
  40. package/src/server/db/index.ts +26 -0
  41. package/src/server/db/schema.ts +45 -0
  42. package/src/server/index.ts +79 -0
  43. package/src/server/middleware/auth.ts +74 -0
  44. package/src/server/routes/admin.ts +36 -0
  45. package/src/server/routes/graph.ts +358 -0
  46. package/src/server/routes/probes.ts +286 -0
  47. package/src/types.ts +147 -0
  48. package/src/utopia-mode/index.ts +206 -0
  49. package/tsconfig.json +19 -0
@@ -0,0 +1,886 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Configuration
7
+ // ---------------------------------------------------------------------------
8
+
9
+ const ENDPOINT = process.env.UTOPIA_ENDPOINT ?? 'http://localhost:7890';
10
+ const PROJECT_ID = process.env.UTOPIA_PROJECT_ID; // optional global filter
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Shared types for API responses
14
+ // ---------------------------------------------------------------------------
15
+
16
+ interface ProbeResponse {
17
+ id: string;
18
+ projectId: string;
19
+ probeType: string;
20
+ timestamp: string;
21
+ file: string;
22
+ line: number;
23
+ functionName: string;
24
+ data: Record<string, unknown>;
25
+ metadata: Record<string, unknown>;
26
+ }
27
+
28
+ interface ProbeListResponse {
29
+ count: number;
30
+ probes: ProbeResponse[];
31
+ keywords?: string[];
32
+ }
33
+
34
+ interface NodeResponse {
35
+ id: string;
36
+ type: string;
37
+ name: string;
38
+ file: string | null;
39
+ metadata: Record<string, unknown>;
40
+ }
41
+
42
+ interface EdgeResponse {
43
+ source: string;
44
+ target: string;
45
+ type: string;
46
+ weight: number;
47
+ lastSeen: string;
48
+ }
49
+
50
+ interface GraphResponse {
51
+ nodes: NodeResponse[];
52
+ edges: EdgeResponse[];
53
+ }
54
+
55
+ interface ImpactResponse {
56
+ startNode: string;
57
+ depth: number;
58
+ nodes: NodeResponse[];
59
+ edges: EdgeResponse[];
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // HTTP helper
64
+ // ---------------------------------------------------------------------------
65
+
66
+ async function fetchFromUtopia(
67
+ path: string,
68
+ params?: Record<string, string>,
69
+ ): Promise<unknown> {
70
+ const url = new URL(path, ENDPOINT);
71
+
72
+ if (params) {
73
+ for (const [key, value] of Object.entries(params)) {
74
+ if (value !== undefined && value !== '') {
75
+ url.searchParams.set(key, value);
76
+ }
77
+ }
78
+ }
79
+
80
+ if (PROJECT_ID) {
81
+ url.searchParams.set('project_id', PROJECT_ID);
82
+ }
83
+
84
+ try {
85
+ const response = await fetch(url.toString(), {
86
+ method: 'GET',
87
+ headers: {
88
+ 'Accept': 'application/json',
89
+ },
90
+ });
91
+
92
+ if (!response.ok) {
93
+ const body = await response.text();
94
+ return { error: `HTTP ${response.status}: ${body}` };
95
+ }
96
+
97
+ return await response.json();
98
+ } catch (err) {
99
+ const message = err instanceof Error ? err.message : String(err);
100
+ return { error: `Failed to reach Utopia data service at ${ENDPOINT}: ${message}` };
101
+ }
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Formatting helpers
106
+ // ---------------------------------------------------------------------------
107
+
108
+ function isErrorResponse(data: unknown): data is { error: string } {
109
+ return (
110
+ typeof data === 'object' &&
111
+ data !== null &&
112
+ 'error' in data &&
113
+ typeof (data as Record<string, unknown>).error === 'string'
114
+ );
115
+ }
116
+
117
+ function formatTimestamp(ts: string): string {
118
+ try {
119
+ const d = new Date(ts);
120
+ return d.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
121
+ } catch {
122
+ return ts;
123
+ }
124
+ }
125
+
126
+ function formatDuration(ms: number): string {
127
+ if (ms < 1) return `${(ms * 1000).toFixed(0)}us`;
128
+ if (ms < 1000) return `${ms.toFixed(1)}ms`;
129
+ return `${(ms / 1000).toFixed(2)}s`;
130
+ }
131
+
132
+ function truncate(s: string, max: number): string {
133
+ if (s.length <= max) return s;
134
+ return s.slice(0, max - 3) + '...';
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Tool response formatters
139
+ // ---------------------------------------------------------------------------
140
+
141
+ function formatContextProbes(probes: ProbeResponse[], keywords: string[]): string {
142
+ if (probes.length === 0) {
143
+ return 'No production context found matching the query.';
144
+ }
145
+
146
+ const lines: string[] = [];
147
+ lines.push(`Found ${probes.length} relevant probe(s) matching keywords: ${keywords.join(', ')}`);
148
+ lines.push('');
149
+
150
+ // Group by probe type
151
+ const grouped = new Map<string, ProbeResponse[]>();
152
+ for (const probe of probes) {
153
+ const existing = grouped.get(probe.probeType) ?? [];
154
+ existing.push(probe);
155
+ grouped.set(probe.probeType, existing);
156
+ }
157
+
158
+ for (const [probeType, group] of grouped) {
159
+ lines.push(`--- ${probeType.toUpperCase()} (${group.length}) ---`);
160
+ lines.push('');
161
+
162
+ for (const probe of group) {
163
+ lines.push(` File: ${probe.file}:${probe.line}`);
164
+ if (probe.functionName) {
165
+ lines.push(` Function: ${probe.functionName}`);
166
+ }
167
+ lines.push(` Time: ${formatTimestamp(probe.timestamp)}`);
168
+
169
+ // Type-specific summary
170
+ const d = probe.data;
171
+ if (probeType === 'error') {
172
+ lines.push(` Error: ${d.errorType}: ${d.message}`);
173
+ if (d.codeLine) lines.push(` Code: ${d.codeLine}`);
174
+ } else if (probeType === 'database') {
175
+ if (d.query) lines.push(` Query: ${truncate(String(d.query), 120)}`);
176
+ if (d.table) lines.push(` Table: ${d.table}`);
177
+ if (d.duration !== undefined) lines.push(` Duration: ${formatDuration(d.duration as number)}`);
178
+ } else if (probeType === 'api') {
179
+ lines.push(` ${d.method} ${d.url} -> ${d.statusCode ?? 'pending'}`);
180
+ if (d.duration !== undefined) lines.push(` Latency: ${formatDuration(d.duration as number)}`);
181
+ } else if (probeType === 'function') {
182
+ if (d.duration !== undefined) lines.push(` Duration: ${formatDuration(d.duration as number)}`);
183
+ if (d.llmContext) lines.push(` Context: ${d.llmContext}`);
184
+ } else if (probeType === 'infra') {
185
+ if (d.provider) lines.push(` Provider: ${d.provider}`);
186
+ if (d.region) lines.push(` Region: ${d.region}`);
187
+ }
188
+
189
+ lines.push('');
190
+ }
191
+ }
192
+
193
+ return lines.join('\n');
194
+ }
195
+
196
+ function formatErrors(probes: ProbeResponse[]): string {
197
+ if (probes.length === 0) {
198
+ return 'No recent errors found.';
199
+ }
200
+
201
+ const lines: string[] = [];
202
+ lines.push(`Found ${probes.length} recent error(s):`);
203
+ lines.push('');
204
+
205
+ for (let i = 0; i < probes.length; i++) {
206
+ const probe = probes[i];
207
+ const d = probe.data;
208
+
209
+ lines.push(`[${i + 1}] ${d.errorType ?? 'Error'}: ${d.message ?? 'Unknown error'}`);
210
+ lines.push(` File: ${probe.file}:${probe.line}`);
211
+ if (probe.functionName) {
212
+ lines.push(` Function: ${probe.functionName}()`);
213
+ }
214
+ lines.push(` Time: ${formatTimestamp(probe.timestamp)}`);
215
+
216
+ if (d.codeLine) {
217
+ lines.push(` Code line: ${d.codeLine}`);
218
+ }
219
+
220
+ if (d.stack) {
221
+ const stackLines = String(d.stack).split('\n').slice(0, 5);
222
+ lines.push(' Stack trace:');
223
+ for (const sl of stackLines) {
224
+ lines.push(` ${sl.trim()}`);
225
+ }
226
+ if (String(d.stack).split('\n').length > 5) {
227
+ lines.push(' ...(truncated)');
228
+ }
229
+ }
230
+
231
+ if (d.inputData && typeof d.inputData === 'object' && Object.keys(d.inputData as object).length > 0) {
232
+ lines.push(` Input data: ${truncate(JSON.stringify(d.inputData), 200)}`);
233
+ }
234
+
235
+ const env = probe.metadata.environment;
236
+ if (env) {
237
+ lines.push(` Environment: ${env}`);
238
+ }
239
+
240
+ lines.push('');
241
+ }
242
+
243
+ return lines.join('\n');
244
+ }
245
+
246
+ function formatDatabaseContext(probes: ProbeResponse[]): string {
247
+ if (probes.length === 0) {
248
+ return 'No database interaction data found.';
249
+ }
250
+
251
+ const lines: string[] = [];
252
+ lines.push(`Found ${probes.length} database interaction(s):`);
253
+ lines.push('');
254
+
255
+ // Compute aggregate stats
256
+ const tableStats = new Map<string, { count: number; totalDuration: number }>();
257
+ const queryPatterns = new Map<string, number>();
258
+
259
+ for (const probe of probes) {
260
+ const d = probe.data;
261
+ const table = String(d.table ?? 'unknown');
262
+ const duration = (d.duration as number) ?? 0;
263
+
264
+ const existing = tableStats.get(table) ?? { count: 0, totalDuration: 0 };
265
+ existing.count++;
266
+ existing.totalDuration += duration;
267
+ tableStats.set(table, existing);
268
+
269
+ if (d.query) {
270
+ // Normalize query to a pattern (strip literals)
271
+ const pattern = String(d.query)
272
+ .replace(/'[^']*'/g, '?')
273
+ .replace(/\b\d+\b/g, '?')
274
+ .trim();
275
+ queryPatterns.set(pattern, (queryPatterns.get(pattern) ?? 0) + 1);
276
+ }
277
+ }
278
+
279
+ // Table summary
280
+ if (tableStats.size > 0) {
281
+ lines.push('--- Table Summary ---');
282
+ for (const [table, stats] of tableStats) {
283
+ const avgMs = stats.totalDuration / stats.count;
284
+ lines.push(` ${table}: ${stats.count} operation(s), avg ${formatDuration(avgMs)}`);
285
+ }
286
+ lines.push('');
287
+ }
288
+
289
+ // Query patterns
290
+ if (queryPatterns.size > 0) {
291
+ lines.push('--- Query Patterns ---');
292
+ const sorted = [...queryPatterns.entries()].sort((a, b) => b[1] - a[1]);
293
+ for (const [pattern, count] of sorted.slice(0, 15)) {
294
+ lines.push(` [${count}x] ${truncate(pattern, 120)}`);
295
+ }
296
+ lines.push('');
297
+ }
298
+
299
+ // Individual interactions
300
+ lines.push('--- Details ---');
301
+ for (const probe of probes) {
302
+ const d = probe.data;
303
+ lines.push(` File: ${probe.file}:${probe.line}`);
304
+ if (probe.functionName) lines.push(` Function: ${probe.functionName}()`);
305
+ lines.push(` Operation: ${d.operation ?? 'unknown'}`);
306
+ if (d.query) lines.push(` Query: ${truncate(String(d.query), 150)}`);
307
+ if (d.table) lines.push(` Table: ${d.table}`);
308
+ if (d.duration !== undefined) lines.push(` Duration: ${formatDuration(d.duration as number)}`);
309
+ if (d.rowCount !== undefined) lines.push(` Rows: ${d.rowCount}`);
310
+
311
+ const conn = d.connectionInfo as Record<string, unknown> | undefined;
312
+ if (conn) {
313
+ const parts: string[] = [];
314
+ if (conn.type) parts.push(String(conn.type));
315
+ if (conn.host) parts.push(String(conn.host));
316
+ if (conn.database) parts.push(String(conn.database));
317
+ if (parts.length > 0) lines.push(` Connection: ${parts.join(' / ')}`);
318
+ }
319
+
320
+ lines.push(` Time: ${formatTimestamp(probe.timestamp)}`);
321
+ lines.push('');
322
+ }
323
+
324
+ return lines.join('\n');
325
+ }
326
+
327
+ function formatApiContext(probes: ProbeResponse[]): string {
328
+ if (probes.length === 0) {
329
+ return 'No external API call data found.';
330
+ }
331
+
332
+ const lines: string[] = [];
333
+ lines.push(`Found ${probes.length} API call(s):`);
334
+ lines.push('');
335
+
336
+ // Aggregate by endpoint pattern
337
+ const endpointStats = new Map<
338
+ string,
339
+ { count: number; totalDuration: number; statuses: Map<number, number> }
340
+ >();
341
+
342
+ for (const probe of probes) {
343
+ const d = probe.data;
344
+ const key = `${d.method ?? 'GET'} ${d.url ?? 'unknown'}`;
345
+ const existing = endpointStats.get(key) ?? {
346
+ count: 0,
347
+ totalDuration: 0,
348
+ statuses: new Map<number, number>(),
349
+ };
350
+ existing.count++;
351
+ existing.totalDuration += (d.duration as number) ?? 0;
352
+ if (d.statusCode !== undefined) {
353
+ const sc = d.statusCode as number;
354
+ existing.statuses.set(sc, (existing.statuses.get(sc) ?? 0) + 1);
355
+ }
356
+ endpointStats.set(key, existing);
357
+ }
358
+
359
+ // Endpoint summary
360
+ lines.push('--- Endpoint Summary ---');
361
+ const sorted = [...endpointStats.entries()].sort((a, b) => b[1].count - a[1].count);
362
+ for (const [endpoint, stats] of sorted) {
363
+ const avgMs = stats.totalDuration / stats.count;
364
+ const statusStr = [...stats.statuses.entries()]
365
+ .map(([code, cnt]) => `${code}:${cnt}`)
366
+ .join(', ');
367
+ lines.push(` ${endpoint}`);
368
+ lines.push(` Calls: ${stats.count}, Avg latency: ${formatDuration(avgMs)}`);
369
+ if (statusStr) lines.push(` Status codes: ${statusStr}`);
370
+ }
371
+ lines.push('');
372
+
373
+ // Individual calls
374
+ lines.push('--- Details ---');
375
+ for (const probe of probes) {
376
+ const d = probe.data;
377
+ lines.push(` File: ${probe.file}:${probe.line}`);
378
+ if (probe.functionName) lines.push(` Function: ${probe.functionName}()`);
379
+ lines.push(` ${d.method ?? 'GET'} ${d.url ?? 'unknown'} -> ${d.statusCode ?? 'pending'}`);
380
+ if (d.duration !== undefined) lines.push(` Latency: ${formatDuration(d.duration as number)}`);
381
+ if (d.error) lines.push(` Error: ${d.error}`);
382
+ lines.push(` Time: ${formatTimestamp(probe.timestamp)}`);
383
+ lines.push('');
384
+ }
385
+
386
+ return lines.join('\n');
387
+ }
388
+
389
+ function formatInfraContext(probes: ProbeResponse[]): string {
390
+ if (probes.length === 0) {
391
+ return 'No infrastructure data found.';
392
+ }
393
+
394
+ const lines: string[] = [];
395
+ lines.push(`Infrastructure context (${probes.length} probe(s)):`);
396
+ lines.push('');
397
+
398
+ for (const probe of probes) {
399
+ const d = probe.data;
400
+
401
+ lines.push(`--- ${probe.file}:${probe.line} ---`);
402
+ if (probe.functionName) lines.push(` Function: ${probe.functionName}()`);
403
+ if (d.provider) lines.push(` Cloud provider: ${d.provider}`);
404
+ if (d.region) lines.push(` Region: ${d.region}`);
405
+ if (d.serviceType) lines.push(` Service type: ${d.serviceType}`);
406
+ if (d.instanceId) lines.push(` Instance ID: ${d.instanceId}`);
407
+
408
+ const container = d.containerInfo as Record<string, unknown> | undefined;
409
+ if (container) {
410
+ if (container.containerId) lines.push(` Container ID: ${container.containerId}`);
411
+ if (container.image) lines.push(` Container image: ${container.image}`);
412
+ }
413
+
414
+ if (d.memoryUsage !== undefined) {
415
+ const mb = (d.memoryUsage as number) / (1024 * 1024);
416
+ lines.push(` Memory usage: ${mb.toFixed(1)} MB`);
417
+ }
418
+ if (d.cpuUsage !== undefined) {
419
+ lines.push(` CPU usage: ${((d.cpuUsage as number) * 100).toFixed(1)}%`);
420
+ }
421
+
422
+ const envVars = d.envVars as Record<string, string> | undefined;
423
+ if (envVars && Object.keys(envVars).length > 0) {
424
+ lines.push(' Environment variables:');
425
+ for (const [k, v] of Object.entries(envVars)) {
426
+ // Mask sensitive values
427
+ const masked = /key|secret|token|password|credential/i.test(k)
428
+ ? '***'
429
+ : String(v);
430
+ lines.push(` ${k}=${masked}`);
431
+ }
432
+ }
433
+
434
+ const env = probe.metadata.environment;
435
+ if (env) lines.push(` Environment: ${env}`);
436
+ const hostname = probe.metadata.hostname;
437
+ if (hostname) lines.push(` Hostname: ${hostname}`);
438
+
439
+ lines.push(` Captured: ${formatTimestamp(probe.timestamp)}`);
440
+ lines.push('');
441
+ }
442
+
443
+ return lines.join('\n');
444
+ }
445
+
446
+ function formatImpactAnalysis(
447
+ startNodeId: string,
448
+ impact: ImpactResponse,
449
+ ): string {
450
+ const lines: string[] = [];
451
+
452
+ const startNode = impact.nodes.find(n => n.id === startNodeId);
453
+ const startLabel = startNode
454
+ ? `${startNode.type}:${startNode.name}${startNode.file ? ` (${startNode.file})` : ''}`
455
+ : startNodeId;
456
+
457
+ lines.push(`Impact analysis for: ${startLabel}`);
458
+ lines.push('');
459
+
460
+ // Separate direct vs transitive
461
+ const directEdges = impact.edges.filter(e => e.source === startNodeId);
462
+ const directTargetIds = new Set(directEdges.map(e => e.target));
463
+ const transitiveNodes = impact.nodes.filter(
464
+ n => n.id !== startNodeId && !directTargetIds.has(n.id),
465
+ );
466
+
467
+ // Direct dependencies
468
+ if (directEdges.length > 0) {
469
+ lines.push(`--- Direct Dependencies (${directEdges.length}) ---`);
470
+ for (const edge of directEdges) {
471
+ const targetNode = impact.nodes.find(n => n.id === edge.target);
472
+ const label = targetNode
473
+ ? `${targetNode.type}:${targetNode.name}${targetNode.file ? ` (${targetNode.file})` : ''}`
474
+ : edge.target;
475
+ lines.push(` -> [${edge.type}] ${label} (weight: ${edge.weight})`);
476
+ }
477
+ lines.push('');
478
+ } else {
479
+ lines.push('No direct dependencies found.');
480
+ lines.push('');
481
+ }
482
+
483
+ // Transitive dependencies
484
+ if (transitiveNodes.length > 0) {
485
+ lines.push(`--- Transitive Dependencies (${transitiveNodes.length}) ---`);
486
+ for (const node of transitiveNodes) {
487
+ const label = `${node.type}:${node.name}${node.file ? ` (${node.file})` : ''}`;
488
+ lines.push(` - ${label}`);
489
+ }
490
+ lines.push('');
491
+ }
492
+
493
+ // Affected services
494
+ const services = impact.nodes.filter(
495
+ n => n.type === 'service' && n.id !== startNodeId,
496
+ );
497
+ if (services.length > 0) {
498
+ lines.push(`--- Affected Services (${services.length}) ---`);
499
+ for (const svc of services) {
500
+ lines.push(` * ${svc.name}${svc.file ? ` (${svc.file})` : ''}`);
501
+ }
502
+ lines.push('');
503
+ }
504
+
505
+ // Affected databases
506
+ const databases = impact.nodes.filter(
507
+ n => n.type === 'database' && n.id !== startNodeId,
508
+ );
509
+ if (databases.length > 0) {
510
+ lines.push(`--- Affected Databases (${databases.length}) ---`);
511
+ for (const db of databases) {
512
+ lines.push(` * ${db.name}`);
513
+ }
514
+ lines.push('');
515
+ }
516
+
517
+ // Summary
518
+ lines.push('--- Summary ---');
519
+ lines.push(` Total nodes in impact graph: ${impact.nodes.length}`);
520
+ lines.push(` Total edges: ${impact.edges.length}`);
521
+ lines.push(` Direct dependencies: ${directEdges.length}`);
522
+ lines.push(` Transitive dependencies: ${transitiveNodes.length}`);
523
+ lines.push(` Affected services: ${services.length}`);
524
+ lines.push(` Affected databases: ${databases.length}`);
525
+
526
+ return lines.join('\n');
527
+ }
528
+
529
+ // ---------------------------------------------------------------------------
530
+ // MCP Server
531
+ // ---------------------------------------------------------------------------
532
+
533
+ const server = new McpServer({
534
+ name: 'utopia',
535
+ version: '0.1.0',
536
+ });
537
+
538
+ // ---- Tool 1: get_production_context ----
539
+
540
+ server.tool(
541
+ 'get_production_context',
542
+ 'Get production context relevant to a coding task. Analyzes probe data from production to provide real-time context about how code runs, including errors, database patterns, API calls, and infrastructure details.',
543
+ {
544
+ prompt: z.string().describe('The coding task or question to find relevant production context for'),
545
+ file: z.string().optional().describe('Specific file path to focus on'),
546
+ limit: z.number().optional().default(20).describe('Maximum number of results to return'),
547
+ },
548
+ async ({ prompt, file, limit }) => {
549
+ const params: Record<string, string> = {
550
+ prompt: encodeURIComponent(prompt),
551
+ limit: String(limit ?? 20),
552
+ };
553
+ if (file) params.file = file;
554
+
555
+ const data = await fetchFromUtopia('/api/v1/probes/context', params);
556
+
557
+ if (isErrorResponse(data)) {
558
+ return { content: [{ type: 'text' as const, text: `Error: ${data.error}` }] };
559
+ }
560
+
561
+ const response = data as ProbeListResponse;
562
+ const text = formatContextProbes(response.probes ?? [], response.keywords ?? []);
563
+
564
+ return { content: [{ type: 'text' as const, text }] };
565
+ },
566
+ );
567
+
568
+ // ---- Tool 2: get_recent_errors ----
569
+
570
+ server.tool(
571
+ 'get_recent_errors',
572
+ 'Get recent production errors with full context including stack traces, input data that caused the error, and the exact code line where it broke.',
573
+ {
574
+ hours: z.number().optional().default(24).describe('Lookback window in hours'),
575
+ file: z.string().optional().describe('Filter errors by file path'),
576
+ limit: z.number().optional().default(20).describe('Maximum number of errors to return'),
577
+ },
578
+ async ({ hours, file, limit }) => {
579
+ const params: Record<string, string> = {
580
+ hours: String(hours ?? 24),
581
+ limit: String(limit ?? 20),
582
+ };
583
+ if (file) params.file = file;
584
+
585
+ const data = await fetchFromUtopia('/api/v1/probes/errors/recent', params);
586
+
587
+ if (isErrorResponse(data)) {
588
+ return { content: [{ type: 'text' as const, text: `Error: ${data.error}` }] };
589
+ }
590
+
591
+ const response = data as ProbeListResponse;
592
+ let probes = response.probes ?? [];
593
+
594
+ // Client-side file filter since the server endpoint doesn't support it natively
595
+ if (file) {
596
+ probes = probes.filter(p => p.file === file || p.file.includes(file));
597
+ }
598
+
599
+ const text = formatErrors(probes);
600
+
601
+ return { content: [{ type: 'text' as const, text }] };
602
+ },
603
+ );
604
+
605
+ // ---- Tool 3: get_database_context ----
606
+
607
+ server.tool(
608
+ 'get_database_context',
609
+ 'Get database interaction patterns from production. Shows queries, tables accessed, response times, connection details, and data patterns for database operations in the codebase.',
610
+ {
611
+ file: z.string().optional().describe('Filter by file path'),
612
+ function_name: z.string().optional().describe('Filter by function name'),
613
+ limit: z.number().optional().default(20).describe('Maximum number of results'),
614
+ },
615
+ async ({ file, function_name, limit }) => {
616
+ const params: Record<string, string> = {
617
+ probe_type: 'database',
618
+ limit: String(limit ?? 20),
619
+ };
620
+ if (file) params.file = file;
621
+ if (function_name) params.function_name = function_name;
622
+
623
+ const data = await fetchFromUtopia('/api/v1/probes', params);
624
+
625
+ if (isErrorResponse(data)) {
626
+ return { content: [{ type: 'text' as const, text: `Error: ${data.error}` }] };
627
+ }
628
+
629
+ const response = data as ProbeListResponse;
630
+ const text = formatDatabaseContext(response.probes ?? []);
631
+
632
+ return { content: [{ type: 'text' as const, text }] };
633
+ },
634
+ );
635
+
636
+ // ---- Tool 4: get_api_context ----
637
+
638
+ server.tool(
639
+ 'get_api_context',
640
+ 'Get external API call patterns from production. Shows endpoints called, HTTP methods, response codes, latencies, and request/response patterns.',
641
+ {
642
+ file: z.string().optional().describe('Filter by file path'),
643
+ url_pattern: z.string().optional().describe('Filter by URL pattern (substring match)'),
644
+ limit: z.number().optional().default(20).describe('Maximum number of results'),
645
+ },
646
+ async ({ file, url_pattern, limit }) => {
647
+ const params: Record<string, string> = {
648
+ probe_type: 'api',
649
+ limit: String(limit ?? 20),
650
+ };
651
+ if (file) params.file = file;
652
+
653
+ const data = await fetchFromUtopia('/api/v1/probes', params);
654
+
655
+ if (isErrorResponse(data)) {
656
+ return { content: [{ type: 'text' as const, text: `Error: ${data.error}` }] };
657
+ }
658
+
659
+ const response = data as ProbeListResponse;
660
+ let probes = response.probes ?? [];
661
+
662
+ // Client-side URL pattern filter
663
+ if (url_pattern) {
664
+ const pattern = url_pattern.toLowerCase();
665
+ probes = probes.filter(p => {
666
+ const url = String(p.data.url ?? '').toLowerCase();
667
+ return url.includes(pattern);
668
+ });
669
+ }
670
+
671
+ const text = formatApiContext(probes);
672
+
673
+ return { content: [{ type: 'text' as const, text }] };
674
+ },
675
+ );
676
+
677
+ // ---- Tool 5: get_infrastructure_context ----
678
+
679
+ server.tool(
680
+ 'get_infrastructure_context',
681
+ 'Get infrastructure and deployment context. Shows where code is deployed, cloud provider, region, service type, environment variables, and resource usage.',
682
+ {},
683
+ async () => {
684
+ const params: Record<string, string> = {
685
+ probe_type: 'infra',
686
+ limit: '10',
687
+ };
688
+
689
+ const data = await fetchFromUtopia('/api/v1/probes', params);
690
+
691
+ if (isErrorResponse(data)) {
692
+ return { content: [{ type: 'text' as const, text: `Error: ${data.error}` }] };
693
+ }
694
+
695
+ const response = data as ProbeListResponse;
696
+ const text = formatInfraContext(response.probes ?? []);
697
+
698
+ return { content: [{ type: 'text' as const, text }] };
699
+ },
700
+ );
701
+
702
+ // ---- Tool 6: get_impact_analysis ----
703
+
704
+ server.tool(
705
+ 'get_impact_analysis',
706
+ 'Analyze the impact of changing a specific function, file, or service. Shows all dependent code, services, and infrastructure that would be affected.',
707
+ {
708
+ node_id: z.string().optional().describe('Direct node ID in the impact graph'),
709
+ file: z.string().optional().describe('File path to find in the graph'),
710
+ function_name: z.string().optional().describe('Function name to find in the graph'),
711
+ },
712
+ async ({ node_id, file, function_name }) => {
713
+ let nodeId = node_id;
714
+
715
+ // If no direct node_id, look up the node by file and/or function_name
716
+ if (!nodeId) {
717
+ if (!file && !function_name) {
718
+ return {
719
+ content: [{
720
+ type: 'text' as const,
721
+ text: 'Error: Provide at least one of node_id, file, or function_name.',
722
+ }],
723
+ };
724
+ }
725
+
726
+ const graphData = await fetchFromUtopia('/api/v1/graph');
727
+
728
+ if (isErrorResponse(graphData)) {
729
+ return { content: [{ type: 'text' as const, text: `Error: ${graphData.error}` }] };
730
+ }
731
+
732
+ const graph = graphData as GraphResponse;
733
+ const matchingNode = graph.nodes.find(n => {
734
+ if (file && function_name) {
735
+ return n.file === file && n.name === function_name;
736
+ }
737
+ if (file) {
738
+ return n.file === file;
739
+ }
740
+ return n.name === function_name;
741
+ });
742
+
743
+ if (!matchingNode) {
744
+ // Try a more relaxed match (substring)
745
+ const relaxedMatch = graph.nodes.find(n => {
746
+ const fileMatch = file
747
+ ? (n.file ?? '').includes(file) || file.includes(n.file ?? '\0')
748
+ : true;
749
+ const fnMatch = function_name
750
+ ? n.name === function_name || n.name.includes(function_name)
751
+ : true;
752
+ return fileMatch && fnMatch;
753
+ });
754
+
755
+ if (!relaxedMatch) {
756
+ const hint = file && function_name
757
+ ? `file="${file}", function="${function_name}"`
758
+ : file
759
+ ? `file="${file}"`
760
+ : `function="${function_name}"`;
761
+ return {
762
+ content: [{
763
+ type: 'text' as const,
764
+ text: `No graph node found matching ${hint}. The impact graph may not have data for this code yet. Ensure probes are running in production.`,
765
+ }],
766
+ };
767
+ }
768
+
769
+ nodeId = relaxedMatch.id;
770
+ } else {
771
+ nodeId = matchingNode.id;
772
+ }
773
+ }
774
+
775
+ const impactData = await fetchFromUtopia(`/api/v1/graph/impact/${encodeURIComponent(nodeId)}`);
776
+
777
+ if (isErrorResponse(impactData)) {
778
+ return { content: [{ type: 'text' as const, text: `Error: ${impactData.error}` }] };
779
+ }
780
+
781
+ const impact = impactData as ImpactResponse;
782
+ const text = formatImpactAnalysis(nodeId, impact);
783
+
784
+ return { content: [{ type: 'text' as const, text }] };
785
+ },
786
+ );
787
+
788
+ // ---- Tool 7: get_full_context ----
789
+
790
+ server.tool(
791
+ 'get_full_context',
792
+ 'Get comprehensive production context for the entire project. Combines recent errors, database patterns, API patterns, and infrastructure into a complete picture. Use this when starting work on a new task.',
793
+ {
794
+ limit: z.number().optional().default(10).describe('Maximum results per category'),
795
+ },
796
+ async ({ limit }) => {
797
+ const perCategory = String(limit ?? 10);
798
+
799
+ // Fire all requests in parallel
800
+ const [errorsData, dbData, apiData, infraData] = await Promise.all([
801
+ fetchFromUtopia('/api/v1/probes/errors/recent', {
802
+ hours: '24',
803
+ limit: perCategory,
804
+ }),
805
+ fetchFromUtopia('/api/v1/probes', {
806
+ probe_type: 'database',
807
+ limit: perCategory,
808
+ }),
809
+ fetchFromUtopia('/api/v1/probes', {
810
+ probe_type: 'api',
811
+ limit: perCategory,
812
+ }),
813
+ fetchFromUtopia('/api/v1/probes', {
814
+ probe_type: 'infra',
815
+ limit: perCategory,
816
+ }),
817
+ ]);
818
+
819
+ const sections: string[] = [];
820
+
821
+ sections.push('=== FULL PRODUCTION CONTEXT ===');
822
+ sections.push('');
823
+
824
+ // Errors section
825
+ sections.push('============================');
826
+ sections.push(' RECENT ERRORS (last 24h)');
827
+ sections.push('============================');
828
+ if (isErrorResponse(errorsData)) {
829
+ sections.push(`Error fetching errors: ${errorsData.error}`);
830
+ } else {
831
+ const resp = errorsData as ProbeListResponse;
832
+ sections.push(formatErrors(resp.probes ?? []));
833
+ }
834
+ sections.push('');
835
+
836
+ // Database section
837
+ sections.push('============================');
838
+ sections.push(' DATABASE PATTERNS');
839
+ sections.push('============================');
840
+ if (isErrorResponse(dbData)) {
841
+ sections.push(`Error fetching database context: ${dbData.error}`);
842
+ } else {
843
+ const resp = dbData as ProbeListResponse;
844
+ sections.push(formatDatabaseContext(resp.probes ?? []));
845
+ }
846
+ sections.push('');
847
+
848
+ // API section
849
+ sections.push('============================');
850
+ sections.push(' API CALL PATTERNS');
851
+ sections.push('============================');
852
+ if (isErrorResponse(apiData)) {
853
+ sections.push(`Error fetching API context: ${apiData.error}`);
854
+ } else {
855
+ const resp = apiData as ProbeListResponse;
856
+ sections.push(formatApiContext(resp.probes ?? []));
857
+ }
858
+ sections.push('');
859
+
860
+ // Infrastructure section
861
+ sections.push('============================');
862
+ sections.push(' INFRASTRUCTURE');
863
+ sections.push('============================');
864
+ if (isErrorResponse(infraData)) {
865
+ sections.push(`Error fetching infrastructure context: ${infraData.error}`);
866
+ } else {
867
+ const resp = infraData as ProbeListResponse;
868
+ sections.push(formatInfraContext(resp.probes ?? []));
869
+ }
870
+
871
+ const text = sections.join('\n');
872
+
873
+ return { content: [{ type: 'text' as const, text }] };
874
+ },
875
+ );
876
+
877
+ // ---------------------------------------------------------------------------
878
+ // Start
879
+ // ---------------------------------------------------------------------------
880
+
881
+ async function main() {
882
+ const transport = new StdioServerTransport();
883
+ await server.connect(transport);
884
+ }
885
+
886
+ main().catch(console.error);