@henrychong-ai/mcp-neo4j-knowledge-graph 2.4.1 → 2.6.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.
@@ -0,0 +1,117 @@
1
+ import axios from 'axios';
2
+ import { logger } from '../utils/logger.js';
3
+ function intEnv(name, fallback) {
4
+ const raw = process.env[name];
5
+ if (!raw)
6
+ return fallback;
7
+ const parsed = Number.parseInt(raw, 10);
8
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
9
+ }
10
+ /**
11
+ * Thin client for a cross-encoder reranker (default: Cloudflare Workers AI
12
+ * `@cf/baai/bge-reranker-base`). OpenAI-style/native HTTP; reuses `axios`.
13
+ *
14
+ * The service itself MAY throw (transport/parse errors) — callers are expected
15
+ * to fail open (return the pre-rerank order). It never enables itself without a
16
+ * resolved endpoint + API key.
17
+ */
18
+ export class RerankerService {
19
+ cfg;
20
+ constructor(cfg) {
21
+ this.cfg = cfg;
22
+ }
23
+ get enabled() {
24
+ return this.cfg.enabled;
25
+ }
26
+ get topN() {
27
+ return this.cfg.topN;
28
+ }
29
+ get topK() {
30
+ return this.cfg.topK;
31
+ }
32
+ get model() {
33
+ return this.cfg.model;
34
+ }
35
+ /**
36
+ * Build a reranker from environment variables. Returns a DISABLED instance
37
+ * unless `RERANK_ENABLED=true` AND an endpoint + API key resolve.
38
+ *
39
+ * Env: RERANK_ENABLED, RERANK_MODEL (default @cf/baai/bge-reranker-base),
40
+ * RERANK_ENDPOINT (or derived from RERANK_ACCOUNT_ID/CF_ACCOUNT_ID + model),
41
+ * RERANK_API_KEY (falls back to EMBEDDING_API_KEY), RERANK_TOP_N (20),
42
+ * RERANK_TOP_K (10), RERANK_MAX_PASSAGE_CHARS (2000), RERANK_TIMEOUT_MS (5000).
43
+ */
44
+ static fromEnv() {
45
+ const requested = process.env.RERANK_ENABLED === 'true';
46
+ const model = process.env.RERANK_MODEL || '@cf/baai/bge-reranker-base';
47
+ const apiKey = process.env.RERANK_API_KEY || process.env.EMBEDDING_API_KEY || '';
48
+ let endpoint = process.env.RERANK_ENDPOINT || '';
49
+ const accountId = process.env.RERANK_ACCOUNT_ID || process.env.CF_ACCOUNT_ID;
50
+ if (!endpoint && accountId) {
51
+ endpoint = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run/${model}`;
52
+ }
53
+ const enabled = requested && endpoint.length > 0 && apiKey.length > 0;
54
+ if (requested && !enabled) {
55
+ logger.warn('RerankerService: RERANK_ENABLED=true but endpoint or API key could not be resolved — ' +
56
+ 'reranking DISABLED (fail-open). Set RERANK_ENDPOINT (or RERANK_ACCOUNT_ID) and RERANK_API_KEY.');
57
+ }
58
+ else if (enabled) {
59
+ logger.info(`RerankerService: enabled (model ${model})`);
60
+ }
61
+ return new RerankerService({
62
+ enabled,
63
+ endpoint,
64
+ model,
65
+ apiKey,
66
+ topN: intEnv('RERANK_TOP_N', 20),
67
+ topK: intEnv('RERANK_TOP_K', 10),
68
+ maxPassageChars: intEnv('RERANK_MAX_PASSAGE_CHARS', 2000),
69
+ timeoutMs: intEnv('RERANK_TIMEOUT_MS', 5000),
70
+ });
71
+ }
72
+ /**
73
+ * Rerank `passages` against `query`. Returns candidate indices reordered
74
+ * descending by relevance, length <= topK. THROWS on transport/parse error —
75
+ * the caller fail-opens.
76
+ *
77
+ * @param query - Search query
78
+ * @param passages - Candidate passages (index-aligned with the caller's results)
79
+ * @returns Indices into `passages`, best-first
80
+ */
81
+ async rerank(query, passages) {
82
+ const contexts = passages
83
+ .slice(0, this.cfg.topN)
84
+ .map(text => ({ text: (text || '').slice(0, this.cfg.maxPassageChars) }));
85
+ const response = await axios.post(this.cfg.endpoint, { query: query.slice(0, this.cfg.maxPassageChars), contexts }, {
86
+ headers: {
87
+ Authorization: `Bearer ${this.cfg.apiKey}`,
88
+ 'Content-Type': 'application/json',
89
+ },
90
+ timeout: this.cfg.timeoutMs,
91
+ });
92
+ const ranked = response.data?.result?.response;
93
+ if (!Array.isArray(ranked)) {
94
+ throw new Error('Reranker returned a malformed response (missing result.response array)');
95
+ }
96
+ // Defensive: keep only integer, in-range, UNIQUE ids (CF returns request-array indices).
97
+ // Fractional / duplicate / out-of-range ids are dropped so a malformed-but-arrayed response
98
+ // can't corrupt ordering or duplicate results — the caller still fully fail-opens on a throw.
99
+ const seen = new Set();
100
+ const indices = [];
101
+ for (const row of ranked) {
102
+ const id = row?.id;
103
+ if (typeof id === 'number' &&
104
+ Number.isInteger(id) &&
105
+ id >= 0 &&
106
+ id < contexts.length &&
107
+ !seen.has(id)) {
108
+ seen.add(id);
109
+ indices.push(id);
110
+ if (indices.length >= this.cfg.topK)
111
+ break;
112
+ }
113
+ }
114
+ return indices;
115
+ }
116
+ }
117
+ //# sourceMappingURL=RerankerService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RerankerService.js","sourceRoot":"","sources":["../../src/retrieval/RerankerService.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,14 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ /**
3
+ * Resolve the package version from package.json at runtime — the single source
4
+ * of truth (no hardcoded literal to drift). Runtime read (not a static JSON
5
+ * import) because tsconfig rootDir=src excludes package.json from the build;
6
+ * `../../package.json` resolves identically from src/server/ (dev) and
7
+ * dist/server/ (published package).
8
+ *
9
+ * @returns The package.json version string
10
+ */
11
+ export declare function getPackageVersion(): string;
2
12
  /**
3
13
  * Sets up and configures the MCP server with the appropriate request handlers.
4
14
  *
@@ -1,7 +1,22 @@
1
+ import { createRequire } from 'node:module';
1
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
3
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
3
4
  import { handleCallToolRequest } from './handlers/callToolHandler.js';
4
5
  import { handleListToolsRequest } from './handlers/listToolsHandler.js';
6
+ const require = createRequire(import.meta.url);
7
+ /**
8
+ * Resolve the package version from package.json at runtime — the single source
9
+ * of truth (no hardcoded literal to drift). Runtime read (not a static JSON
10
+ * import) because tsconfig rootDir=src excludes package.json from the build;
11
+ * `../../package.json` resolves identically from src/server/ (dev) and
12
+ * dist/server/ (published package).
13
+ *
14
+ * @returns The package.json version string
15
+ */
16
+ export function getPackageVersion() {
17
+ const { version } = require('../../package.json');
18
+ return version;
19
+ }
5
20
  /**
6
21
  * Sets up and configures the MCP server with the appropriate request handlers.
7
22
  *
@@ -13,7 +28,7 @@ export function setupServer(knowledgeGraphManager) {
13
28
  // Create server instance
14
29
  const server = new Server({
15
30
  name: 'mcp-neo4j-knowledge-graph',
16
- version: '2.3.2',
31
+ version: getPackageVersion(),
17
32
  }, {
18
33
  capabilities: {
19
34
  tools: {},
@@ -1 +1 @@
1
- {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/server/setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAEnG,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAExE;;;;;GAKG;AACH,8DAA8D;AAC9D,MAAM,UAAU,WAAW,CAAC,qBAA0B;IACpD,yBAAyB;IACzB,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB;QACE,IAAI,EAAE,2BAA2B;QACjC,OAAO,EAAE,OAAO;KACjB,EACD;QACE,YAAY,EAAE;YACZ,KAAK,EAAE,EAAE;YACT,OAAO,EAAE,EAAE;SACZ;KACF,CACF,CAAC;IAEF,4BAA4B;IAC5B,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,EAAC,QAAQ,EAAC,EAAE;QAChE,OAAO,MAAM,sBAAsB,EAAE,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;QAC9D,OAAO,MAAM,qBAAqB,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/server/setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAEnG,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAExE,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,oBAAoB,CAAwB,CAAC;IACzE,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,8DAA8D;AAC9D,MAAM,UAAU,WAAW,CAAC,qBAA0B;IACpD,yBAAyB;IACzB,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB;QACE,IAAI,EAAE,2BAA2B;QACjC,OAAO,EAAE,iBAAiB,EAAE;KAC7B,EACD;QACE,YAAY,EAAE;YACZ,KAAK,EAAE,EAAE;YACT,OAAO,EAAE,EAAE;SACZ;KACF,CACF,CAAC;IAEF,4BAA4B;IAC5B,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,EAAC,QAAQ,EAAC,EAAE;QAChE,OAAO,MAAM,sBAAsB,EAAE,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;QAC9D,OAAO,MAAM,qBAAqB,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -237,6 +237,16 @@ export declare class Neo4jStorageProvider implements StorageProvider {
237
237
  * @returns Array of entity names that need an embedding job scheduled.
238
238
  */
239
239
  getEntityNamesMissingEmbeddings(): Promise<string[]>;
240
+ /**
241
+ * Guard: reject embedding vectors whose length does not match the configured
242
+ * vector index dimension (`NEO4J_VECTOR_DIMENSIONS`). A mismatched write —
243
+ * e.g. a 1536-dim vector into a 1024-dim index — can never be indexed and
244
+ * silently corrupts semantic search. Throwing turns silent corruption into a
245
+ * loud failed job. Inert when `vectorDimensions` is unset.
246
+ *
247
+ * @param vector The embedding vector about to be persisted
248
+ */
249
+ private assertEmbeddingDimension;
240
250
  /**
241
251
  * Store or update the embedding vector for an entity
242
252
  * @param entityName The name of the entity to update
@@ -40,23 +40,38 @@ export class Neo4jStorageProvider {
40
40
  this.connectionManager = options?.connectionManager || new Neo4jConnectionManager(this.config);
41
41
  // Set up schema manager
42
42
  this.schemaManager = new Neo4jSchemaManager(this.connectionManager, this.config, false);
43
- // Set up vector store
43
+ // Set up vector store — dimensions MUST follow the configured index dimension
44
+ // (was hardcoded 1536, which would create a wrong-sized index on non-1536 deployments)
44
45
  this.vectorStore = new Neo4jVectorStore({
45
46
  connectionManager: this.connectionManager,
46
47
  indexName: this.config.vectorIndexName,
47
- dimensions: 1536,
48
+ dimensions: this.config.vectorDimensions,
48
49
  similarityFunction: 'cosine',
49
50
  entityNodeLabel: 'Entity',
50
51
  });
51
52
  logger.debug('Neo4jStorageProvider: Initializing embedding service');
52
53
  try {
53
- // Set up embedding service
54
- this.embeddingService = EmbeddingServiceFactory.createFromEnvironment();
55
- logger.debug('Neo4jStorageProvider: Embedding service initialized successfully', {
56
- provider: this.embeddingService.getProviderInfo().provider,
57
- model: this.embeddingService.getProviderInfo().model,
58
- dimensions: this.embeddingService.getProviderInfo().dimensions,
59
- });
54
+ // Set up embedding service — same provider/production gates as index.ts, so the
55
+ // storage provider's DIRECT write paths (createEntities/createEntitiesBatch)
56
+ // can never generate random/mock vectors when no real provider is configured.
57
+ if (!EmbeddingServiceFactory.hasEmbeddingProvider()) {
58
+ logger.info('Neo4jStorageProvider: no embedding provider configured — entity writes will persist with NULL embeddings');
59
+ }
60
+ else {
61
+ const service = EmbeddingServiceFactory.createFromEnvironment();
62
+ if (!EmbeddingServiceFactory.shouldWriteEmbeddings(service)) {
63
+ logger.error('Neo4jStorageProvider: refusing random/mock embedding service under NODE_ENV=production — ' +
64
+ 'entity writes will persist with NULL embeddings');
65
+ }
66
+ else {
67
+ this.embeddingService = service;
68
+ logger.debug('Neo4jStorageProvider: Embedding service initialized successfully', {
69
+ provider: this.embeddingService.getProviderInfo().provider,
70
+ model: this.embeddingService.getProviderInfo().model,
71
+ dimensions: this.embeddingService.getProviderInfo().dimensions,
72
+ });
73
+ }
74
+ }
60
75
  }
61
76
  catch (error) {
62
77
  logger.error('Neo4jStorageProvider: Failed to initialize embedding service', error);
@@ -595,11 +610,15 @@ export class Neo4jStorageProvider {
595
610
  : '';
596
611
  // Generate embedding using the instance's embedding service
597
612
  embedding = await this.embeddingService.generateEmbedding(text);
613
+ // Wrong-dimension vectors must never be persisted — throw into the
614
+ // catch below so the entity is still created, with embedding=NULL.
615
+ this.assertEmbeddingDimension(embedding);
598
616
  logger.info(`Generated embedding for entity: ${entity.name}`);
599
617
  }
600
618
  catch (error) {
601
619
  logger.error(`Failed to generate embedding for entity: ${entity.name}`, error);
602
620
  // Continue without embedding if generation fails
621
+ embedding = null;
603
622
  }
604
623
  }
605
624
  else {
@@ -1682,6 +1701,29 @@ export class Neo4jStorageProvider {
1682
1701
  `, {});
1683
1702
  return result.records.map(r => String(r.get('name')));
1684
1703
  }
1704
+ /**
1705
+ * Guard: reject embedding vectors whose length does not match the configured
1706
+ * vector index dimension (`NEO4J_VECTOR_DIMENSIONS`). A mismatched write —
1707
+ * e.g. a 1536-dim vector into a 1024-dim index — can never be indexed and
1708
+ * silently corrupts semantic search. Throwing turns silent corruption into a
1709
+ * loud failed job. Inert when `vectorDimensions` is unset.
1710
+ *
1711
+ * @param vector The embedding vector about to be persisted
1712
+ */
1713
+ assertEmbeddingDimension(vector) {
1714
+ const expected = this.config.vectorDimensions;
1715
+ // Number.isFinite (not truthiness): parseInt of a malformed env value yields
1716
+ // NaN, which is falsy and would otherwise silently disable the guard.
1717
+ if (typeof expected === 'number' &&
1718
+ Number.isFinite(expected) &&
1719
+ expected > 0 &&
1720
+ Array.isArray(vector) &&
1721
+ vector.length !== expected) {
1722
+ throw new Error(`Embedding dimension mismatch: got ${vector.length}, vector index expects ${expected} ` +
1723
+ `(NEO4J_VECTOR_DIMENSIONS). Ensure EMBEDDING_MODEL's native output dimension and ` +
1724
+ `EMBEDDING_DIMENSIONS both match the index — refusing to write a corrupt vector.`);
1725
+ }
1726
+ }
1685
1727
  /**
1686
1728
  * Store or update the embedding vector for an entity
1687
1729
  * @param entityName The name of the entity to update
@@ -1689,6 +1731,7 @@ export class Neo4jStorageProvider {
1689
1731
  */
1690
1732
  async updateEntityEmbedding(entityName, embedding) {
1691
1733
  try {
1734
+ this.assertEmbeddingDimension(embedding.vector);
1692
1735
  // Verify that the entity exists
1693
1736
  const entity = await this.getEntity(entityName);
1694
1737
  if (!entity) {
@@ -2342,9 +2385,13 @@ export class Neo4jStorageProvider {
2342
2385
  ? entity.observations.join('\n')
2343
2386
  : '';
2344
2387
  embedding = await this.embeddingService.generateEmbedding(text);
2388
+ // Same write-path dimension guard as createEntities: a mismatched
2389
+ // vector throws into the catch and the entity persists with NULL.
2390
+ this.assertEmbeddingDimension(embedding);
2345
2391
  }
2346
2392
  catch (error) {
2347
2393
  logger.warn(`Failed to generate embedding for entity: ${entity.name}`, error);
2394
+ embedding = null;
2348
2395
  }
2349
2396
  }
2350
2397
  const now = Date.now();