@arbotdev/metis-mcp 1.0.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/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # Metis MCP Server
2
+
3
+ MCP (Model Context Protocol) server that exposes Metis code intelligence tools to Cursor/VS Code AI chat.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @arbotdev/metis-mcp
9
+ ```
10
+
11
+ Or use directly with npx (recommended for Cursor config):
12
+
13
+ ```bash
14
+ npx @arbotdev/metis-mcp
15
+ ```
16
+
17
+ ## Configuration
18
+
19
+ ### 1. Get a JWT Token
20
+
21
+ Contact your Metis administrator to get a JWT token for your repositories.
22
+
23
+ ### 2. Configure Cursor
24
+
25
+ Add to `~/.cursor/mcp.json`:
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "metis": {
31
+ "command": "npx",
32
+ "args": ["-y", "@arbotdev/metis-mcp@1.0.0"],
33
+ "env": {
34
+ "METIS_API_URL": "https://metis-api-13539721132.us-central1.run.app",
35
+ "METIS_API_TOKEN": "your-jwt-token-here"
36
+ }
37
+ }
38
+ }
39
+ }
40
+ ```
41
+
42
+ ### 3. Restart Cursor
43
+
44
+ Restart Cursor to load the MCP server.
45
+
46
+ ## Tools
47
+
48
+ | Tool | Description |
49
+ |------|-------------|
50
+ | `metis_plan_change` | Plan a code change with blast radius + AI explanation |
51
+ | `metis_explain_impact` | Get blast radius / impact analysis for a symbol |
52
+ | `metis_search` | Search for symbols in the codebase |
53
+ | `metis_resolve` | Resolve symbol from file:line position |
54
+ | `metis_get_snippet` | Get code snippet from a file |
55
+ | `metis_doctor` | Check authentication and API connectivity |
56
+
57
+ ## Usage Examples
58
+
59
+ In Cursor chat, ask:
60
+
61
+ - "What will this change break?" → Uses `metis_plan_change`
62
+ - "What calls this function?" → Uses `metis_explain_impact`
63
+ - "Find the UserAuth class" → Uses `metis_search`
64
+ - "Check my Metis connection" → Uses `metis_doctor`
65
+
66
+ ### Plan a refactor
67
+
68
+ ```
69
+ I want to refactor the get_backend function to support multiple database backends.
70
+ What will this change break?
71
+ ```
72
+
73
+ ### Check impact
74
+
75
+ ```
76
+ Show me the blast radius for the CloudBackend class.
77
+ ```
78
+
79
+ ### Find a symbol
80
+
81
+ ```
82
+ Search for validate_token in the codebase.
83
+ ```
84
+
85
+ ### Verify setup
86
+
87
+ ```
88
+ Run metis doctor to check my connection.
89
+ ```
90
+
91
+ ## Environment Variables
92
+
93
+ | Variable | Required | Default | Description |
94
+ |----------|----------|---------|-------------|
95
+ | `METIS_API_TOKEN` | **Yes** | - | JWT token for authentication |
96
+ | `METIS_API_URL` | No | `https://metis-api-...` | Metis API base URL |
97
+
98
+ ## Troubleshooting
99
+
100
+ ### "Authentication Not Configured"
101
+
102
+ The `METIS_API_TOKEN` environment variable is not set. Add it to your Cursor MCP config.
103
+
104
+ ### "REPO_NOT_ALLOWED"
105
+
106
+ Your token doesn't have access to the requested repository. Check your `repo_allowlist` with `metis_doctor`.
107
+
108
+ ### "TOKEN_EXPIRED"
109
+
110
+ Your JWT has expired. Contact your Metis administrator for a new token.
111
+
112
+ ## License
113
+
114
+ MIT
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Metis MCP Server
4
+ *
5
+ * Exposes Metis code intelligence tools to Cursor/VS Code AI chat.
6
+ *
7
+ * Tools:
8
+ * - metis_plan_change: Plan a code change with blast radius + LLM explanation
9
+ * - metis_explain_impact: Get blast radius for a symbol
10
+ * - metis_search: Search for symbols in the codebase
11
+ * - metis_resolve: Resolve symbol from file:line position
12
+ * - metis_get_snippet: Get code snippet from a file
13
+ * - metis_doctor: Check authentication and API connectivity
14
+ *
15
+ * Environment:
16
+ * - METIS_API_URL: API base URL (default: production)
17
+ * - METIS_API_TOKEN: JWT token for authentication (REQUIRED)
18
+ */
19
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,666 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Metis MCP Server
5
+ *
6
+ * Exposes Metis code intelligence tools to Cursor/VS Code AI chat.
7
+ *
8
+ * Tools:
9
+ * - metis_plan_change: Plan a code change with blast radius + LLM explanation
10
+ * - metis_explain_impact: Get blast radius for a symbol
11
+ * - metis_search: Search for symbols in the codebase
12
+ * - metis_resolve: Resolve symbol from file:line position
13
+ * - metis_get_snippet: Get code snippet from a file
14
+ * - metis_doctor: Check authentication and API connectivity
15
+ *
16
+ * Environment:
17
+ * - METIS_API_URL: API base URL (default: production)
18
+ * - METIS_API_TOKEN: JWT token for authentication (REQUIRED)
19
+ */
20
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ var desc = Object.getOwnPropertyDescriptor(m, k);
23
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
24
+ desc = { enumerable: true, get: function() { return m[k]; } };
25
+ }
26
+ Object.defineProperty(o, k2, desc);
27
+ }) : (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ o[k2] = m[k];
30
+ }));
31
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
32
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
33
+ }) : function(o, v) {
34
+ o["default"] = v;
35
+ });
36
+ var __importStar = (this && this.__importStar) || (function () {
37
+ var ownKeys = function(o) {
38
+ ownKeys = Object.getOwnPropertyNames || function (o) {
39
+ var ar = [];
40
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
41
+ return ar;
42
+ };
43
+ return ownKeys(o);
44
+ };
45
+ return function (mod) {
46
+ if (mod && mod.__esModule) return mod;
47
+ var result = {};
48
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
49
+ __setModuleDefault(result, mod);
50
+ return result;
51
+ };
52
+ })();
53
+ Object.defineProperty(exports, "__esModule", { value: true });
54
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
55
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
56
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
57
+ // Configuration
58
+ const METIS_API_URL = process.env.METIS_API_URL || "https://metis-api-i5j6s4n2zq-uc.a.run.app";
59
+ const METIS_API_TOKEN = process.env.METIS_API_TOKEN || "";
60
+ // =============================================================================
61
+ // Tool Definitions
62
+ // =============================================================================
63
+ const TOOLS = [
64
+ {
65
+ name: "metis_plan_change",
66
+ description: `Plan a code change and get blast radius analysis with AI explanation.
67
+
68
+ Use this when the user asks:
69
+ - "What will this change break?"
70
+ - "Plan this refactor"
71
+ - "What's the impact of changing X?"
72
+ - "Help me understand the blast radius"
73
+
74
+ Returns structured impact analysis plus AI-generated explanation.`,
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ repo: {
79
+ type: "string",
80
+ description: "Repository name (e.g., 'owner/repo')"
81
+ },
82
+ symbol_id: {
83
+ type: "string",
84
+ description: "Symbol ID to analyze (e.g., 'owner/repo//path/file.py::module.ClassName')"
85
+ },
86
+ intent: {
87
+ type: "string",
88
+ description: "What the user is trying to change (1-2 sentences)"
89
+ },
90
+ depth: {
91
+ type: "number",
92
+ description: "Traversal depth (1-5, default: 3)",
93
+ default: 3
94
+ },
95
+ min_confidence: {
96
+ type: "number",
97
+ description: "Minimum confidence for cross-repo edges (0-1, default: 0.85)",
98
+ default: 0.85
99
+ }
100
+ },
101
+ required: ["repo", "symbol_id", "intent"]
102
+ }
103
+ },
104
+ {
105
+ name: "metis_explain_impact",
106
+ description: `Get blast radius / impact analysis for a symbol.
107
+
108
+ Use this when the user asks:
109
+ - "What calls this function?"
110
+ - "What depends on this?"
111
+ - "Show me the blast radius"
112
+ - "Who uses this class?"
113
+
114
+ Returns callers, references, cross-repo impact, and risk assessment.`,
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ symbol_id: {
119
+ type: "string",
120
+ description: "Symbol ID to analyze"
121
+ },
122
+ depth: {
123
+ type: "number",
124
+ description: "Traversal depth (1-5, default: 3)",
125
+ default: 3
126
+ },
127
+ min_confidence: {
128
+ type: "number",
129
+ description: "Minimum confidence for cross-repo edges (0-1, default: 0.85)",
130
+ default: 0.85
131
+ },
132
+ max_paths: {
133
+ type: "number",
134
+ description: "Maximum paths to return (default: 50)",
135
+ default: 50
136
+ }
137
+ },
138
+ required: ["symbol_id"]
139
+ }
140
+ },
141
+ {
142
+ name: "metis_search",
143
+ description: `Search for symbols in the codebase.
144
+
145
+ Use this to find symbol IDs before calling other tools.
146
+ Returns matching symbols with their IDs, types, and locations.`,
147
+ inputSchema: {
148
+ type: "object",
149
+ properties: {
150
+ query: {
151
+ type: "string",
152
+ description: "Search query (function name, class name, etc.)"
153
+ },
154
+ repo: {
155
+ type: "string",
156
+ description: "Optional: filter to specific repo (e.g., 'owner/repo')"
157
+ },
158
+ limit: {
159
+ type: "number",
160
+ description: "Maximum results to return (default: 10)",
161
+ default: 10
162
+ }
163
+ },
164
+ required: ["query"]
165
+ }
166
+ },
167
+ {
168
+ name: "metis_resolve",
169
+ description: `Resolve a symbol from file path and line number.
170
+
171
+ Use this when you know the file and line but need the symbol ID.`,
172
+ inputSchema: {
173
+ type: "object",
174
+ properties: {
175
+ repo: {
176
+ type: "string",
177
+ description: "Repository name (e.g., 'owner/repo')"
178
+ },
179
+ file_path: {
180
+ type: "string",
181
+ description: "Path to the file"
182
+ },
183
+ line: {
184
+ type: "number",
185
+ description: "Line number (1-indexed)"
186
+ },
187
+ symbol_name: {
188
+ type: "string",
189
+ description: "Optional: symbol name hint"
190
+ }
191
+ },
192
+ required: ["repo", "file_path", "line"]
193
+ }
194
+ },
195
+ {
196
+ name: "metis_get_snippet",
197
+ description: `Get a code snippet from a file in an indexed repository.
198
+
199
+ Use this when you need to see specific code:
200
+ - Show code context around a symbol
201
+ - Get a function or class definition
202
+ - Retrieve code for inclusion in prompts`,
203
+ inputSchema: {
204
+ type: "object",
205
+ properties: {
206
+ repo: {
207
+ type: "string",
208
+ description: "Repository name (e.g., 'owner/repo')"
209
+ },
210
+ file_path: {
211
+ type: "string",
212
+ description: "Path to file within repo"
213
+ },
214
+ line_start: {
215
+ type: "number",
216
+ description: "Start line (1-indexed)"
217
+ },
218
+ line_end: {
219
+ type: "number",
220
+ description: "End line (1-indexed, inclusive)"
221
+ }
222
+ },
223
+ required: ["repo", "file_path", "line_start", "line_end"]
224
+ }
225
+ },
226
+ {
227
+ name: "metis_doctor",
228
+ description: `Check Metis API connectivity and authentication status.
229
+
230
+ Use this to verify:
231
+ - API is reachable
232
+ - Token is valid
233
+ - What repos you have access to
234
+ - What scopes are granted`,
235
+ inputSchema: {
236
+ type: "object",
237
+ properties: {},
238
+ required: []
239
+ }
240
+ }
241
+ ];
242
+ // =============================================================================
243
+ // API Helpers
244
+ // =============================================================================
245
+ function getAuthHeaders() {
246
+ const headers = {
247
+ "Content-Type": "application/json",
248
+ };
249
+ if (METIS_API_TOKEN) {
250
+ headers["Authorization"] = `Bearer ${METIS_API_TOKEN}`;
251
+ }
252
+ return headers;
253
+ }
254
+ function checkAuthConfigured() {
255
+ if (!METIS_API_TOKEN) {
256
+ return `## ❌ Authentication Not Configured
257
+
258
+ METIS_API_TOKEN environment variable is not set.
259
+
260
+ **To fix:**
261
+ 1. Generate a token: \`python scripts/mint_jwt.py --sub "your-name" --repos "owner/repo1,owner/repo2"\`
262
+ 2. Add to your Cursor MCP config (~/.cursor/mcp.json):
263
+
264
+ \`\`\`json
265
+ {
266
+ "mcpServers": {
267
+ "metis": {
268
+ "command": "npx",
269
+ "args": ["@arbotdev/metis-mcp"],
270
+ "env": {
271
+ "METIS_API_URL": "${METIS_API_URL}",
272
+ "METIS_API_TOKEN": "your-jwt-token-here"
273
+ }
274
+ }
275
+ }
276
+ }
277
+ \`\`\`
278
+
279
+ 3. Restart Cursor`;
280
+ }
281
+ return null;
282
+ }
283
+ async function fetchJson(url, options) {
284
+ // Dynamic import for node-fetch
285
+ const { default: fetch } = await Promise.resolve().then(() => __importStar(require("node-fetch")));
286
+ const headers = {
287
+ ...getAuthHeaders(),
288
+ ...options?.headers,
289
+ };
290
+ const response = await fetch(url, {
291
+ method: options?.method || "GET",
292
+ body: options?.body,
293
+ headers,
294
+ });
295
+ const text = await response.text();
296
+ let data = null;
297
+ try {
298
+ data = text ? JSON.parse(text) : null;
299
+ }
300
+ catch {
301
+ // Ignore parse errors
302
+ }
303
+ if (!response.ok) {
304
+ // Extract structured error if available
305
+ const errorCode = data?.detail?.error_code || data?.error_code || `HTTP_${response.status}`;
306
+ const errorMsg = data?.detail?.message || data?.message || data?.detail || text.slice(0, 200);
307
+ throw new Error(`${errorCode}: ${errorMsg}`);
308
+ }
309
+ return data;
310
+ }
311
+ async function handlePlanChange(args) {
312
+ const authError = checkAuthConfigured();
313
+ if (authError)
314
+ return authError;
315
+ const url = `${METIS_API_URL}/api/symbols/plan/change`;
316
+ const response = await fetchJson(url, {
317
+ method: "POST",
318
+ body: JSON.stringify({
319
+ repo: args.repo,
320
+ symbol_id: args.symbol_id,
321
+ intent: args.intent,
322
+ depth: args.depth || 3,
323
+ min_confidence: args.min_confidence || 0.85,
324
+ max_paths: 100,
325
+ include_inferred: true,
326
+ include_declared_only: false,
327
+ explain: true,
328
+ }),
329
+ });
330
+ // Format response for chat
331
+ const impact = response.impact || {};
332
+ const riskLevel = impact.overall_risk || "unknown";
333
+ const riskEmoji = riskLevel === "high" ? "🔴" : riskLevel === "medium" ? "🟡" : "🟢";
334
+ const fileDisplay = impact.symbol_file || "(path unavailable)";
335
+ let result = `## 🧭 Plan: \`${impact.symbol_name || args.symbol_id}\`\n\n`;
336
+ result += `**Location:** \`${impact.symbol_repo || args.repo}\` · \`${fileDisplay}\`\n`;
337
+ result += `**Risk Level:** ${riskEmoji} ${riskLevel.toUpperCase()}\n\n`;
338
+ result += `### Impact Summary\n`;
339
+ result += `- **Direct callers:** ${impact.direct_caller_count || 0}\n`;
340
+ result += `- **Direct references:** ${impact.direct_reference_count || 0}\n`;
341
+ result += `- **Transitive callers:** ${impact.transitive_caller_count || 0}\n`;
342
+ result += `- **Repos affected:** ${impact.total_repos_affected || 0}\n\n`;
343
+ // Risk flags
344
+ if (response.risk_flags?.length > 0) {
345
+ result += `### ⚠️ Risk Flags\n`;
346
+ for (const flag of response.risk_flags) {
347
+ result += `- **${flag.code}** (${flag.severity})\n`;
348
+ }
349
+ result += `\n`;
350
+ }
351
+ // Recommended steps
352
+ if (response.recommended_next_steps?.length > 0) {
353
+ result += `### ✅ Suggested Next Steps\n`;
354
+ for (const step of response.recommended_next_steps) {
355
+ result += `${step.priority}. ${step.type.replace(/_/g, " ").toLowerCase()}\n`;
356
+ }
357
+ result += `\n`;
358
+ }
359
+ // LLM explanation
360
+ if (response.explanation_markdown) {
361
+ result += `### 📝 AI Explanation\n`;
362
+ result += response.explanation_markdown + `\n\n`;
363
+ }
364
+ // "What Metis ran" footer (transparency)
365
+ result += `---\n\n`;
366
+ result += `<details>\n`;
367
+ result += `<summary>🔍 What Metis ran</summary>\n\n`;
368
+ result += `**Endpoint:** \`POST /api/symbols/plan/change\`\n\n`;
369
+ result += `**Parameters:**\n`;
370
+ result += "```json\n";
371
+ result += JSON.stringify({
372
+ repo: args.repo,
373
+ symbol_id: args.symbol_id,
374
+ intent: args.intent,
375
+ depth: args.depth || 3,
376
+ min_confidence: args.min_confidence || 0.85,
377
+ max_paths: 100,
378
+ explain: true
379
+ }, null, 2);
380
+ result += "\n```\n\n";
381
+ result += `**Request ID:** \`${response.request_id}\`\n`;
382
+ result += `**Plan ID:** \`${response.plan_id}\`\n\n`;
383
+ result += `</details>`;
384
+ return result;
385
+ }
386
+ async function handleExplainImpact(args) {
387
+ const authError = checkAuthConfigured();
388
+ if (authError)
389
+ return authError;
390
+ // Use POST endpoint to avoid URL encoding issues with symbol_id containing // and ::
391
+ const url = `${METIS_API_URL}/api/symbols/impact`;
392
+ const response = await fetchJson(url, {
393
+ method: "POST",
394
+ body: JSON.stringify({
395
+ symbol_id: args.symbol_id,
396
+ depth: args.depth || 3,
397
+ min_confidence: args.min_confidence || 0.85,
398
+ max_paths: args.max_paths || 50,
399
+ include_references: true,
400
+ }),
401
+ });
402
+ const riskLevel = response.overall_risk || "unknown";
403
+ const riskEmoji = riskLevel === "high" ? "🔴" : riskLevel === "medium" ? "🟡" : "🟢";
404
+ const fileDisplay = response.symbol_file || "(path unavailable)";
405
+ let result = `## 💥 Impact Analysis: \`${response.symbol_name}\`\n\n`;
406
+ result += `**Location:** \`${response.symbol_repo}\` · \`${fileDisplay}\`\n`;
407
+ result += `**Risk Level:** ${riskEmoji} ${riskLevel.toUpperCase()}\n\n`;
408
+ result += `### Summary\n`;
409
+ result += `- **Direct callers:** ${response.direct_caller_count || 0}\n`;
410
+ result += `- **Direct references:** ${response.direct_reference_count || 0}\n`;
411
+ result += `- **Transitive callers:** ${response.transitive_caller_count || 0}\n`;
412
+ result += `- **Transitive references:** ${response.transitive_reference_count || 0}\n`;
413
+ result += `- **Repos affected:** ${response.total_repos_affected || 0}\n`;
414
+ if (response.truncated) {
415
+ result += `- ⚠️ Results truncated (max ${response.max_paths_used} paths)\n`;
416
+ }
417
+ result += `\n`;
418
+ // Top callers
419
+ if (response.direct_callers?.length > 0) {
420
+ result += `### Top Callers\n`;
421
+ for (const caller of response.direct_callers.slice(0, 5)) {
422
+ const xrepo = caller.is_cross_repo ? " 🌐" : "";
423
+ result += `- \`${caller.name}\` in \`${caller.repo}\`${xrepo}\n`;
424
+ }
425
+ result += `\n`;
426
+ }
427
+ // Cross-repo impact
428
+ if (response.cross_repo_impact?.length > 0) {
429
+ result += `### 🌐 Cross-Repo Impact\n`;
430
+ for (const repo of response.cross_repo_impact.slice(0, 5)) {
431
+ result += `- **${repo.repo}**: ${repo.caller_count} callers, ${repo.reference_count} refs\n`;
432
+ }
433
+ result += `\n`;
434
+ }
435
+ // "What Metis ran" footer (transparency)
436
+ result += `---\n\n`;
437
+ result += `<details>\n`;
438
+ result += `<summary>🔍 What Metis ran</summary>\n\n`;
439
+ result += `**Endpoint:** \`POST /api/symbols/impact\`\n\n`;
440
+ result += `**Parameters:**\n`;
441
+ result += "```json\n";
442
+ result += JSON.stringify({
443
+ symbol_id: args.symbol_id,
444
+ depth: args.depth || 3,
445
+ min_confidence: args.min_confidence || 0.85,
446
+ max_paths: args.max_paths || 50,
447
+ include_references: true
448
+ }, null, 2);
449
+ result += "\n```\n\n";
450
+ result += `**Request ID:** \`${response.request_id || "N/A"}\`\n\n`;
451
+ result += `</details>`;
452
+ return result;
453
+ }
454
+ async function handleSearch(args) {
455
+ const authError = checkAuthConfigured();
456
+ if (authError)
457
+ return authError;
458
+ const params = new URLSearchParams({
459
+ q: args.query,
460
+ k: String(args.limit || 10),
461
+ });
462
+ if (args.repo) {
463
+ params.set("repo", args.repo);
464
+ }
465
+ const url = `${METIS_API_URL}/api/search/v2?${params}`;
466
+ const response = await fetchJson(url);
467
+ if (!response.candidates?.length) {
468
+ return `No symbols found matching "${args.query}"`;
469
+ }
470
+ let result = `## 🔍 Search Results: "${args.query}"\n\n`;
471
+ result += `Found ${response.candidates.length} symbol(s):\n\n`;
472
+ for (const item of response.candidates) {
473
+ result += `### \`${item.display_name || item.symbol_key}\`\n`;
474
+ result += `- **ID:** \`${item.symbol_key}\`\n`;
475
+ if (item.kind)
476
+ result += `- **Kind:** ${item.kind}\n`;
477
+ if (item.path)
478
+ result += `- **File:** ${item.path}\n`;
479
+ if (item.scores?.final)
480
+ result += `- **Score:** ${item.scores.final.toFixed(3)}\n`;
481
+ result += `\n`;
482
+ }
483
+ return result;
484
+ }
485
+ async function handleResolve(args) {
486
+ const authError = checkAuthConfigured();
487
+ if (authError)
488
+ return authError;
489
+ const url = `${METIS_API_URL}/api/symbols/resolve`;
490
+ const response = await fetchJson(url, {
491
+ method: "POST",
492
+ body: JSON.stringify({
493
+ repo: args.repo,
494
+ file_path: args.file_path,
495
+ line: args.line,
496
+ symbol_name: args.symbol_name,
497
+ }),
498
+ });
499
+ if (!response.found) {
500
+ return `No symbol found at ${args.file_path}:${args.line}`;
501
+ }
502
+ let result = `## ✅ Symbol Resolved\n\n`;
503
+ result += `- **ID:** \`${response.symbol_id}\`\n`;
504
+ if (response.name)
505
+ result += `- **Name:** ${response.name}\n`;
506
+ if (response.qualname)
507
+ result += `- **Qualified Name:** ${response.qualname}\n`;
508
+ if (response.kind)
509
+ result += `- **Kind:** ${response.kind}\n`;
510
+ return result;
511
+ }
512
+ async function handleSnippet(args) {
513
+ const authError = checkAuthConfigured();
514
+ if (authError)
515
+ return authError;
516
+ const url = `${METIS_API_URL}/api/snippet`;
517
+ const response = await fetchJson(url, {
518
+ method: "POST",
519
+ body: JSON.stringify({
520
+ repo: args.repo,
521
+ file_path: args.file_path,
522
+ line_start: args.line_start,
523
+ line_end: args.line_end,
524
+ }),
525
+ });
526
+ let result = `## 📄 Code Snippet\n\n`;
527
+ result += `**File:** \`${response.repo}/${response.file_path}\`\n`;
528
+ result += `**Lines:** ${response.line_start}-${response.line_end} (of ${response.total_lines})\n\n`;
529
+ if (response.signature) {
530
+ result += `**Signature:** \`${response.signature}\`\n\n`;
531
+ }
532
+ // Format snippet with line numbers
533
+ const lines = response.snippet.split('\n');
534
+ result += '```\n';
535
+ lines.forEach((line, idx) => {
536
+ const lineNum = response.line_start + idx;
537
+ result += `${lineNum.toString().padStart(4)} | ${line}\n`;
538
+ });
539
+ result += '```\n';
540
+ return result;
541
+ }
542
+ async function handleDoctor() {
543
+ let result = `## 🩺 Metis Doctor\n\n`;
544
+ // Check configuration
545
+ result += `### Configuration\n`;
546
+ result += `- **API URL:** \`${METIS_API_URL}\`\n`;
547
+ result += `- **Token configured:** ${METIS_API_TOKEN ? "✅ Yes" : "❌ No"}\n\n`;
548
+ if (!METIS_API_TOKEN) {
549
+ result += `### ❌ Authentication Required\n\n`;
550
+ result += `METIS_API_TOKEN environment variable is not set.\n\n`;
551
+ result += `**To fix:**\n`;
552
+ result += `1. Generate a token using \`scripts/mint_jwt.py\`\n`;
553
+ result += `2. Add \`METIS_API_TOKEN\` to your MCP config\n`;
554
+ result += `3. Restart Cursor\n`;
555
+ return result;
556
+ }
557
+ // Check API connectivity
558
+ result += `### API Connectivity\n`;
559
+ try {
560
+ const { default: fetch } = await Promise.resolve().then(() => __importStar(require("node-fetch")));
561
+ const healthResp = await fetch(`${METIS_API_URL}/health`, {
562
+ headers: getAuthHeaders(),
563
+ });
564
+ const health = await healthResp.json();
565
+ result += `- **Status:** ${health.status === "healthy" ? "✅ Healthy" : "⚠️ " + health.status}\n`;
566
+ result += `- **Mode:** ${health.mode || "unknown"}\n`;
567
+ result += `- **Neo4j:** ${health.neo4j_connected ? "✅ Connected" : "❌ Not connected"}\n\n`;
568
+ }
569
+ catch (e) {
570
+ result += `- **Status:** ❌ Unreachable\n`;
571
+ result += `- **Error:** ${e instanceof Error ? e.message : String(e)}\n\n`;
572
+ return result;
573
+ }
574
+ // Check authentication
575
+ result += `### Authentication\n`;
576
+ try {
577
+ const whoami = await fetchJson(`${METIS_API_URL}/api/whoami`);
578
+ result += `- **Subject:** ${whoami.sub}\n`;
579
+ result += `- **Token ID:** ${whoami.jti}\n`;
580
+ result += `- **Expires:** ${whoami.expires_at}\n`;
581
+ result += `- **Scopes:** ${whoami.scopes.join(", ")}\n`;
582
+ result += `- **Repos allowed:** ${whoami.repo_allowlist.length}\n\n`;
583
+ if (whoami.repo_allowlist.length <= 10) {
584
+ result += `**Allowed repositories:**\n`;
585
+ for (const repo of whoami.repo_allowlist) {
586
+ result += `- \`${repo}\`\n`;
587
+ }
588
+ }
589
+ else {
590
+ result += `**First 10 allowed repositories:**\n`;
591
+ for (const repo of whoami.repo_allowlist.slice(0, 10)) {
592
+ result += `- \`${repo}\`\n`;
593
+ }
594
+ result += `- ... and ${whoami.repo_allowlist.length - 10} more\n`;
595
+ }
596
+ }
597
+ catch (e) {
598
+ result += `- **Status:** ❌ Authentication failed\n`;
599
+ result += `- **Error:** ${e instanceof Error ? e.message : String(e)}\n`;
600
+ }
601
+ return result;
602
+ }
603
+ // =============================================================================
604
+ // MCP Server
605
+ // =============================================================================
606
+ async function main() {
607
+ const server = new index_js_1.Server({
608
+ name: "metis-mcp-server",
609
+ version: "1.0.0",
610
+ }, {
611
+ capabilities: {
612
+ tools: {},
613
+ },
614
+ });
615
+ // List available tools
616
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
617
+ tools: TOOLS,
618
+ }));
619
+ // Handle tool calls
620
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
621
+ const { name, arguments: args } = request.params;
622
+ try {
623
+ let result;
624
+ switch (name) {
625
+ case "metis_plan_change":
626
+ result = await handlePlanChange(args);
627
+ break;
628
+ case "metis_explain_impact":
629
+ result = await handleExplainImpact(args);
630
+ break;
631
+ case "metis_search":
632
+ result = await handleSearch(args);
633
+ break;
634
+ case "metis_resolve":
635
+ result = await handleResolve(args);
636
+ break;
637
+ case "metis_get_snippet":
638
+ result = await handleSnippet(args);
639
+ break;
640
+ case "metis_doctor":
641
+ result = await handleDoctor();
642
+ break;
643
+ default:
644
+ throw new Error(`Unknown tool: ${name}`);
645
+ }
646
+ return {
647
+ content: [{ type: "text", text: result }],
648
+ };
649
+ }
650
+ catch (error) {
651
+ const message = error instanceof Error ? error.message : String(error);
652
+ return {
653
+ content: [{ type: "text", text: `Error: ${message}` }],
654
+ isError: true,
655
+ };
656
+ }
657
+ });
658
+ // Start server
659
+ const transport = new stdio_js_1.StdioServerTransport();
660
+ await server.connect(transport);
661
+ console.error("Metis MCP Server running on stdio");
662
+ }
663
+ main().catch((error) => {
664
+ console.error("Fatal error:", error);
665
+ process.exit(1);
666
+ });
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@arbotdev/metis-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Metis MCP Server - Code intelligence tools for Cursor/VS Code AI chat",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "metis-mcp": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "start": "node dist/index.js",
16
+ "dev": "ts-node src/index.ts",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": [
20
+ "metis",
21
+ "mcp",
22
+ "cursor",
23
+ "vscode",
24
+ "code-intelligence",
25
+ "blast-radius",
26
+ "impact-analysis"
27
+ ],
28
+ "author": "arbotdev",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/arbotdev/metis_real.git",
33
+ "directory": "mcp-server"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/arbotdev/metis_real/issues"
37
+ },
38
+ "homepage": "https://github.com/arbotdev/metis_real#readme",
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^0.5.0",
41
+ "node-fetch": "^2.7.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^20.0.0",
45
+ "@types/node-fetch": "^2.6.0",
46
+ "typescript": "^5.0.0",
47
+ "ts-node": "^10.9.0"
48
+ },
49
+ "engines": {
50
+ "node": ">=18.0.0"
51
+ }
52
+ }