@grafema/mcp 0.3.29 → 0.3.31

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafema/mcp",
3
- "version": "0.3.29",
3
+ "version": "0.3.31",
4
4
  "description": "MCP server for Grafema code analysis toolkit",
5
5
  "type": "module",
6
6
  "main": "./dist/server.js",
@@ -14,8 +14,8 @@
14
14
  "import": "./dist/server.js"
15
15
  },
16
16
  "./handlers": {
17
- "types": "./dist/handlers.d.ts",
18
- "import": "./dist/handlers.js"
17
+ "types": "./dist/handlers/index.d.ts",
18
+ "import": "./dist/handlers/index.js"
19
19
  }
20
20
  },
21
21
  "files": [
@@ -39,9 +39,9 @@
39
39
  "@modelcontextprotocol/sdk": "^1.25.1",
40
40
  "ajv": "^8.17.1",
41
41
  "yaml": "^2.8.2",
42
- "@grafema/api": "0.3.29",
43
- "@grafema/util": "0.3.29",
44
- "@grafema/types": "0.3.29"
42
+ "@grafema/api": "0.3.31",
43
+ "@grafema/types": "0.3.31",
44
+ "@grafema/util": "0.3.31"
45
45
  },
46
46
  "optionalDependencies": {
47
47
  "@grafema/grafema-darwin-arm64": "0.3.29",
@@ -58,7 +58,7 @@
58
58
  "build": "tsc",
59
59
  "clean": "rm -rf dist",
60
60
  "start": "node dist/server.js",
61
- "test": "node --import tsx --test test/*.test.ts",
62
- "test:watch": "node --import tsx --test --watch test/*.test.ts"
61
+ "test": "node --import tsx --experimental-test-module-mocks --test --test-concurrency=1 test/*.test.ts",
62
+ "test:watch": "node --import tsx --experimental-test-module-mocks --test --test-concurrency=1 --watch test/*.test.ts"
63
63
  }
64
64
  }
@@ -124,14 +124,13 @@ Example: recall(query="federation architecture", depth=2)`,
124
124
  },
125
125
  {
126
126
  name: 'semantic_search',
127
- description: `Embedding-based similarity search across the knowledge graph.
127
+ description: `Substring search across knowledge-graph node names (case-insensitive).
128
128
 
129
- Use this for precise similarity matching finds nodes whose content
130
- is semantically close to the query, even if exact keywords differ.
129
+ NOTE: despite the name, embedding-based semantic ranking is not wired yet
130
+ (RFD-63). This currently matches substrings of node names results are
131
+ plain matches, not similarity-ranked. Use recall for broader retrieval.
131
132
 
132
- More targeted than recall: returns ranked results without graph traversal.
133
-
134
- Example: semantic_search(query="Docker container auth token", top_k=5, domain="devops")`,
133
+ Example: semantic_search(query="auth token", top_k=5, domain="devops")`,
135
134
  inputSchema: {
136
135
  type: 'object',
137
136
  properties: {
@@ -147,10 +146,6 @@ Example: semantic_search(query="Docker container auth token", top_k=5, domain="d
147
146
  type: 'string',
148
147
  description: 'Filter results to a specific domain',
149
148
  },
150
- include_edges: {
151
- type: 'boolean',
152
- description: 'Include edges connected to matched nodes (default: false)',
153
- },
154
149
  },
155
150
  required: ['query'],
156
151
  },
@@ -31,9 +31,19 @@ export async function handleGetCoverage(args: GetCoverageArgs): Promise<ToolResu
31
31
  output += `File breakdown:\n`;
32
32
  output += ` Total files: ${result.total}\n`;
33
33
  output += ` Analyzed: ${result.analyzed.count} (${result.percentages.analyzed}%) - in graph\n`;
34
+ if (result.failed.count > 0) {
35
+ output += ` Failed: ${result.failed.count} (${result.percentages.failed}%) - skipped/failed during analysis\n`;
36
+ }
34
37
  output += ` Unsupported: ${result.unsupported.count} (${result.percentages.unsupported}%) - no indexer available\n`;
35
38
  output += ` Unreachable: ${result.unreachable.count} (${result.percentages.unreachable}%) - not imported from entrypoints\n`;
36
39
 
40
+ if (result.failed.count > 0) {
41
+ output += `\nFailed files by reason:\n`;
42
+ for (const [category, files] of Object.entries(result.failed.byCategory)) {
43
+ output += ` ${category}: ${files.length} files\n`;
44
+ }
45
+ }
46
+
37
47
  if (result.unsupported.count > 0) {
38
48
  output += `\nUnsupported files by extension:\n`;
39
49
  for (const [ext, files] of Object.entries(result.unsupported.byExtension)) {
@@ -40,11 +40,21 @@ Grafema is a static code analyzer that builds a graph of your codebase.
40
40
  ## Syntax
41
41
  violation(X) :- node(X, "TYPE"), attr(X, "name", "value").
42
42
 
43
- ## Available Predicates
44
- - type(Id, Type) - match nodes (alias: node)
45
- - edge(Src, Dst, Type) - match edges
46
- - attr(Id, Name, Value) - match node attributes (name, file, line, etc.)
47
- - \\+ - negation (not)
43
+ ## Node / Edge / Attribute Predicates
44
+ - node(Id, Type) - match nodes by type (alias: type(Id, Type))
45
+ - edge(Src, Dst, Type) - match outgoing edges Src -> Dst of the given type
46
+ - incoming(Dst, Src, Type) - match edges pointing TO Dst (reverse of edge)
47
+ - path(Src, Dst) - transitive reachability Src -> Dst (BFS over edges)
48
+ - attr(Id, Name, Value) - match node attributes (name, file, line, ...; nested paths like "a.b" supported)
49
+ - attr_edge(Src, Dst, EdgeType, AttrName, Value) - match EDGE attributes; Src/Dst/EdgeType/AttrName must be bound, Value may be variable/constant/wildcard
50
+ - parent_function(NodeId, FunctionId) - bind the enclosing FUNCTION of NodeId via CONTAINS; NodeId must be bound; empty at module level
51
+
52
+ ## Negation & String Predicates
53
+ - \\+ - negation (not); also for negative joins on a dst-position variable, e.g. \\+ edge(X, _, "CALLS")
54
+ - neq(X, Y) - inequality; BOTH arguments must be bound
55
+ - starts_with(Value, Prefix) - string prefix match
56
+ - not_starts_with(Value, Prefix) - negative string prefix match
57
+ - string_contains(Value, Substring) - substring match
48
58
 
49
59
  ## Numeric Comparison Predicates
50
60
  - gt(Value, Threshold) - greater than
@@ -55,6 +65,11 @@ violation(X) :- node(X, "TYPE"), attr(X, "name", "value").
55
65
  Values are parsed as floating-point numbers. Non-numeric values produce no matches.
56
66
  Use with attr() to filter by metadata values (e.g., metrics, line numbers).
57
67
 
68
+ ## Limitations
69
+ - No aggregations (count/sum/avg), no GROUP BY, no ORDER BY - aggregate/sort client-side.
70
+ - Predicate ORDER matters: bind a variable (via node/edge/attr) before a comparison /
71
+ string / attr_edge predicate uses it, or the query fails to place ("circular dependency").
72
+
58
73
  ## Examples
59
74
  Find all functions:
60
75
  violation(X) :- node(X, "FUNCTION").
@@ -67,6 +82,12 @@ Find files where parsing took > 500ms:
67
82
 
68
83
  Find functions with more than 100 lines:
69
84
  violation(X, Lines) :- node(X, "FUNCTION"), attr(X, "line", Start), attr(X, "endLine", End), gt(End, Start).
85
+
86
+ Find the enclosing function of every call:
87
+ violation(C, F) :- node(C, "CALL"), parent_function(C, F).
88
+
89
+ Find nodes that have incoming CALLS edges (i.e. are called):
90
+ violation(D) :- incoming(D, _, "CALLS").
70
91
  `,
71
92
  types: `
72
93
  # Node & Edge Types
@@ -348,14 +348,25 @@ export async function handleRecall(args: RecallArgs): Promise<ToolResult> {
348
348
 
349
349
  export interface SemanticSearchArgs {
350
350
  query: string;
351
- limit?: number;
351
+ top_k?: number;
352
352
  domain?: string;
353
353
  }
354
354
 
355
- export async function handleSemanticSearch(args: SemanticSearchArgs): Promise<ToolResult> {
355
+ /**
356
+ * Handle the `semantic_search` MCP tool.
357
+ *
358
+ * @param args - tool input. `top_k` (matching the published schema) caps the
359
+ * number of results; defaults to 10 per the schema's documented default.
360
+ * @param clientOverride - optional RFDB client, injected by tests; production
361
+ * callers omit it and the shared knowledge client is used.
362
+ */
363
+ export async function handleSemanticSearch(
364
+ args: SemanticSearchArgs,
365
+ clientOverride?: RFDBClient,
366
+ ): Promise<ToolResult> {
356
367
  try {
357
- const client = await getKnowledgeClient();
358
- const limit = args.limit ?? 20;
368
+ const client = clientOverride ?? await getKnowledgeClient();
369
+ const limit = args.top_k ?? 10;
359
370
 
360
371
  // TODO: Wire to RFDB embedding engine when enabled.
361
372
  // For now, fall back to substring search via queryNodes.
@@ -378,9 +389,9 @@ export async function handleSemanticSearch(args: SemanticSearchArgs): Promise<To
378
389
  for (let i = 0; i < results.length; i++) {
379
390
  const node = results[i];
380
391
  const meta = parseMeta(node);
381
- // Pseudo-similarity score based on match quality (placeholder until embeddings)
382
- const similarity = (1.0 - (i * 0.05)).toFixed(2);
383
- lines.push(`${i + 1}. [${similarity}] ${node.name} (${node.nodeType})`);
392
+ // NOTE: this is substring matching, not embeddings do not fabricate a
393
+ // similarity score (it would read to an agent as a real metric). Just rank.
394
+ lines.push(`${i + 1}. ${node.name} (${node.nodeType})`);
384
395
  if (meta.domain) lines.push(` Domain: ${meta.domain}`);
385
396
  if (meta.content) lines.push(` ${String(meta.content).slice(0, 200)}`);
386
397
  lines.push('');
@@ -524,22 +524,35 @@ async function enrichNodes(
524
524
 
525
525
  /** Convert a semantic ID to human-readable "name in file.ts" format.
526
526
  * Handles: grafema://host/path/file.ts#TYPE->name[scope], path/file.ts#TYPE->name,
527
- * TYPE-%3Ename (URL-encoded, no file prefix), or raw node IDs. */
528
- function humanReadableId(semanticId: string): string {
527
+ * TYPE-%3Ename (URL-encoded, no file prefix), or raw node IDs.
528
+ *
529
+ * Exported for unit testing.
530
+ * @internal */
531
+ export function humanReadableId(semanticId: string): string {
529
532
  if (!semanticId) return '?';
530
- // Decode URI components first
531
- let id = semanticId;
532
- try { id = decodeURIComponent(id); } catch { /* keep as-is */ }
533
-
534
- // Find the # separator between file path and node descriptor
535
- const hashIdx = id.lastIndexOf('#');
536
- let filePart = '';
537
- let nodePart = id;
533
+
534
+ // Split file path from node descriptor on the FIRST literal '#' of the RAW
535
+ // (still percent-encoded) id, BEFORE decoding. The grafema:// URI form encodes
536
+ // the disambiguation counter ('#N', appended by computeSemanticIdV2 for hash
537
+ // collisions) as '%23N' inside the fragment. Decoding first and then splitting
538
+ // on the LAST '#' would mistake that counter for the path/fragment boundary and
539
+ // corrupt the label. A file path never contains a literal '#'. This mirrors the
540
+ // canonical parseSemanticIdV2 (packages/util/src/core/SemanticId.ts).
541
+ const hashIdx = semanticId.indexOf('#');
542
+ let rawFile = '';
543
+ let rawNode = semanticId;
538
544
  if (hashIdx !== -1) {
539
- filePart = id.slice(0, hashIdx);
540
- nodePart = id.slice(hashIdx + 1);
545
+ rawFile = semanticId.slice(0, hashIdx);
546
+ rawNode = semanticId.slice(hashIdx + 1);
541
547
  }
542
548
 
549
+ const decode = (s: string): string => {
550
+ try { return decodeURIComponent(s); } catch { return s; }
551
+ };
552
+ const filePart = decode(rawFile);
553
+ // Strip the trailing disambiguation counter ('#N') from the decoded descriptor.
554
+ const nodePart = decode(rawNode).replace(/#\d+$/, '');
555
+
543
556
  const fileName = filePart ? (filePart.split('/').pop() || '') : '';
544
557
 
545
558
  // Extract name from TYPE->name or TYPE->name[in:scope,h:hash]
@@ -1,23 +0,0 @@
1
- /**
2
- * MCP Tool Definitions
3
- */
4
- interface SchemaProperty {
5
- type: string;
6
- description?: string;
7
- enum?: string[];
8
- items?: SchemaProperty;
9
- properties?: Record<string, SchemaProperty>;
10
- required?: string[];
11
- }
12
- export interface ToolDefinition {
13
- name: string;
14
- description: string;
15
- inputSchema: {
16
- type: 'object';
17
- properties: Record<string, SchemaProperty>;
18
- required?: string[];
19
- };
20
- }
21
- export declare const TOOLS: ToolDefinition[];
22
- export {};
23
- //# sourceMappingURL=definitions.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../src/definitions.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC5C,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;QAC3C,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,KAAK,EAAE,cAAc,EA+nBjC,CAAC"}