@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.
- package/.claude/settings.json +1 -0
- package/.claude/settings.local.json +38 -0
- package/bin/utopia.js +20 -0
- package/package.json +46 -0
- package/python/README.md +34 -0
- package/python/instrumenter/instrument.py +1148 -0
- package/python/pyproject.toml +32 -0
- package/python/setup.py +27 -0
- package/python/utopia_runtime/__init__.py +30 -0
- package/python/utopia_runtime/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/utopia_runtime/__pycache__/client.cpython-313.pyc +0 -0
- package/python/utopia_runtime/__pycache__/probe.cpython-313.pyc +0 -0
- package/python/utopia_runtime/client.py +31 -0
- package/python/utopia_runtime/probe.py +446 -0
- package/python/utopia_runtime.egg-info/PKG-INFO +59 -0
- package/python/utopia_runtime.egg-info/SOURCES.txt +10 -0
- package/python/utopia_runtime.egg-info/dependency_links.txt +1 -0
- package/python/utopia_runtime.egg-info/top_level.txt +1 -0
- package/scripts/publish-npm.sh +14 -0
- package/scripts/publish-pypi.sh +17 -0
- package/src/cli/commands/codex.ts +193 -0
- package/src/cli/commands/context.ts +188 -0
- package/src/cli/commands/destruct.ts +237 -0
- package/src/cli/commands/easter-eggs.ts +203 -0
- package/src/cli/commands/init.ts +505 -0
- package/src/cli/commands/instrument.ts +962 -0
- package/src/cli/commands/mcp.ts +16 -0
- package/src/cli/commands/serve.ts +194 -0
- package/src/cli/commands/status.ts +304 -0
- package/src/cli/commands/validate.ts +328 -0
- package/src/cli/index.ts +37 -0
- package/src/cli/utils/config.ts +54 -0
- package/src/graph/index.ts +687 -0
- package/src/instrumenter/javascript.ts +1798 -0
- package/src/mcp/index.ts +886 -0
- package/src/runtime/js/index.ts +518 -0
- package/src/runtime/js/package-lock.json +30 -0
- package/src/runtime/js/package.json +30 -0
- package/src/runtime/js/tsconfig.json +16 -0
- package/src/server/db/index.ts +26 -0
- package/src/server/db/schema.ts +45 -0
- package/src/server/index.ts +79 -0
- package/src/server/middleware/auth.ts +74 -0
- package/src/server/routes/admin.ts +36 -0
- package/src/server/routes/graph.ts +358 -0
- package/src/server/routes/probes.ts +286 -0
- package/src/types.ts +147 -0
- package/src/utopia-mode/index.ts +206 -0
- package/tsconfig.json +19 -0
package/src/mcp/index.ts
ADDED
|
@@ -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);
|