@supermodeltools/mcp-server 0.4.3 → 0.4.4

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.
@@ -0,0 +1,241 @@
1
+ "use strict";
2
+ /**
3
+ * Query dispatcher - routes queries to appropriate handlers
4
+ * Central entry point for the query engine
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
18
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
19
+ };
20
+ var __importDefault = (this && this.__importDefault) || function (mod) {
21
+ return (mod && mod.__esModule) ? mod : { "default": mod };
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.graphCache = void 0;
25
+ exports.executeQuery = executeQuery;
26
+ exports.getAvailableQueries = getAvailableQueries;
27
+ // @ts-ignore - jq-web doesn't have type declarations
28
+ const jq_web_1 = __importDefault(require("jq-web"));
29
+ const graph_cache_1 = require("../cache/graph-cache");
30
+ const types_1 = require("./types");
31
+ const summary_1 = require("./summary");
32
+ const discovery_1 = require("./discovery");
33
+ const traversal_1 = require("./traversal");
34
+ // jq import moved to top of file
35
+ /**
36
+ * Execute a query against a graph
37
+ * Handles caching, graph loading, and query dispatch
38
+ */
39
+ async function executeQuery(params, apiResponse) {
40
+ // Cache by idempotencyKey only - file path shouldn't affect cache hits
41
+ const cacheKey = params.idempotencyKey;
42
+ // Try to get from cache first
43
+ let graph = graph_cache_1.graphCache.get(cacheKey);
44
+ let source = 'cache';
45
+ // If not cached and we have API response, build indexes and cache
46
+ if (!graph && apiResponse) {
47
+ graph = (0, graph_cache_1.buildIndexes)(apiResponse, cacheKey);
48
+ graph_cache_1.graphCache.set(cacheKey, graph);
49
+ source = 'api';
50
+ }
51
+ // Special case: graph_status works even without a cached graph
52
+ if (params.query === 'graph_status') {
53
+ return (0, summary_1.graphStatus)(params, graph);
54
+ }
55
+ // All other queries require a graph
56
+ if (!graph) {
57
+ return (0, types_1.createError)('CACHE_MISS', `Graph not found in cache for key '${cacheKey}'`, {
58
+ retryable: true,
59
+ detail: 'Re-call with the same file and idempotencyKey to fetch from API',
60
+ });
61
+ }
62
+ // Dispatch to appropriate handler
63
+ return dispatchQuery(params, graph, source);
64
+ }
65
+ /**
66
+ * Dispatch query to the appropriate handler function
67
+ */
68
+ function dispatchQuery(params, graph, source) {
69
+ switch (params.query) {
70
+ // Summary queries
71
+ case 'summary':
72
+ return (0, summary_1.summary)(params, graph, source);
73
+ // Discovery queries
74
+ case 'get_node':
75
+ return (0, discovery_1.getNode)(params, graph, source);
76
+ case 'search':
77
+ return (0, discovery_1.search)(params, graph, source);
78
+ case 'list_nodes':
79
+ return (0, discovery_1.listNodes)(params, graph, source);
80
+ // Traversal queries (v1 MVP)
81
+ case 'function_calls_in':
82
+ return (0, traversal_1.functionCallsIn)(params, graph, source);
83
+ case 'function_calls_out':
84
+ return (0, traversal_1.functionCallsOut)(params, graph, source);
85
+ case 'definitions_in_file':
86
+ return (0, traversal_1.definitionsInFile)(params, graph, source);
87
+ // Traversal queries (v1.1)
88
+ case 'file_imports':
89
+ return (0, traversal_1.fileImports)(params, graph, source);
90
+ case 'domain_map':
91
+ return (0, traversal_1.domainMap)(params, graph, source);
92
+ case 'domain_membership':
93
+ return (0, traversal_1.domainMembership)(params, graph, source);
94
+ case 'neighborhood':
95
+ return (0, traversal_1.neighborhood)(params, graph, source);
96
+ // Escape hatch
97
+ case 'jq':
98
+ return executeJqQuery(params, graph, source);
99
+ // Not implemented yet
100
+ case 'uses_in_file':
101
+ case 'list_files_in_dir':
102
+ return (0, types_1.createError)('INVALID_QUERY', `Query type '${params.query}' is not yet implemented`);
103
+ default:
104
+ return (0, types_1.createError)('INVALID_QUERY', `Unknown query type: ${params.query}`);
105
+ }
106
+ }
107
+ /**
108
+ * Execute a raw jq query against the graph
109
+ * Escape hatch for queries not covered by canned types
110
+ */
111
+ async function executeJqQuery(params, graph, source) {
112
+ if (!params.jq_filter) {
113
+ return (0, types_1.createError)('INVALID_PARAMS', 'jq_filter is required for jq query');
114
+ }
115
+ try {
116
+ // Execute jq using jq-web (in-memory, no temp file needed)
117
+ const jqInstance = await jq_web_1.default;
118
+ const result = jqInstance.json(graph.raw, params.jq_filter);
119
+ return {
120
+ query: 'jq',
121
+ cacheKey: graph.cacheKey,
122
+ source,
123
+ cachedAt: graph.cachedAt,
124
+ result,
125
+ warnings: [
126
+ 'jq is an escape hatch. Consider using a canned query type for better performance.',
127
+ ],
128
+ };
129
+ }
130
+ catch (error) {
131
+ const message = error instanceof Error ? error.message : String(error);
132
+ return (0, types_1.createError)('BAD_JQ', `jq filter error: ${message}`, {
133
+ detail: params.jq_filter,
134
+ });
135
+ }
136
+ }
137
+ /**
138
+ * Get list of available query types with descriptions
139
+ * Useful for agent discovery
140
+ */
141
+ function getAvailableQueries() {
142
+ return [
143
+ // v1 MVP
144
+ {
145
+ query: 'graph_status',
146
+ description: 'Check if graph is cached and get summary stats',
147
+ requiredParams: ['file', 'idempotencyKey'],
148
+ optionalParams: [],
149
+ phase: 'v1',
150
+ },
151
+ {
152
+ query: 'summary',
153
+ description: 'Get high-level stats about the codebase',
154
+ requiredParams: ['file', 'idempotencyKey'],
155
+ optionalParams: [],
156
+ phase: 'v1',
157
+ },
158
+ {
159
+ query: 'get_node',
160
+ description: 'Get full details for a specific node by ID',
161
+ requiredParams: ['file', 'idempotencyKey', 'targetId'],
162
+ optionalParams: [],
163
+ phase: 'v1',
164
+ },
165
+ {
166
+ query: 'search',
167
+ description: 'Search nodes by name substring',
168
+ requiredParams: ['file', 'idempotencyKey', 'searchText'],
169
+ optionalParams: ['labels', 'filePathPrefix', 'limit'],
170
+ phase: 'v1',
171
+ },
172
+ {
173
+ query: 'list_nodes',
174
+ description: 'List nodes with filters (labels, namePattern, filePathPrefix)',
175
+ requiredParams: ['file', 'idempotencyKey'],
176
+ optionalParams: ['labels', 'namePattern', 'filePathPrefix', 'searchText', 'limit'],
177
+ phase: 'v1',
178
+ },
179
+ {
180
+ query: 'function_calls_in',
181
+ description: 'Find all callers of a function',
182
+ requiredParams: ['file', 'idempotencyKey', 'targetId'],
183
+ optionalParams: ['limit'],
184
+ phase: 'v1',
185
+ },
186
+ {
187
+ query: 'function_calls_out',
188
+ description: 'Find all functions called by a function',
189
+ requiredParams: ['file', 'idempotencyKey', 'targetId'],
190
+ optionalParams: ['limit'],
191
+ phase: 'v1',
192
+ },
193
+ {
194
+ query: 'definitions_in_file',
195
+ description: 'Get all classes, functions, types defined in a file',
196
+ requiredParams: ['file', 'idempotencyKey'],
197
+ optionalParams: ['targetId', 'filePathPrefix', 'limit'],
198
+ phase: 'v1',
199
+ },
200
+ {
201
+ query: 'jq',
202
+ description: 'Execute raw jq filter (escape hatch)',
203
+ requiredParams: ['file', 'idempotencyKey', 'jq_filter'],
204
+ optionalParams: [],
205
+ phase: 'v1',
206
+ },
207
+ // v1.1
208
+ {
209
+ query: 'file_imports',
210
+ description: 'Get imports for a file (outgoing and incoming)',
211
+ requiredParams: ['file', 'idempotencyKey', 'targetId'],
212
+ optionalParams: ['limit'],
213
+ phase: 'v1.1',
214
+ },
215
+ {
216
+ query: 'domain_map',
217
+ description: 'List all domains with relationships',
218
+ requiredParams: ['file', 'idempotencyKey'],
219
+ optionalParams: [],
220
+ phase: 'v1.1',
221
+ },
222
+ {
223
+ query: 'domain_membership',
224
+ description: 'Get members of a domain',
225
+ requiredParams: ['file', 'idempotencyKey'],
226
+ optionalParams: ['targetId', 'searchText', 'limit'],
227
+ phase: 'v1.1',
228
+ },
229
+ {
230
+ query: 'neighborhood',
231
+ description: 'Get ego graph around a node',
232
+ requiredParams: ['file', 'idempotencyKey', 'targetId'],
233
+ optionalParams: ['depth', 'relationshipTypes', 'limit'],
234
+ phase: 'v1.1',
235
+ },
236
+ ];
237
+ }
238
+ // Re-export types for convenience
239
+ __exportStar(require("./types"), exports);
240
+ var graph_cache_2 = require("../cache/graph-cache");
241
+ Object.defineProperty(exports, "graphCache", { enumerable: true, get: function () { return graph_cache_2.graphCache; } });
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ /**
3
+ * Summary queries: graph_status, summary
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.graphStatus = graphStatus;
7
+ exports.summary = summary;
8
+ const graph_cache_1 = require("../cache/graph-cache");
9
+ const types_1 = require("./types");
10
+ /**
11
+ * graph_status - Return cache status and summary if available
12
+ */
13
+ function graphStatus(params, graph) {
14
+ const cacheKey = params.idempotencyKey;
15
+ const cacheStats = graph_cache_1.graphCache.status();
16
+ if (!graph) {
17
+ return (0, types_1.createResponse)('graph_status', cacheKey, 'cache', new Date().toISOString(), {
18
+ cached: false,
19
+ cacheKey,
20
+ cacheStats,
21
+ });
22
+ }
23
+ return (0, types_1.createResponse)('graph_status', cacheKey, 'cache', graph.cachedAt, {
24
+ cached: true,
25
+ cacheKey: graph.cacheKey,
26
+ cachedAt: graph.cachedAt,
27
+ summary: graph.summary,
28
+ cacheStats,
29
+ });
30
+ }
31
+ /**
32
+ * summary - Return high-level stats about the graph
33
+ */
34
+ function summary(params, graph, source) {
35
+ return (0, types_1.createResponse)('summary', graph.cacheKey, source, graph.cachedAt, graph.summary);
36
+ }
@@ -0,0 +1,392 @@
1
+ "use strict";
2
+ /**
3
+ * Traversal queries: function_calls_in, function_calls_out, definitions_in_file
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.functionCallsIn = functionCallsIn;
7
+ exports.functionCallsOut = functionCallsOut;
8
+ exports.definitionsInFile = definitionsInFile;
9
+ exports.fileImports = fileImports;
10
+ exports.domainMap = domainMap;
11
+ exports.domainMembership = domainMembership;
12
+ exports.neighborhood = neighborhood;
13
+ const graph_cache_1 = require("../cache/graph-cache");
14
+ const types_1 = require("./types");
15
+ const DEFAULT_LIMIT = 200;
16
+ /**
17
+ * function_calls_in - Find all callers of a function
18
+ */
19
+ function functionCallsIn(params, graph, source) {
20
+ if (!params.targetId) {
21
+ return (0, types_1.createError)('INVALID_PARAMS', 'targetId is required for function_calls_in query');
22
+ }
23
+ // Verify target exists and is a function
24
+ const targetNode = graph.nodeById.get(params.targetId);
25
+ if (!targetNode) {
26
+ return (0, types_1.createError)('NOT_FOUND', `Node with id '${params.targetId}' not found`, {
27
+ detail: 'Use search or list_nodes with labels=["Function"] to discover function IDs',
28
+ });
29
+ }
30
+ if (targetNode.labels?.[0] !== 'Function') {
31
+ return (0, types_1.createError)('INVALID_PARAMS', `Node '${params.targetId}' is not a Function (got ${targetNode.labels?.[0]})`);
32
+ }
33
+ const adj = graph.callAdj.get(params.targetId);
34
+ if (!adj) {
35
+ return (0, types_1.createResponse)('function_calls_in', graph.cacheKey, source, graph.cachedAt, { nodes: [], edges: [] });
36
+ }
37
+ const limit = params.limit || DEFAULT_LIMIT;
38
+ const callerIds = adj.in.slice(0, limit);
39
+ const nodes = [];
40
+ const edges = [];
41
+ for (const callerId of callerIds) {
42
+ const callerNode = graph.nodeById.get(callerId);
43
+ if (callerNode) {
44
+ nodes.push((0, graph_cache_1.toNodeDescriptor)(callerNode));
45
+ edges.push({
46
+ type: 'calls',
47
+ from: callerId,
48
+ to: params.targetId,
49
+ });
50
+ }
51
+ }
52
+ const hasMore = adj.in.length > limit;
53
+ return (0, types_1.createResponse)('function_calls_in', graph.cacheKey, source, graph.cachedAt, { nodes, edges }, {
54
+ page: { limit, hasMore },
55
+ warnings: hasMore ? [`${adj.in.length - limit} more callers not shown. Increase limit to see more.`] : undefined,
56
+ });
57
+ }
58
+ /**
59
+ * function_calls_out - Find all functions called by a function
60
+ */
61
+ function functionCallsOut(params, graph, source) {
62
+ if (!params.targetId) {
63
+ return (0, types_1.createError)('INVALID_PARAMS', 'targetId is required for function_calls_out query');
64
+ }
65
+ // Verify target exists and is a function
66
+ const targetNode = graph.nodeById.get(params.targetId);
67
+ if (!targetNode) {
68
+ return (0, types_1.createError)('NOT_FOUND', `Node with id '${params.targetId}' not found`, {
69
+ detail: 'Use search or list_nodes with labels=["Function"] to discover function IDs',
70
+ });
71
+ }
72
+ if (targetNode.labels?.[0] !== 'Function') {
73
+ return (0, types_1.createError)('INVALID_PARAMS', `Node '${params.targetId}' is not a Function (got ${targetNode.labels?.[0]})`);
74
+ }
75
+ const adj = graph.callAdj.get(params.targetId);
76
+ if (!adj) {
77
+ return (0, types_1.createResponse)('function_calls_out', graph.cacheKey, source, graph.cachedAt, { nodes: [], edges: [] });
78
+ }
79
+ const limit = params.limit || DEFAULT_LIMIT;
80
+ const calleeIds = adj.out.slice(0, limit);
81
+ const nodes = [];
82
+ const edges = [];
83
+ for (const calleeId of calleeIds) {
84
+ const calleeNode = graph.nodeById.get(calleeId);
85
+ if (calleeNode) {
86
+ nodes.push((0, graph_cache_1.toNodeDescriptor)(calleeNode));
87
+ edges.push({
88
+ type: 'calls',
89
+ from: params.targetId,
90
+ to: calleeId,
91
+ });
92
+ }
93
+ }
94
+ const hasMore = adj.out.length > limit;
95
+ return (0, types_1.createResponse)('function_calls_out', graph.cacheKey, source, graph.cachedAt, { nodes, edges }, {
96
+ page: { limit, hasMore },
97
+ warnings: hasMore ? [`${adj.out.length - limit} more callees not shown. Increase limit to see more.`] : undefined,
98
+ });
99
+ }
100
+ /**
101
+ * definitions_in_file - Get all classes, functions, types defined in a file
102
+ */
103
+ function definitionsInFile(params, graph, source) {
104
+ // Accept either targetId (file node ID) or filePathPrefix (file path)
105
+ let filePath = null;
106
+ if (params.targetId) {
107
+ const targetNode = graph.nodeById.get(params.targetId);
108
+ if (!targetNode) {
109
+ return (0, types_1.createError)('NOT_FOUND', `Node with id '${params.targetId}' not found`);
110
+ }
111
+ if (targetNode.labels?.[0] !== 'File') {
112
+ return (0, types_1.createError)('INVALID_PARAMS', `Node '${params.targetId}' is not a File (got ${targetNode.labels?.[0]})`);
113
+ }
114
+ filePath = targetNode.properties?.filePath || targetNode.properties?.path;
115
+ }
116
+ else if (params.filePathPrefix) {
117
+ filePath = params.filePathPrefix;
118
+ }
119
+ else {
120
+ return (0, types_1.createError)('INVALID_PARAMS', 'Either targetId (file node ID) or filePathPrefix is required');
121
+ }
122
+ if (!filePath) {
123
+ return (0, types_1.createError)('INVALID_PARAMS', 'Could not determine file path');
124
+ }
125
+ const normalizedPath = (0, graph_cache_1.normalizePath)(filePath);
126
+ const pathEntry = graph.pathIndex.get(normalizedPath);
127
+ let resolvedPath = normalizedPath;
128
+ if (!pathEntry) {
129
+ // Try to find by partial match
130
+ for (const [path] of graph.pathIndex) {
131
+ if (path.endsWith(normalizedPath) || normalizedPath.endsWith(path)) {
132
+ resolvedPath = path;
133
+ break;
134
+ }
135
+ }
136
+ if (resolvedPath === normalizedPath) {
137
+ return (0, types_1.createError)('NOT_FOUND', `No definitions found for file '${filePath}'`, {
138
+ detail: 'File may not exist in the analyzed codebase or has no class/function/type definitions',
139
+ });
140
+ }
141
+ }
142
+ const entry = pathEntry || graph.pathIndex.get(resolvedPath);
143
+ if (!entry) {
144
+ return (0, types_1.createResponse)('definitions_in_file', graph.cacheKey, source, graph.cachedAt, { file: null, definitions: { nodes: [] } });
145
+ }
146
+ const limit = params.limit || DEFAULT_LIMIT;
147
+ const nodes = [];
148
+ // Get file node
149
+ let fileNode = null;
150
+ if (entry.fileId) {
151
+ const file = graph.nodeById.get(entry.fileId);
152
+ if (file) {
153
+ fileNode = (0, graph_cache_1.toNodeDescriptor)(file);
154
+ }
155
+ }
156
+ // Collect all definitions
157
+ const allIds = [...entry.classIds, ...entry.functionIds, ...entry.typeIds];
158
+ for (const id of allIds.slice(0, limit)) {
159
+ const node = graph.nodeById.get(id);
160
+ if (node) {
161
+ nodes.push((0, graph_cache_1.toNodeDescriptor)(node));
162
+ }
163
+ }
164
+ const hasMore = allIds.length > limit;
165
+ return (0, types_1.createResponse)('definitions_in_file', graph.cacheKey, source, graph.cachedAt, { file: fileNode, definitions: { nodes } }, {
166
+ page: { limit, hasMore },
167
+ warnings: hasMore ? [`${allIds.length - limit} more definitions not shown.`] : undefined,
168
+ });
169
+ }
170
+ /**
171
+ * file_imports - Get imports for a file (both outgoing and incoming)
172
+ * v1.1 query
173
+ */
174
+ function fileImports(params, graph, source) {
175
+ if (!params.targetId) {
176
+ return (0, types_1.createError)('INVALID_PARAMS', 'targetId is required for file_imports query');
177
+ }
178
+ const targetNode = graph.nodeById.get(params.targetId);
179
+ if (!targetNode) {
180
+ return (0, types_1.createError)('NOT_FOUND', `Node with id '${params.targetId}' not found`);
181
+ }
182
+ const label = targetNode.labels?.[0];
183
+ if (label !== 'File' && label !== 'LocalModule' && label !== 'ExternalModule') {
184
+ return (0, types_1.createError)('INVALID_PARAMS', `Node '${params.targetId}' is not a File/Module (got ${label})`);
185
+ }
186
+ const limit = params.limit || DEFAULT_LIMIT;
187
+ const outIds = new Set();
188
+ const addOutgoingImports = (nodeId) => {
189
+ const adj = graph.importAdj.get(nodeId);
190
+ if (!adj)
191
+ return;
192
+ for (const outId of adj.out) {
193
+ outIds.add(outId);
194
+ }
195
+ };
196
+ // Always include imports directly attached to the target node.
197
+ addOutgoingImports(params.targetId);
198
+ // If the target is a File node, also aggregate IMPORTS edges attached to
199
+ // definitions within the file. This handles graphs where IMPORTS edges
200
+ // are modeled as Function -> Module rather than File -> Module.
201
+ if (label === 'File') {
202
+ const filePathRaw = targetNode.properties?.filePath ||
203
+ targetNode.properties?.path ||
204
+ '';
205
+ const filePath = (0, graph_cache_1.normalizePath)(filePathRaw);
206
+ const entry = graph.pathIndex.get(filePath);
207
+ if (entry) {
208
+ for (const id of entry.functionIds)
209
+ addOutgoingImports(id);
210
+ for (const id of entry.classIds)
211
+ addOutgoingImports(id);
212
+ for (const id of entry.typeIds)
213
+ addOutgoingImports(id);
214
+ }
215
+ }
216
+ const imports = [];
217
+ for (const id of outIds) {
218
+ if (imports.length >= limit)
219
+ break;
220
+ const node = graph.nodeById.get(id);
221
+ if (node) {
222
+ imports.push((0, graph_cache_1.toNodeDescriptor)(node));
223
+ }
224
+ }
225
+ const hasMore = outIds.size > limit;
226
+ return (0, types_1.createResponse)('file_imports', graph.cacheKey, source, graph.cachedAt, { imports: { nodes: imports } }, {
227
+ page: { limit, hasMore },
228
+ warnings: label === 'File' && outIds.size === 0
229
+ ? [
230
+ 'No IMPORTS edges found directly on the File node or on its contained definitions. This may reflect graph modeling choices for this repository.',
231
+ ]
232
+ : undefined,
233
+ });
234
+ }
235
+ /**
236
+ * domain_map - List all domains with their relationships
237
+ * v1.1 query
238
+ */
239
+ function domainMap(params, graph, source) {
240
+ const domains = [];
241
+ const relationships = [];
242
+ // Get domain nodes
243
+ const domainIds = graph.labelIndex.get('Domain') || [];
244
+ for (const id of domainIds) {
245
+ const node = graph.nodeById.get(id);
246
+ if (node) {
247
+ const name = node.properties?.name;
248
+ const description = node.properties?.description;
249
+ const domainEntry = graph.domainIndex.get(name);
250
+ domains.push({
251
+ name,
252
+ description,
253
+ memberCount: domainEntry?.memberIds.length || 0,
254
+ });
255
+ // Collect relationships
256
+ if (domainEntry) {
257
+ for (const rel of domainEntry.relationships) {
258
+ const targetNode = graph.nodeById.get(rel.endNode);
259
+ if (targetNode) {
260
+ relationships.push({
261
+ from: name,
262
+ to: targetNode.properties?.name,
263
+ type: rel.type,
264
+ });
265
+ }
266
+ }
267
+ }
268
+ }
269
+ }
270
+ return (0, types_1.createResponse)('domain_map', graph.cacheKey, source, graph.cachedAt, { domains, relationships });
271
+ }
272
+ /**
273
+ * domain_membership - Get members of a domain
274
+ * v1.1 query
275
+ */
276
+ function domainMembership(params, graph, source) {
277
+ if (!params.searchText && !params.targetId) {
278
+ return (0, types_1.createError)('INVALID_PARAMS', 'searchText (domain name) or targetId (domain node ID) is required');
279
+ }
280
+ let domainName;
281
+ if (params.targetId) {
282
+ const node = graph.nodeById.get(params.targetId);
283
+ if (!node) {
284
+ return (0, types_1.createError)('NOT_FOUND', `Node with id '${params.targetId}' not found`);
285
+ }
286
+ domainName = node.properties?.name;
287
+ }
288
+ else {
289
+ domainName = params.searchText;
290
+ }
291
+ const domainEntry = graph.domainIndex.get(domainName);
292
+ if (!domainEntry) {
293
+ return (0, types_1.createError)('NOT_FOUND', `Domain '${domainName}' not found`, {
294
+ detail: 'Use domain_map to list available domains',
295
+ });
296
+ }
297
+ const limit = params.limit || DEFAULT_LIMIT;
298
+ const members = [];
299
+ for (const id of domainEntry.memberIds.slice(0, limit)) {
300
+ const node = graph.nodeById.get(id);
301
+ if (node)
302
+ members.push((0, graph_cache_1.toNodeDescriptor)(node));
303
+ }
304
+ const hasMore = domainEntry.memberIds.length > limit;
305
+ return (0, types_1.createResponse)('domain_membership', graph.cacheKey, source, graph.cachedAt, { domain: domainName, members: { nodes: members } }, {
306
+ page: { limit, hasMore },
307
+ });
308
+ }
309
+ /**
310
+ * neighborhood - Get ego graph around a node
311
+ * v1.1 query
312
+ */
313
+ function neighborhood(params, graph, source) {
314
+ if (!params.targetId) {
315
+ return (0, types_1.createError)('INVALID_PARAMS', 'targetId is required for neighborhood query');
316
+ }
317
+ const targetNode = graph.nodeById.get(params.targetId);
318
+ if (!targetNode) {
319
+ return (0, types_1.createError)('NOT_FOUND', `Node with id '${params.targetId}' not found`);
320
+ }
321
+ const depth = Math.min(params.depth || 1, 3); // Cap at 3 to prevent explosion
322
+ const limit = params.limit || 100;
323
+ // Only include relationship types we actually implement
324
+ const relationshipTypes = params.relationshipTypes || ['calls', 'IMPORTS'];
325
+ const visited = new Set();
326
+ const nodes = [];
327
+ const edges = [];
328
+ // BFS
329
+ let frontier = [params.targetId];
330
+ visited.add(params.targetId);
331
+ nodes.push((0, graph_cache_1.toNodeDescriptor)(targetNode));
332
+ for (let d = 0; d < depth && nodes.length < limit; d++) {
333
+ const nextFrontier = [];
334
+ for (const nodeId of frontier) {
335
+ if (nodes.length >= limit)
336
+ break;
337
+ // Get adjacent nodes via call graph
338
+ if (relationshipTypes.includes('calls')) {
339
+ const callAdj = graph.callAdj.get(nodeId);
340
+ if (callAdj) {
341
+ for (const adjId of [...callAdj.out, ...callAdj.in]) {
342
+ if (!visited.has(adjId) && nodes.length < limit) {
343
+ visited.add(adjId);
344
+ const adjNode = graph.nodeById.get(adjId);
345
+ if (adjNode) {
346
+ nodes.push((0, graph_cache_1.toNodeDescriptor)(adjNode));
347
+ nextFrontier.push(adjId);
348
+ // Add edge
349
+ if (callAdj.out.includes(adjId)) {
350
+ edges.push({ type: 'calls', from: nodeId, to: adjId });
351
+ }
352
+ else {
353
+ edges.push({ type: 'calls', from: adjId, to: nodeId });
354
+ }
355
+ }
356
+ }
357
+ }
358
+ }
359
+ }
360
+ // Get adjacent nodes via import graph
361
+ if (relationshipTypes.includes('IMPORTS')) {
362
+ const importAdj = graph.importAdj.get(nodeId);
363
+ if (importAdj) {
364
+ for (const adjId of [...importAdj.out, ...importAdj.in]) {
365
+ if (!visited.has(adjId) && nodes.length < limit) {
366
+ visited.add(adjId);
367
+ const adjNode = graph.nodeById.get(adjId);
368
+ if (adjNode) {
369
+ nodes.push((0, graph_cache_1.toNodeDescriptor)(adjNode));
370
+ nextFrontier.push(adjId);
371
+ if (importAdj.out.includes(adjId)) {
372
+ edges.push({ type: 'IMPORTS', from: nodeId, to: adjId });
373
+ }
374
+ else {
375
+ edges.push({ type: 'IMPORTS', from: adjId, to: nodeId });
376
+ }
377
+ }
378
+ }
379
+ }
380
+ }
381
+ }
382
+ }
383
+ frontier = nextFrontier;
384
+ if (frontier.length === 0)
385
+ break;
386
+ }
387
+ const hasMore = nodes.length >= limit;
388
+ return (0, types_1.createResponse)('neighborhood', graph.cacheKey, source, graph.cachedAt, { nodes, edges }, {
389
+ page: { limit, hasMore },
390
+ warnings: hasMore ? [`Neighborhood truncated at ${limit} nodes. Decrease depth or increase limit.`] : undefined,
391
+ });
392
+ }