@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 +114 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +666 -0
- package/package.json +52 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|