@supermodeltools/mcp-server 0.4.3 → 0.4.5

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 CHANGED
@@ -126,7 +126,7 @@ claude mcp list
126
126
 
127
127
  ## Tools
128
128
 
129
- ### `analyze_codebase`
129
+ ### `explore_codebase`
130
130
 
131
131
  Analyzes code structure, dependencies, and relationships across a repository. Use this to understand unfamiliar codebases, plan refactorings, assess change impact, or map system architecture.
132
132
 
@@ -148,40 +148,54 @@ Analyzes code structure, dependencies, and relationships across a repository. Us
148
148
 
149
149
  | Argument | Type | Required | Description |
150
150
  |----------|------|----------|-------------|
151
- | `file` | string | Yes | Path to repository ZIP file |
151
+ | `directory` | string | Yes* | Path to repository directory (automatic zipping) |
152
+ | `file` | string | Yes* | Path to pre-zipped archive (deprecated) |
152
153
  | `Idempotency-Key` | string | Yes | Cache key in format `{repo}:{type}:{hash}` |
153
- | `jq_filter` | string | No | jq filter to extract specific data (strongly recommended) |
154
+ | `query` | string | No | Query type (summary, search, list_nodes, etc.) |
155
+ | `jq_filter` | string | No | jq filter for custom data extraction |
156
+
157
+ \* Either `directory` (recommended) or `file` must be provided
154
158
 
155
159
  **Quick start:**
156
160
 
157
161
  ```bash
158
- # 1. Create ZIP from your git repo
159
- git archive -o /tmp/repo.zip HEAD
160
-
161
- # 2. Get commit hash for cache key
162
+ # 1. Get commit hash for cache key
162
163
  git rev-parse --short HEAD
163
164
  # Output: abc123
164
165
 
165
- # 3. Ask Claude to analyze
166
- # "Analyze the codebase at /tmp/repo.zip using key myproject:supermodel:abc123"
166
+ # 2. Ask Claude to analyze (no manual zipping needed!)
167
+ # "Analyze the codebase at /path/to/repo using key myproject:supermodel:abc123"
167
168
  ```
168
169
 
169
170
  **Example prompts:**
170
- - "Analyze the codebase at /tmp/repo.zip to understand its architecture"
171
- - "Before I refactor the authentication module, analyze /tmp/repo.zip to show me what depends on it"
172
- - "What's the structure of the codebase in /tmp/repo.zip?"
171
+ - "Analyze the codebase at . to understand its architecture"
172
+ - "Before I refactor the authentication module, analyze this repo to show me what depends on it"
173
+ - "What's the structure of the codebase in /Users/me/project?"
174
+
175
+ **Automatic features:**
176
+ - Respects `.gitignore` patterns automatically
177
+ - Excludes sensitive files (`.env`, `*.pem`, credentials, etc.)
178
+ - Skips dependencies (`node_modules`, `venv`, `vendor`)
179
+ - Removes build outputs (`dist`, `build`, `out`)
180
+ - Cleans up temporary files automatically
181
+ - Cross-platform compatible
173
182
 
174
183
  ## Troubleshooting
175
184
 
176
185
  Debug logs go to stderr:
177
186
 
178
187
  - `[DEBUG] Server configuration:` - Startup config
188
+ - `[DEBUG] Auto-zipping directory:` - Starting zip creation
189
+ - `[DEBUG] Auto-zip complete:` - Zip stats (file count, size)
179
190
  - `[DEBUG] Making API request` - Request details
180
191
  - `[ERROR] API call failed:` - Error details with HTTP status
181
192
 
182
193
  **Common issues:**
183
194
  - 401: Check `SUPERMODEL_API_KEY` is set
184
- - ZIP too large: Exclude node_modules/dist (use `git archive`)
195
+ - ZIP too large: Directory contains too many files/dependencies. Ensure `.gitignore` is configured properly
196
+ - Permission denied: Check read permissions on the directory
197
+ - Insufficient disk space: Free up space in your system's temp directory
198
+ - Directory does not exist: Verify the path is correct and absolute
185
199
 
186
200
  ## Links
187
201
 
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+ /**
3
+ * LRU Cache for indexed graphs
4
+ * Stores raw API responses + derived indexes for fast query execution
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.graphCache = exports.GraphCache = void 0;
8
+ exports.buildIndexes = buildIndexes;
9
+ exports.normalizePath = normalizePath;
10
+ exports.toNodeDescriptor = toNodeDescriptor;
11
+ /**
12
+ * Build indexes from raw SupermodelIR response
13
+ */
14
+ function buildIndexes(raw, cacheKey) {
15
+ const nodes = raw.graph?.nodes || [];
16
+ const relationships = raw.graph?.relationships || [];
17
+ // Initialize indexes
18
+ const nodeById = new Map();
19
+ const labelIndex = new Map();
20
+ const pathIndex = new Map();
21
+ const dirIndex = new Map();
22
+ const nameIndex = new Map();
23
+ const callAdj = new Map();
24
+ const importAdj = new Map();
25
+ const domainIndex = new Map();
26
+ // Build node indexes
27
+ for (const node of nodes) {
28
+ const id = node.id;
29
+ const props = node.properties || {};
30
+ const labels = node.labels || [];
31
+ // nodeById
32
+ nodeById.set(id, node);
33
+ // labelIndex
34
+ for (const label of labels) {
35
+ if (!labelIndex.has(label)) {
36
+ labelIndex.set(label, []);
37
+ }
38
+ labelIndex.get(label).push(id);
39
+ }
40
+ // nameIndex (lowercase for case-insensitive search)
41
+ const name = props.name;
42
+ if (name) {
43
+ const lowerName = name.toLowerCase();
44
+ if (!nameIndex.has(lowerName)) {
45
+ nameIndex.set(lowerName, []);
46
+ }
47
+ nameIndex.get(lowerName).push(id);
48
+ }
49
+ // pathIndex - track what's defined in each file
50
+ const filePath = props.filePath;
51
+ if (filePath) {
52
+ const normalized = normalizePath(filePath);
53
+ if (!pathIndex.has(normalized)) {
54
+ pathIndex.set(normalized, { fileId: '', classIds: [], functionIds: [], typeIds: [] });
55
+ }
56
+ const entry = pathIndex.get(normalized);
57
+ const primaryLabel = labels[0];
58
+ if (primaryLabel === 'File') {
59
+ entry.fileId = id;
60
+ }
61
+ else if (primaryLabel === 'Class') {
62
+ entry.classIds.push(id);
63
+ }
64
+ else if (primaryLabel === 'Function') {
65
+ entry.functionIds.push(id);
66
+ }
67
+ else if (primaryLabel === 'Type') {
68
+ entry.typeIds.push(id);
69
+ }
70
+ }
71
+ // dirIndex - build directory tree
72
+ if (labels[0] === 'Directory') {
73
+ const dirPath = normalizePath(props.path || props.name || '');
74
+ if (!dirIndex.has(dirPath)) {
75
+ dirIndex.set(dirPath, []);
76
+ }
77
+ }
78
+ // Initialize adjacency lists for functions and files
79
+ if (labels[0] === 'Function') {
80
+ callAdj.set(id, { out: [], in: [] });
81
+ }
82
+ if (labels[0] === 'File' || labels[0] === 'LocalModule' || labels[0] === 'ExternalModule') {
83
+ importAdj.set(id, { out: [], in: [] });
84
+ }
85
+ // domainIndex
86
+ if (labels[0] === 'Domain' || labels[0] === 'Subdomain') {
87
+ domainIndex.set(name || id, { memberIds: [], relationships: [] });
88
+ }
89
+ }
90
+ // Build relationship indexes
91
+ for (const rel of relationships) {
92
+ const { type, startNode, endNode } = rel;
93
+ // Call adjacency
94
+ if (type === 'calls') {
95
+ if (callAdj.has(startNode)) {
96
+ callAdj.get(startNode).out.push(endNode);
97
+ }
98
+ if (callAdj.has(endNode)) {
99
+ callAdj.get(endNode).in.push(startNode);
100
+ }
101
+ }
102
+ // Import adjacency
103
+ if (type === 'IMPORTS') {
104
+ // Some graphs emit IMPORTS edges from non-File nodes (e.g. Function -> Module).
105
+ // Create adjacency lazily for any node that participates.
106
+ let startAdj = importAdj.get(startNode);
107
+ if (!startAdj) {
108
+ startAdj = { out: [], in: [] };
109
+ importAdj.set(startNode, startAdj);
110
+ }
111
+ startAdj.out.push(endNode);
112
+ let endAdj = importAdj.get(endNode);
113
+ if (!endAdj) {
114
+ endAdj = { out: [], in: [] };
115
+ importAdj.set(endNode, endAdj);
116
+ }
117
+ endAdj.in.push(startNode);
118
+ }
119
+ // Directory contains
120
+ if (type === 'CONTAINS_FILE' || type === 'CHILD_DIRECTORY') {
121
+ const startNode_ = nodeById.get(startNode);
122
+ if (startNode_ && startNode_.labels?.[0] === 'Directory') {
123
+ const dirPath = normalizePath(startNode_.properties?.path || startNode_.properties?.name || '');
124
+ if (dirIndex.has(dirPath)) {
125
+ dirIndex.get(dirPath).push(endNode);
126
+ }
127
+ }
128
+ }
129
+ // Domain membership
130
+ if (type === 'belongsTo') {
131
+ const targetNode = nodeById.get(endNode);
132
+ if (targetNode) {
133
+ const domainName = targetNode.properties?.name;
134
+ if (domainIndex.has(domainName)) {
135
+ domainIndex.get(domainName).memberIds.push(startNode);
136
+ }
137
+ }
138
+ }
139
+ // Domain relationships
140
+ const startNodeData = nodeById.get(startNode);
141
+ const endNodeData = nodeById.get(endNode);
142
+ if (startNodeData?.labels?.[0] === 'Domain' && endNodeData?.labels?.[0] === 'Domain') {
143
+ const domainName = startNodeData.properties?.name;
144
+ if (domainIndex.has(domainName)) {
145
+ domainIndex.get(domainName).relationships.push(rel);
146
+ }
147
+ }
148
+ }
149
+ // Compute summary
150
+ const summary = {
151
+ filesProcessed: raw.summary?.filesProcessed || labelIndex.get('File')?.length || 0,
152
+ classes: raw.summary?.classes || labelIndex.get('Class')?.length || 0,
153
+ functions: raw.summary?.functions || labelIndex.get('Function')?.length || 0,
154
+ types: raw.summary?.types || labelIndex.get('Type')?.length || 0,
155
+ domains: raw.summary?.domains || labelIndex.get('Domain')?.length || 0,
156
+ primaryLanguage: raw.summary?.primaryLanguage || 'unknown',
157
+ nodeCount: nodes.length,
158
+ relationshipCount: relationships.length,
159
+ };
160
+ return {
161
+ raw,
162
+ nodeById,
163
+ labelIndex,
164
+ pathIndex,
165
+ dirIndex,
166
+ nameIndex,
167
+ callAdj,
168
+ importAdj,
169
+ domainIndex,
170
+ summary,
171
+ cachedAt: new Date().toISOString(),
172
+ cacheKey,
173
+ };
174
+ }
175
+ /**
176
+ * Normalize file paths for consistent matching
177
+ */
178
+ function normalizePath(path) {
179
+ return path.replace(/\\/g, '/');
180
+ }
181
+ /**
182
+ * Convert full node to lightweight descriptor
183
+ */
184
+ function toNodeDescriptor(node) {
185
+ const props = node.properties || {};
186
+ return {
187
+ id: node.id,
188
+ labels: node.labels || [],
189
+ name: props.name,
190
+ filePath: props.filePath,
191
+ startLine: props.startLine,
192
+ endLine: props.endLine,
193
+ kind: props.kind,
194
+ };
195
+ }
196
+ /**
197
+ * LRU Cache for indexed graphs
198
+ */
199
+ class GraphCache {
200
+ cache = new Map();
201
+ maxGraphs;
202
+ maxNodes;
203
+ currentNodes = 0;
204
+ constructor(options) {
205
+ this.maxGraphs = options?.maxGraphs || 20;
206
+ this.maxNodes = options?.maxNodes || 1000000;
207
+ }
208
+ get(cacheKey) {
209
+ const entry = this.cache.get(cacheKey);
210
+ if (entry) {
211
+ // Update access time (LRU)
212
+ entry.lastAccessed = Date.now();
213
+ return entry.graph;
214
+ }
215
+ return null;
216
+ }
217
+ set(cacheKey, graph) {
218
+ const nodeCount = graph.summary.nodeCount;
219
+ // Evict if needed
220
+ while ((this.cache.size >= this.maxGraphs || this.currentNodes + nodeCount > this.maxNodes) &&
221
+ this.cache.size > 0) {
222
+ this.evictOldest();
223
+ }
224
+ // Store
225
+ this.cache.set(cacheKey, {
226
+ graph,
227
+ nodeCount,
228
+ lastAccessed: Date.now(),
229
+ });
230
+ this.currentNodes += nodeCount;
231
+ }
232
+ has(cacheKey) {
233
+ return this.cache.has(cacheKey);
234
+ }
235
+ evictOldest() {
236
+ let oldestKey = null;
237
+ let oldestTime = Infinity;
238
+ for (const [key, entry] of this.cache) {
239
+ if (entry.lastAccessed < oldestTime) {
240
+ oldestTime = entry.lastAccessed;
241
+ oldestKey = key;
242
+ }
243
+ }
244
+ if (oldestKey) {
245
+ const entry = this.cache.get(oldestKey);
246
+ this.currentNodes -= entry.nodeCount;
247
+ this.cache.delete(oldestKey);
248
+ }
249
+ }
250
+ status() {
251
+ return {
252
+ graphs: this.cache.size,
253
+ nodes: this.currentNodes,
254
+ keys: Array.from(this.cache.keys()),
255
+ };
256
+ }
257
+ }
258
+ exports.GraphCache = GraphCache;
259
+ // Global cache instance
260
+ exports.graphCache = new GraphCache();
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ /**
3
+ * Type definitions for Supermodel IR graph data
4
+ * Mirrors the API response schema
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ /**
3
+ * Cache module exports
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
17
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ __exportStar(require("./graph-types"), exports);
21
+ __exportStar(require("./graph-cache"), exports);
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ /**
3
+ * Discovery queries: get_node, search, list_nodes
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getNode = getNode;
7
+ exports.search = search;
8
+ exports.listNodes = listNodes;
9
+ const graph_cache_1 = require("../cache/graph-cache");
10
+ const types_1 = require("./types");
11
+ const DEFAULT_LIMIT = 200;
12
+ /**
13
+ * get_node - Return full details for a specific node ID
14
+ */
15
+ function getNode(params, graph, source) {
16
+ if (!params.targetId) {
17
+ return (0, types_1.createError)('INVALID_PARAMS', 'targetId is required for get_node query');
18
+ }
19
+ const node = graph.nodeById.get(params.targetId);
20
+ if (!node) {
21
+ return (0, types_1.createError)('NOT_FOUND', `Node with id '${params.targetId}' not found`, {
22
+ detail: 'Use search or list_nodes to discover valid node IDs',
23
+ });
24
+ }
25
+ const result = {
26
+ node: (0, graph_cache_1.toNodeDescriptor)(node),
27
+ };
28
+ // Only include raw if explicitly requested (reduces response size)
29
+ if (params.includeRaw) {
30
+ result.raw = node;
31
+ }
32
+ return (0, types_1.createResponse)('get_node', graph.cacheKey, source, graph.cachedAt, result);
33
+ }
34
+ /**
35
+ * search - Simple substring search across node names
36
+ */
37
+ function search(params, graph, source) {
38
+ if (!params.searchText) {
39
+ return (0, types_1.createError)('INVALID_PARAMS', 'searchText is required for search query');
40
+ }
41
+ const searchLower = params.searchText.toLowerCase();
42
+ const limit = params.limit || DEFAULT_LIMIT;
43
+ const labels = params.labels;
44
+ const filePrefix = params.filePathPrefix ? (0, graph_cache_1.normalizePath)(params.filePathPrefix) : null;
45
+ const results = [];
46
+ let scanned = 0;
47
+ // Scan through nameIndex for matches
48
+ for (const [name, ids] of graph.nameIndex) {
49
+ if (!name.includes(searchLower))
50
+ continue;
51
+ for (const id of ids) {
52
+ if (results.length >= limit)
53
+ break;
54
+ const node = graph.nodeById.get(id);
55
+ if (!node)
56
+ continue;
57
+ // Filter by labels if specified
58
+ if (labels && labels.length > 0) {
59
+ const nodeLabel = node.labels?.[0];
60
+ if (!nodeLabel || !labels.includes(nodeLabel))
61
+ continue;
62
+ }
63
+ // Filter by file path prefix if specified
64
+ if (filePrefix) {
65
+ const filePath = node.properties?.filePath;
66
+ if (!filePath || !(0, graph_cache_1.normalizePath)(filePath).startsWith(filePrefix))
67
+ continue;
68
+ }
69
+ results.push((0, graph_cache_1.toNodeDescriptor)(node));
70
+ scanned++;
71
+ }
72
+ if (results.length >= limit)
73
+ break;
74
+ }
75
+ const hasMore = results.length >= limit;
76
+ return (0, types_1.createResponse)('search', graph.cacheKey, source, graph.cachedAt, { nodes: results }, {
77
+ page: { limit, hasMore },
78
+ warnings: hasMore ? [`Results limited to ${limit}. Use more specific searchText or add filters.`] : undefined,
79
+ });
80
+ }
81
+ /**
82
+ * list_nodes - Filtered list of nodes by labels, namePattern, filePathPrefix
83
+ */
84
+ function listNodes(params, graph, source) {
85
+ const limit = params.limit || DEFAULT_LIMIT;
86
+ const labels = params.labels;
87
+ const filePrefix = params.filePathPrefix ? (0, graph_cache_1.normalizePath)(params.filePathPrefix) : null;
88
+ const searchText = params.searchText?.toLowerCase();
89
+ const namePattern = params.namePattern;
90
+ // Compile regex if namePattern provided
91
+ let regex = null;
92
+ if (namePattern) {
93
+ try {
94
+ regex = new RegExp(namePattern, 'i');
95
+ }
96
+ catch (e) {
97
+ return (0, types_1.createError)('INVALID_PARAMS', `Invalid namePattern regex: ${namePattern}`);
98
+ }
99
+ }
100
+ const results = [];
101
+ // Determine which nodes to scan
102
+ let candidateIds;
103
+ if (labels && labels.length > 0) {
104
+ // Start with nodes matching specified labels (use Set to dedupe)
105
+ candidateIds = new Set();
106
+ for (const label of labels) {
107
+ const ids = graph.labelIndex.get(label) || [];
108
+ for (const id of ids) {
109
+ candidateIds.add(id);
110
+ }
111
+ }
112
+ }
113
+ else {
114
+ // Scan all nodes
115
+ candidateIds = new Set(graph.nodeById.keys());
116
+ }
117
+ for (const id of candidateIds) {
118
+ if (results.length >= limit)
119
+ break;
120
+ const node = graph.nodeById.get(id);
121
+ if (!node)
122
+ continue;
123
+ const props = node.properties || {};
124
+ const name = props.name;
125
+ const filePath = props.filePath;
126
+ // Filter by file path prefix
127
+ if (filePrefix) {
128
+ if (!filePath || !(0, graph_cache_1.normalizePath)(filePath).startsWith(filePrefix))
129
+ continue;
130
+ }
131
+ // Filter by search text (simple substring)
132
+ if (searchText) {
133
+ if (!name || !name.toLowerCase().includes(searchText))
134
+ continue;
135
+ }
136
+ // Filter by name pattern (regex)
137
+ if (regex) {
138
+ if (!name || !regex.test(name))
139
+ continue;
140
+ }
141
+ results.push((0, graph_cache_1.toNodeDescriptor)(node));
142
+ }
143
+ const hasMore = results.length >= limit;
144
+ return (0, types_1.createResponse)('list_nodes', graph.cacheKey, source, graph.cachedAt, { nodes: results }, {
145
+ page: { limit, hasMore },
146
+ warnings: hasMore ? [`Results limited to ${limit}. Add filters to narrow results.`] : undefined,
147
+ });
148
+ }