Skip to content

Rebase from SecKatie/FalkorDB-MCPServer#14

Merged
gkorland merged 39 commits into
mainfrom
rebase
Feb 23, 2026
Merged

Rebase from SecKatie/FalkorDB-MCPServer#14
gkorland merged 39 commits into
mainfrom
rebase

Conversation

@gkorland
Copy link
Copy Markdown
Contributor

@gkorland gkorland commented Feb 8, 2026

Summary by CodeRabbit

  • New Features

    • Read-only query mode, Redis-backed key-value support, MCP tools, prompts, and resources
    • Centralized error handling and structured logging with optional file logging
    • npm package metadata and CLI/publish support
  • Infrastructure

    • Replaced HTTP server with a standalone MCP (stdio) server; CI/CD reworked for npm publishing and newer Node/ESM targets
    • Removed Docker-based build/publish flow and HTTP routing/middleware
  • Documentation

    • Expanded README, contributing guide, and new Claude integration doc
  • Chores

    • Updated environment configuration and .gitignore
  • Tests

    • Added extensive unit and integration tests across services and tooling

SecKatie and others added 12 commits July 25, 2025 10:30
…rvice integration

- Updated package dependencies in package.json and package-lock.json to latest versions.
- Refactored the server implementation in src/index.ts to utilize the Model Context Protocol SDK for handling graph operations.
- Introduced a new Redis service in src/services/redis.service.ts for managing key-value storage.
- Enhanced configuration to include Redis connection details in src/config/index.ts.
- Changed TypeScript target and module settings in tsconfig.json to ES2022 for improved compatibility.
- Remove legacy HTTP API components (controllers, routes, middleware)
- Clean up Express dependencies from package.json
- Simplify graph_list resource to return markdown format
- Add comprehensive CLAUDE.md for development guidance
- Modernize README with enhanced setup instructions and examples
- Focus entirely on MCP server functionality via stdio transport

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Replace @/* path aliases with relative imports in src/index.ts and src/mcp/resources.ts
- Update logger service to use platformdirs for cross-platform log directory
- Add platformdirs dependency and mock in Jest configuration
- Fix transformIgnorePatterns in jest.config.js for ES module compatibility
- Update lint configuration to use flat config format (eslint.config.js)
- Add error handling for log directory creation with fallback
- All tests now pass and build succeeds without module resolution errors

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Updated .env.example to reflect new environment and Redis configurations.
- Added CONTRIBUTING.md to provide guidelines for community contributions.
- Modified .gitignore to exclude log files and MCP-specific artifacts.
- Updated package.json to require Node.js 18+ and added prepublishOnly script for CI checks.
- Improved README.md with clearer setup instructions and additional commands.
- Refined GitHub Actions workflow for CI/CD, including security audits and coverage reporting.
- Refactored prompts in src/mcp/prompts.ts to include a new memory query prompt for enhanced functionality.
- Added environment name and URL for the release job in the node.yml workflow to link to the published package on npm.
- Bump package version to 1.0.1 in package.json.
- Add instructions for running the server from npm in README.md.
- Include a shebang in src/index.ts for direct execution.
- Introduce a new graph reorganization prompt in src/mcp/prompts.ts for improved graph management.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Katie Mulliken <katie@mulliken.net>
chore: update version and enhance README with npm usage instructions
Add comprehensive read-only query support to enable safe querying
of FalkorDB read replicas and prevent accidental write operations.

Features:
- Add FALKORDB_DEFAULT_READONLY config option for global read-only mode
- Enhance query_graph tool with optional readOnly parameter
- Add dedicated query_graph_readonly tool for explicit read-only queries
- Use graph.roQuery() for read-only operations (GRAPH.RO_QUERY)
- Maintain backward compatibility (default: read-write mode)

Implementation:
- Update FalkorDBService.executeQuery() with readOnly parameter
- Add FalkorDBService.executeReadOnlyQuery() convenience method
- Add config.falkorDB.defaultReadOnly setting
- Update MCP tools with read-only support
- Add comprehensive test coverage (24 new tests)

Tests:
- All 100 tests passing
- Test coverage for read-only query execution
- Test both roQuery() and query() code paths
- Test error handling for read-only operations
- Test configuration defaults

Documentation:
- Update README.md with read-only mode usage
- Update CLAUDE.md with API documentation
- Add .env.example configuration
- Document use cases for replica instances

Use Cases:
- Connect to FalkorDB read replicas in replication setups
- Prevent accidental writes in production environments
- Run analytics/reporting queries safely
- Provide read-only access in multi-tenant setups

Breaking Changes: None
Backward Compatibility: Fully maintained
feat: add read-only query support for replica instances
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces the Express HTTP MCP server with a stdio-based ModelContextProtocol server; adds Redis support, centralized error and logging subsystems, robust FalkorDB and Redis initialization with retries, extensive MCP tools/resources/prompts, ESM/tooling migration, and large additions to tests and documentation.

Changes

Cohort / File(s) Summary
Server Core & Startup
src/index.ts
Replaced Express app with MCP server (stdio transport), added global error handlers, graceful shutdown, and removed HTTP export.
MCP Surface
src/mcp/tools.ts, src/mcp/resources.ts, src/mcp/prompts.ts
Added registerable MCP tools (graph ops, Redis KV), a graph-listing resource, and three assistant prompts (user_setup, memory_query, graph_reorganization).
Services: FalkorDB
src/services/falkordb.service.ts, src/services/falkordb.service.test.ts
Added explicit initialize() with retry/backoff, readOnly support, executeReadOnlyQuery, deleteGraph, robust error handling, and expanded tests.
Services: Redis
src/services/redis.service.ts, src/services/redis.service.test.ts
New RedisService singleton with deduplicated init, retry/backoff, CRUD, listKeys (SCAN), close, and comprehensive tests.
Logging & Error Handling
src/services/logger.service.ts, src/services/logger.service.test.ts, src/errors/AppError.ts, src/errors/ErrorHandler.ts, src/errors/*.test.ts
Introduced Logger (file + MCP notifications), AppError/CommonErrors, ErrorHandler singleton, sync/async logging APIs, and tests.
Removed HTTP & Middleware
src/controllers/mcp.controller.ts, src/routes/mcp.routes.ts, src/middleware/auth.middleware.ts, src/controllers/mcp.controller.test.ts
Deleted Express controllers, routes, auth middleware and related HTTP tests.
Config & Env
.env.example, src/config/index.ts, src/config/index.test.ts
Renamed header to Environment Configuration, removed PORT, added FALKORDB_DEFAULT_READONLY and Redis env vars, dotenv quiet mode; tests updated for defaultReadOnly.
Tooling & ESM Migration
package.json, jest.config.js, tsconfig.json, eslint.config.js, .eslintrc
Moved to ESM (type: "module"), updated Jest for ESM, bumped TS target/moduleResolution, added eslint.config.js and removed legacy .eslintrc, expanded metadata, scripts and deps.
CI / CD
.github/workflows/node.yml
Reworked workflow: renamed, updated triggers, Node matrix bumped, removed Docker publish in favor of npm publish on releases, added Codecov and updated actions versions.
Docs & Repo Files
README.md, CLAUDE.md, CONTRIBUTING.md, .gitignore
Major README rewrite, added CLAUDE.md and CONTRIBUTING.md, updated .gitignore to exclude logs and *.mcp.log.
Tests & Utilities
src/utils/connection-parser.ts, src/utils/connection-parser.test.ts, src/models/..., many src/services/*.test.ts
Improved connection parsing to handle '@' and ':' in passwords; added broad unit tests for services, parsers, models, and configs; many new/expanded test suites.
Packaging / Build
Dockerfile
Removed Dockerfile (multi-stage build deleted).

Sequence Diagram(s)

sequenceDiagram
  participant Starter as Startup
  participant CFG as Config
  participant LOG as Logger
  participant FDB as FalkorDB Service
  participant RED as Redis Service
  participant MCP as McpServer

  Starter->>CFG: load env & defaults
  Starter->>LOG: instantiate logger
  Starter->>FDB: initialize()
  FDB-->>Starter: success / retries
  Starter->>RED: initialize()
  RED-->>Starter: success / retries
  Starter->>MCP: create McpServer (stdio)
  Starter->>LOG: setMcpServer(MCP)
  Starter->>MCP: register tools/resources/prompts
  Starter->>MCP: start listening (stdio)
  MCP-->>Starter: ready
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped from ports to stdio bright,
Redis leaves and retries in sight,
Errors caught, logs softly hum,
Prompts and tools wake MCP from slum,
A rabbit's refactor—fast and light 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Rebase from SecKatie/FalkorDB-MCPServer' is vague and does not clearly communicate the actual changes made in this pull request. The PR contains substantial architecture changes (HTTP to MCP migration, service refactoring, configuration updates) but the title only indicates a rebase without explaining what was actually changed. Revise the title to clearly describe the main changes, such as 'Migrate from HTTP API to MCP server architecture' or 'Refactor MCP server implementation with service layer and error handling'. This will help reviewers understand the scope at a glance.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch rebase

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread .github/workflows/node.yml Fixed
Comment thread .github/workflows/node.yml Fixed
Comment thread .github/workflows/node.yml Fixed
@gkorland gkorland requested a review from Copilot February 8, 2026 20:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Rebases the project onto an MCP stdio-based server architecture (moving away from the prior Express HTTP API), adding service-layer resiliency, logging, and expanded MCP capabilities.

Changes:

  • Replaced the Express server/routes/auth/controller stack with an MCP server over stdio, plus MCP tools/resources/prompts registration.
  • Added Redis + enhanced FalkorDB services with retries, structured logging, and centralized error handling.
  • Updated project config/tooling (ESM, ESLint flat config, CI workflow, docs) and added/expanded Jest test coverage.

Reviewed changes

Copilot reviewed 30 out of 34 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
src/utils/connection-parser.test.ts Adds unit tests for connection string parsing behavior and edge cases
src/services/redis.service.ts Introduces Redis client service with initialization, retries, and basic operations
src/services/redis.service.test.ts Adds Jest coverage for Redis service initialize/get/set/close behavior
src/services/logger.service.ts Adds MCP-aware logger with optional file logging fallback
src/services/logger.service.test.ts Adds basic logger interface/resilience tests
src/services/falkordb.service.ts Refactors FalkorDB service to explicit initialization + retries + typed replies + logging
src/services/falkordb.service.test.ts Expands FalkorDB service tests for init, read-only queries, deleteGraph, errors
src/routes/mcp.routes.ts Removes old Express MCP routes
src/middleware/auth.middleware.ts Removes old Express API key auth middleware
src/controllers/mcp.controller.ts Removes old Express MCP controller
src/controllers/mcp.controller.test.ts Removes tests tied to removed Express controller
src/models/mcp-client-config.test.ts Adds tests validating MCP client/server config model shapes
src/mcp/tools.ts Adds MCP tools for graph/redis operations (query/list/delete/set/get)
src/mcp/resources.ts Adds MCP resource for graph listing
src/mcp/prompts.ts Adds MCP prompts (user setup, memory query, graph reorg)
src/index.ts Replaces Express startup with MCP server startup + shutdown + global error handlers
src/errors/ErrorHandler.ts Adds centralized error handler and trusted/untrusted crash policy
src/errors/ErrorHandler.test.ts Adds tests for ErrorHandler behavior
src/errors/AppError.ts Adds AppError + common error names for consistent error handling
src/config/index.ts Extends config (read-only default + redis config) and tweaks dotenv loading
src/config/index.test.ts Updates config tests for new falkorDB defaultReadOnly
package.json Migrates to ESM + updates deps/scripts/metadata; adds redis/platformdirs/zod
eslint.config.js Adds ESLint flat config for TS
README.md Updates docs for MCP stdio usage, new tools, and configuration
CONTRIBUTING.md Adds contribution guidelines
CLAUDE.md Adds repo guidance for Claude Code
.github/workflows/node.yml Updates CI to modern Node versions, coverage, and NPM publish on release
.eslintrc Removes legacy ESLint config
Dockerfile Removes Docker image build definition
.env.example Updates environment variable examples for new server/services

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/services/redis.service.ts Outdated
Comment thread src/services/falkordb.service.ts Outdated
Comment thread src/services/logger.service.ts
Comment thread src/services/redis.service.ts Outdated
Comment thread src/services/redis.service.ts
Comment thread src/utils/connection-parser.test.ts Outdated
Comment thread src/utils/connection-parser.test.ts Outdated
Comment thread src/mcp/tools.ts Outdated
Comment thread src/mcp/tools.ts Outdated
Comment thread package.json Outdated
gkorland and others added 2 commits February 8, 2026 22:45
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…in permissions

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@gkorland
Copy link
Copy Markdown
Contributor Author

gkorland commented Feb 8, 2026

@claude[agent] open a new pull request to apply changes based on the relevant comments in this thread

@Claude
Copy link
Copy Markdown
Contributor

Claude AI commented Feb 8, 2026

@gkorland I've opened a new pull request, #15, to work on those changes. Once the pull request is ready, I'll request review from you.

Claude AI and others added 2 commits February 8, 2026 21:05
…ng, add tests

Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com>
Address PR review comments: fix concurrent init, improve error handling, add tests
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tsconfig.json (1)

33-35: ⚠️ Potential issue | 🟡 Minor

Remove unused path alias configuration.

The @/* path alias is not used anywhere in the codebase. Remove the paths configuration along with baseUrl to eliminate unnecessary configuration.

🤖 Fix all issues with AI agents
In @.env.example:
- Line 18: The file is missing a POSIX-style trailing newline; open the
.env.example file and ensure the final line ("ENABLE_FILE_LOGGING=false") is
terminated with a newline character so the file ends with a blank line; simply
add a single trailing newline at EOF and save the file.

In @.github/workflows/node.yml:
- Around line 58-61: The environment URL currently interpolates
github.event.release.tag_name which may include a leading "v" and produce a
broken npm URL; add a preceding workflow step that normalizes the release tag
into a plain version string (remove any leading "v"/"V" from
github.event.release.tag_name using a small shell or actions step to export
RELEASE_VERSION) and then set environment.url to use that normalized value
(replace usage of github.event.release.tag_name with the computed
RELEASE_VERSION) so the npm link becomes .../v/1.0.1 instead of .../v/v1.0.1.
- Around line 45-52: The Codecov upload step using codecov/codecov-action@v4 is
missing the required upload token; add the token parameter under the action
inputs (e.g., add token: ${{ secrets.CODECOV_TOKEN }} to the with: block for the
Upload coverage to Codecov step) so v4 can perform uploads, and ensure the
secret CODECOV_TOKEN is configured in the repository secrets; keep the existing
file, flags, name, and fail_ci_if_error settings intact.

In `@CLAUDE.md`:
- Line 12: Replace the incorrect npm command string `npm test:coverage` with the
correct `npm run test:coverage` in the CLAUDE.md entry for "Test with coverage"
so npm invokes the script via `run` instead of treating it as a built-in
command; update the markdown line that currently reads `**Test with coverage**:
`npm test:coverage` to use `npm run test:coverage` instead.

In `@CONTRIBUTING.md`:
- Around line 9-10: Update CONTRIBUTING.md so the Node requirement line "Node.js
16+" is changed to "Node.js 18+" to match package.json and CI, leave the "npm
8+" line unchanged, and add language specifiers (e.g., bash or text) to the two
fenced code blocks that currently lack a language tag (the blocks near the
examples referenced in the doc) so they render correctly.

In `@package.json`:
- Around line 95-98: The package.json funding entry currently points to the fork
author's sponsor URL; update the "funding" object (the funding field and its
"url" value) to reference the correct org sponsor or funding destination for
FalkorDB (e.g., change the URL from "https://github.com/sponsors/SecKatie" to
the FalkorDB sponsors URL or remove the field if no org sponsorship exists) and
ensure the "type" remains appropriate ("github") if using a GitHub sponsors
link.
- Around line 11-18: Update the package metadata fields in package.json so they
point to the canonical upstream repo instead of the fork: replace the
"repository" object url, the "bugs" url, and the "homepage" string (the
"repository", "bugs", and "homepage" fields) to reference
https://github.com/FalkorDB/FalkorDB-MCPServer (use git+https for the
repository.url and the correct /issues and README anchor for bugs and homepage
respectively) so npm metadata, issue links and docs point to the official
repository.
- Line 87: The dependency declaration for "platformdirs" in package.json is
using a release-candidate version ("platformdirs": "^4.3.8-rc3"); update it to a
stable release by replacing that version with "4.3.7" (or, if you truly need the
RC, pin it exactly without a caret) so production installs use a supported
stable release; ensure you run package manager install (npm/yarn/pnpm) and
update lockfile after changing the "platformdirs" version string.

In `@README.md`:
- Line 56: The README's git clone URL currently points to the fork URL
"https://github.com/SecKatie/falkordb-mcpserver.git"; update that line to the
upstream repository "https://github.com/FalkorDB/FalkorDB-MCPServer.git"
(matching the PR target) so contributors clone the canonical repo, and verify
the README contains the correct repository name/capitalization for
FalkorDB/FalkorDB-MCPServer.

In `@src/index.ts`:
- Around line 14-18: The uncaughtException handler calls the async
errorHandler.handleError(error) without awaiting it, creating unhandled promise
risk and racing with errorHandler.crashIfUntrustedError(error); change this to
not call the async function directly — either remove the handleError call from
the process.on('uncaughtException', ...) handler and rely on synchronous logging
(logger.errorSync) and crashIfUntrustedError, or, if you must invoke it, call
errorHandler.handleError(error).catch(err => logger.errorSync('handleError
rejected', err)) to swallow rejections and avoid unhandledRejection/recursion
while ensuring crashIfUntrustedError still runs synchronously.

In `@src/mcp/prompts.ts`:
- Around line 107-114: The prompt injects user values directly into the Cypher
example (the interpolations of query and relationship_depth in the Example
Cypher patterns), risking injection; update the prompt generation in
src/mcp/prompts.ts to stop splicing raw values into the code block—either remove
the inline Cypher example and ask the LLM to construct the query from textual
instructions, or sanitize/escape user input before interpolation: escape
double-quotes and Cypher-special characters in query and strictly coerce
relationship_depth to an integer with safe bounds (e.g., clamp to a small max)
before use; locate the template where query and relationship_depth are
interpolated and apply one of these mitigations.
- Line 5: The comment "Register graph_list resource" is stale; update or replace
it to accurately describe the code that registers the user_setup prompt (e.g.,
change to "Register user_setup prompt" or remove the comment entirely). Locate
the comment near the code that calls/defines the user_setup registration (look
for the user_setup identifier and the function that registers prompts) and
ensure the comment text matches that function's purpose.
- Around line 63-66: The argsSchema entry relationship_depth currently uses
z.string() but is later interpolated into a Cypher range
"*1..${relationship_depth}", allowing non-numeric or out-of-range values; change
the schema for relationship_depth in argsSchema (in src/mcp/prompts.ts) to
validate/coerce a numeric integer within 1–3 (e.g.,
z.coerce.number().int().min(1).max(3)) or alternatively restrict to
z.enum(["1","2","3"]) so only safe, expected values reach the Cypher
interpolation and prompt generation.

In `@src/mcp/resources.ts`:
- Line 13: The resource currently declares mimeType: "text/plain" while the
content generated in the same resource (see the line that formats graph names as
`- ${name}`) is Markdown; change the mimeType value to "text/markdown" so MCP
clients render the response as Markdown. Locate the mimeType property in the
resource definition in src/mcp/resources.ts and replace the string "text/plain"
with "text/markdown", leaving the rest of the object and the Markdown list
formatting unchanged.

In `@src/services/falkordb.service.ts`:
- Around line 20-32: The initialize() entrypoint currently leaves the
instance-level retryCount unchanged after a failed _initialize(), causing
subsequent initialize() calls to immediately throw; fix by resetting retryCount
to 0 at the start of initialize() (before setting this.initializingPromise or
calling _initialize()) so every top-level initialize() call gets a fresh retry
budget; ensure this change addresses both occurrences related to
initialize()/_initialize() and that close() no longer needs to be called to
restore retries.
- Around line 106-113: The error log currently passes the full query string to
logger.error (logger.error('Query execution failed', appError, { graphName,
query, readOnly })), which can leak sensitive/user-supplied data; change this to
log a truncated/sanitized version of query (reuse the same truncation logic used
for debug logs) before calling logger.error, e.g., compute a safeQuery variable
from query via the existing truncation/sanitization helper and pass safeQuery
instead of query along with graphName, readOnly, and the AppError instance.

In `@src/services/logger.service.ts`:
- Around line 88-97: The MCP notification currently uses level.toLowerCase(),
which turns 'WARN' into 'warn' but the MCP spec expects 'warning'; update the
logic around the notification call (reference: the level variable and the
this.mcpServer.server.notification(...) invocation) to map 'WARN' (and any
'Warn' variants) to 'warning' before sending — e.g., normalize level then apply
a small mapping or switch (create a helper or inline conditional) so all other
levels continue to use toLowerCase() while 'WARN' becomes 'warning'.

In `@src/services/redis.service.ts`:
- Around line 30-71: The _initialize method leaks a connected Redis client when
connect() succeeds but ping() fails because this.client is overwritten on retry;
before creating or reassigning a new client in _initialize (and before recursing
to retry), check if this.client is set and connected and gracefully
close/disconnect it (await this.client.disconnect() or equivalent, catching any
errors), then null out this.client before creating a fresh client via
createClient; also ensure the same cleanup runs in the final failure path
(before throwing AppError) so no connected client is left open.

In `@src/utils/connection-parser.test.ts`:
- Around line 264-275: The test reveals parseFalkorDBConnectionString currently
truncates passwords containing ':' because it does auth.split(':') and only uses
parts[0] and parts[1]; fix parseFalkorDBConnectionString so when parsing the
auth segment you treat the first ':' as the user/password separator and
reconstruct the password by joining any remaining parts (e.g.,
parts.slice(1).join(':')) instead of dropping extras, ensuring username =
parts[0] and password = joined remainder.
- Around line 251-262: The test exposes that parseFalkorDBConnectionString
incorrectly splits on the first '@' causing parts after additional '@' to be
dropped; update the parser to locate the auth/host delimiter with
lastIndexOf('@') instead of naive split so the segment before the last '@' is
treated as userinfo (username:password) and the segment after as host:port;
modify parseFalkorDBConnectionString to extract auth = str.slice(0, lastAt) and
hostPort = str.slice(lastAt + 1), then parse username/password from auth
(handling missing username or password) and parse host/port from hostPort, and
add a follow-up to update this test expectation once the parser is fixed.
🧹 Nitpick comments (24)
src/models/mcp-client-config.test.ts (3)

85-92: "Immutable reference" test is misleading — it only checks referential identity, not immutability.

Assigning a module-level const to two variables and comparing them with toBe will always pass — it proves nothing about immutability. If the intent is to ensure the exported config isn't accidentally mutated between tests, consider using Object.freeze on the export and testing that mutations throw, or remove this test.


77-83: Type-compliance tests provide no runtime value.

These tests assign a sample to a typed variable and then assert reference equality. The type check is a compile-time concern already handled by TypeScript — the runtime assertion (toBe) adds nothing. Consider removing these or replacing them with meaningful structural assertions if the intent is to guard against breaking changes.

Also applies to: 121-127


105-142: Redundant coverage: exact args snapshot overlaps with individual toContain checks.

Lines 105-119 duplicate the full args array from the source (brittle if the sample changes), and lines 129-142 re-check the same values with toContain. Pick one approach — the toContain style (lines 129-142) is more resilient to ordering/addition changes, while the toEqual style (lines 105-119) serves as a snapshot. Having both is redundant.

.gitignore (1)

139-140: Optional: Consider whether explicit patterns add clarity or create redundancy.

The patterns logs/ and *.mcp.log are technically redundant with existing entries:

  • Line 2 already contains logs which matches both files and directories named "logs"
  • Line 3 already contains *.log which matches all .log files including .mcp.log

That said, explicit project-specific patterns can serve as useful documentation of what artifacts the project generates, and the redundancy is harmless.

src/utils/connection-parser.test.ts (1)

227-249: Mocking global.parseInt is brittle — consider a less invasive approach.

Overriding global.parseInt risks leaking if the finally block doesn't restore it (e.g., a test runner crash). It also won't break if the implementation is refactored to use Number() or a different parsing path. A more targeted approach would be to pass in a string that actually exercises the catch block in the implementation, though admittedly the current try/catch is very broad and hard to trigger naturally. This is acceptable for now but worth noting.

tsconfig.json (1)

29-31: Consider using "moduleResolution": "node16" for ES module compatibility.

With "module": "es2022" and "type": "module" in package.json, "moduleResolution": "node" uses the legacy CommonJS resolution algorithm, which doesn't enforce file extensions in imports and doesn't properly resolve exports fields in package.json. Using "node16" or "nodenext" aligns resolution with Node.js's actual ESM behavior and catches missing .js extensions at compile time.

♻️ Suggested change
-    "moduleResolution": "node",                     /* Specify how TypeScript looks up a file from a given module specifier. */
+    "moduleResolution": "node16",                   /* Specify how TypeScript looks up a file from a given module specifier. */
CONTRIBUTING.md (1)

71-78: Add language specifiers to fenced code blocks.

Per markdownlint MD040, the code block at Line 71 (commit message examples) and Line 183 (project structure) should have language specifiers.

📝 Suggested fix
-```
-type(scope): description
+```text
+type(scope): description
-```
-src/
+```text
+src/

Also applies to: 183-198

CLAUDE.md (1)

44-58: Add language specifier to the project structure code block.

Per markdownlint MD040, the fenced code block at Line 45 should have a language specified (e.g., text).

Also, the project structure here doesn't mention logger.service.ts under services/, though CONTRIBUTING.md and README.md both list it. Consider keeping these in sync.

README.md (1)

116-121: Add language specifiers to fenced code blocks.

Several code blocks showing natural language prompts and the project structure tree lack language specifiers (markdownlint MD040). Use text for these blocks.

Also applies to: 131-135, 138-142, 188-209

eslint.config.js (1)

1-27: Update ESLint flat config to follow @typescript-eslint v8 documented pattern.

The current configuration works but uses a legacy spreading pattern. With @typescript-eslint v8.38.0 and ESLint 9.31.0, the recommended approach is to import typescript-eslint directly and use the flat config arrays:

Suggested refactor
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';

export default [
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  {
    ignores: ['node_modules/**', 'dist/**', 'coverage/**'],
  },
];

If type-aware rules are needed, use tseslint.configs.recommendedTypeChecked and add parserOptions.projectService: true.

package.json (1)

68-69: prepublish and prepublishOnly overlap.

prepublish runs on both npm publish and npm install (in some npm versions), and prepublishOnly runs only on npm publish. Since prepublishOnly already does clean && test:ci && lint && build, the separate prepublish script is redundant and may cause double execution of tests/lint/build during npm publish. Consider removing the prepublish script.

Proposed fix
-    "prepublish": "npm run test:ci && npm run lint && npm run build",
     "prepublishOnly": "npm run clean && npm run test:ci && npm run lint && npm run build"
src/mcp/resources.ts (1)

7-31: Inconsistent indentation throughout the resource registration block.

The arguments to server.registerResource mix 1-space, 3-space, and 4-space indentation (e.g., lines 8–14 vs. 15–29). Consider normalizing to a consistent style.

.env.example (1)

5-15: Consider adding a comment clarifying the relationship between FalkorDB and Redis configs.

FALKORDB_HOST/FALKORDB_PORT default to localhost:6379, and REDIS_URL also defaults to redis://localhost:6379. Since FalkorDB is backed by Redis, newcomers may wonder if these are the same instance or need separate servers. A brief comment would help.

src/services/logger.service.test.ts (2)

125-138: The failing MCP server mock set via setMcpServer is never cleaned up.

Line 133 sets a failing MCP server on the singleton logger, but there's no corresponding teardown to reset it. Since logger is a module-level singleton, this state could leak into other test files if Jest happens to share the module instance (depending on --isolateModules config). Consider resetting the MCP server in afterEach or at the end of this test.

Proposed fix
       // Logger should not throw even if MCP notifications fail
       await expect(logger.info('Test with failing MCP')).resolves.not.toThrow();
+
+      // Reset MCP server to avoid leaking state
+      logger.setMcpServer(null as any);
     });

57-103: Tests only verify no-throw behavior, not actual logging output.

The async and sync tests are useful as smoke tests, but they don't assert that messages are actually written (to console, file, or MCP). If the logger silently swallows all input due to a bug, these tests would still pass. Consider adding at least one assertion that verifies the log output reaches the expected sink (e.g., console.error was called for error-level logs, or fs.appendFileSync was called when file logging is enabled).

src/mcp/tools.ts (2)

9-57: Consider exposing query parameters in the query_graph tool input schema.

The underlying executeQuery method supports a params argument, but the tool always passes undefined (line 42). For parameterized OpenCypher queries, this means users must inline values into the query string rather than using parameter binding, which is less safe and less convenient.

💡 Proposed schema addition
       inputSchema: {
         graphName: z.string().describe("The name of the graph to query"),
         query: z.string().describe("The OpenCypher query to run"),
+        params: z.record(z.unknown()).optional().describe("Optional query parameters for parameterized queries"),
         readOnly: z.boolean().optional().describe("If true, executes as a read-only query (GRAPH.RO_QUERY). Useful for replica instances or to prevent accidental writes. Defaults to FALKORDB_DEFAULT_READONLY environment variable."),
       },
     },
-    async ({graphName, query, readOnly}) => {
+    async ({graphName, query, params, readOnly}) => {

Then pass params instead of undefined on line 42:

-        const result = await falkorDBService.executeQuery(graphName, query, undefined, isReadOnly);
+        const result = await falkorDBService.executeQuery(graphName, query, params, isReadOnly);

220-226: The value null/undefined check is unreachable when zod validates input.

Since value is declared as z.string() in the input schema (line 207), the MCP SDK will validate the input before the handler runs. A string value will never be undefined or null here. If the intention is defense-in-depth, the check is harmless but misleading.

src/services/redis.service.ts (1)

32-35: Logger calls are not awaited throughout the service.

All logger.*() calls in this file are fire-and-forget (e.g., line 32 logger.info(...) without await). Since logger methods are async, unhandled rejections could occur if the logger fails. This is inconsistent with src/mcp/tools.ts where all logger calls are awaited.

♻️ Example fix for a few locations
-      logger.info('Attempting to connect to Redis', {
+      await logger.info('Attempting to connect to Redis', {
         url: config.redis.url,
         attempt: this.retryCount + 1
       });
-      logger.info('Successfully connected to Redis');
+      await logger.info('Successfully connected to Redis');

Apply similarly to all other logger.*() calls in the file.

Also applies to: 46-47, 52-56

src/index.ts (1)

104-105: Unhandled promise from startServer() call.

The returned promise from startServer() is not handled. While startServer has its own try/catch that calls gracefulShutdown, if gracefulShutdown itself throws (unlikely but possible), the rejection propagates to the unhandledRejection handler, which re-throws, then hits uncaughtException. This chain works but is fragile.

💡 Add a catch handler for clarity
-startServer();
+startServer().catch((error) => {
+  logger.errorSync('Fatal startup error', error instanceof Error ? error : new Error(String(error)));
+  process.exit(1);
+});
src/errors/ErrorHandler.test.ts (1)

226-232: Singleton test is a tautology—expect(errorHandler).toBe(errorHandler) is always true.

Line 230 compares a reference to itself, which doesn't actually verify singleton behavior. To meaningfully test the singleton, import errorHandler from two different calls or compare the exported instance against a new ErrorHandler() to confirm they differ.

💡 A more meaningful singleton check
   describe('singleton instance', () => {
     it('should export a singleton instance', () => {
       // Assert
       expect(errorHandler).toBeInstanceOf(ErrorHandler);
-      expect(errorHandler).toBe(errorHandler); // Same instance
+      // Verify it's a pre-constructed singleton, not a new instance each access
+      const anotherNew = new ErrorHandler();
+      expect(errorHandler).not.toBe(anotherNew);
     });
   });
src/services/logger.service.ts (2)

70-78: Synchronous file I/O on every log call — no rotation or size cap.

appendFileSync blocks the event loop on each write. For a low-throughput stdio server this is tolerable, but the log file can grow without bound. Consider adding a simple size check or rotation strategy (e.g., rotate when the file exceeds a threshold) to prevent disk exhaustion in long-running instances.


121-128: Conditional spread with && works but is non-obvious — consider explicit conditional.

...(error as any).isOperational !== undefined && { isOperational: ... } relies on ...false being a no-op. It works, but a more readable pattern would be an explicit conditional or helper.

Also, this same logic is duplicated in errorSync (lines 153-160). Consider extracting a small private helper to build the error context.

src/services/falkordb.service.ts (2)

67-68: Fixed 5-second retry delay — consider exponential backoff.

All 5 retry attempts use a static 5 000 ms delay. Exponential backoff (e.g., Math.min(1000 * 2 ** this.retryCount, 30000)) would be more resilient to transient outages and avoid thundering-herd issues if multiple instances retry simultaneously.


82-89: Optional params before required-with-default readOnly — API ergonomics concern.

The signature executeQuery(graphName, query, params?, readOnly = false) means callers who want read-only without params must pass undefined explicitly: executeQuery('g', 'q', undefined, true). The dedicated executeReadOnlyQuery helper mitigates this, but consider using an options object for the trailing parameters if this API is expected to grow.

Comment thread .env.example Outdated
Comment thread .github/workflows/node.yml
Comment on lines +58 to +61
if: github.event_name == 'release' && github.event.action == 'published'
environment:
name: Default
url: https://www.npmjs.com/package/falkordb-mcpserver/v/${{ github.event.release.tag_name }}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Environment URL may be incorrect if release tags use a v prefix.

github.event.release.tag_name typically includes a v prefix (e.g., v1.0.1), but npm package URLs expect the bare version (1.0.1). This would produce a broken link like .../v/v1.0.1.

Consider stripping the prefix:

url: https://www.npmjs.com/package/falkordb-mcpserver/v/${{ github.event.release.tag_name }}

url: https://www.npmjs.com/package/falkordb-mcpserver/v/${{ github.event.release.tag_name  }}

Unfortunately, GitHub Actions expressions don't have a built-in replace, so you may need to accept a potentially imperfect URL or handle tag naming conventions (e.g., always tag without the v prefix, or use a step to compute the version).

🤖 Prompt for AI Agents
In @.github/workflows/node.yml around lines 58 - 61, The environment URL
currently interpolates github.event.release.tag_name which may include a leading
"v" and produce a broken npm URL; add a preceding workflow step that normalizes
the release tag into a plain version string (remove any leading "v"/"V" from
github.event.release.tag_name using a small shell or actions step to export
RELEASE_VERSION) and then set environment.url to use that normalized value
(replace usage of github.event.release.tag_name with the computed
RELEASE_VERSION) so the npm link becomes .../v/1.0.1 instead of .../v/v1.0.1.

Comment thread CLAUDE.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread src/services/falkordb.service.ts
Comment thread src/services/logger.service.ts Outdated
Comment thread src/services/redis.service.ts
Comment thread src/utils/connection-parser.test.ts
Comment on lines +264 to +275
it('should handle edge case with multiple : in auth', () => {
// Act
const result = parseFalkorDBConnectionString('falkordb://user:pass:extra@host:1234');

// Assert - implementation takes first : as separator between user and pass
expect(result).toEqual({
host: 'host',
port: 1234,
username: 'user',
password: 'pass'
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test documents password truncation when it contains :.

auth.split(':') on user:pass:extra yields ['user', 'pass', 'extra'], and only parts[0] and parts[1] are used — extra is silently dropped. Passwords containing : are valid, so the parser should rejoin from index 1 onward (e.g., parts.slice(1).join(':')). Same follow-up as the @ issue above.

🤖 Prompt for AI Agents
In `@src/utils/connection-parser.test.ts` around lines 264 - 275, The test reveals
parseFalkorDBConnectionString currently truncates passwords containing ':'
because it does auth.split(':') and only uses parts[0] and parts[1]; fix
parseFalkorDBConnectionString so when parsing the auth segment you treat the
first ':' as the user/password separator and reconstruct the password by joining
any remaining parts (e.g., parts.slice(1).join(':')) instead of dropping extras,
ensuring username = parts[0] and password = joined remainder.

gkorland and others added 2 commits February 8, 2026 23:40
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Comment thread .github/workflows/node.yml Dismissed
@gkorland
Copy link
Copy Markdown
Contributor Author

gkorland commented Feb 9, 2026

@claude[agent]

In @src/services/falkordb.service.ts around lines 106 - 113, The error log
currently passes the full query string to logger.error (logger.error('Query
execution failed', appError, { graphName, query, readOnly })), which can leak
sensitive/user-supplied data; change this to log a truncated/sanitized version
of query (reuse the same truncation logic used for debug logs) before calling
logger.error, e.g., compute a safeQuery variable from query via the existing
truncation/sanitization helper and pass safeQuery instead of query along with
graphName, readOnly, and the AppError instance.

In @src/services/logger.service.ts around lines 88 - 97, The MCP notification
currently uses level.toLowerCase(), which turns 'WARN' into 'warn' but the MCP
spec expects 'warning'; update the logic around the notification call
(reference: the level variable and the this.mcpServer.server.notification(...)
invocation) to map 'WARN' (and any 'Warn' variants) to 'warning' before sending
— e.g., normalize level then apply a small mapping or switch (create a helper or
inline conditional) so all other levels continue to use toLowerCase() while
'WARN' becomes 'warning'.

In @src/services/redis.service.ts around lines 30 - 71, The _initialize method
leaks a connected Redis client when connect() succeeds but ping() fails because
this.client is overwritten on retry; before creating or reassigning a new client
in _initialize (and before recursing to retry), check if this.client is set and
connected and gracefully close/disconnect it (await this.client.disconnect() or
equivalent, catching any errors), then null out this.client before creating a
fresh client via createClient; also ensure the same cleanup runs in the final
failure path (before throwing AppError) so no connected client is left open.

In @src/utils/connection-parser.test.ts around lines 251 - 262, The test exposes
that parseFalkorDBConnectionString incorrectly splits on the first '@' causing
parts after additional '@' to be dropped; update the parser to locate the
auth/host delimiter with lastIndexOf('@') instead of naive split so the segment
before the last '@' is treated as userinfo (username:password) and the segment
after as host:port; modify parseFalkorDBConnectionString to extract auth =
str.slice(0, lastAt) and hostPort = str.slice(lastAt + 1), then parse
username/password from auth (handling missing username or password) and parse
host/port from hostPort, and add a follow-up to update this test expectation
once the parser is fixed.

In @src/utils/connection-parser.test.ts around lines 264 - 275, The test reveals
parseFalkorDBConnectionString currently truncates passwords containing ':'
because it does auth.split(':') and only uses parts[0] and parts[1]; fix
parseFalkorDBConnectionString so when parsing the auth segment you treat the
first ':' as the user/password separator and reconstruct the password by joining
any remaining parts (e.g., parts.slice(1).join(':')) instead of dropping extras,
ensuring username = parts[0] and password = joined remainder.

@Claude
Copy link
Copy Markdown
Contributor

Claude AI commented Feb 9, 2026

@gkorland I've opened a new pull request, #16, to work on those changes. Once the pull request is ready, I'll request review from you.

Claude AI and others added 2 commits February 9, 2026 11:46
…ping, clean up Redis client on retry, fix connection parser edge cases

Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com>
Address PR review comments: sanitize query logging, fix MCP level mapping, clean up Redis client on retry, fix connection parser edge cases
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @.github/workflows/node.yml:
- Line 21: The CI matrix includes an unsupported Node.js version: update the
node-version array (the node-version key in the workflow) to remove "18.x" and
test only against actively supported releases—replace node-version: [18.x, 20.x,
22.x] with node-version: [22.x, 24.x] (or [22.x, 24.x] as suggested) so the
workflow runs on current LTS/maintenance versions.

In `@src/services/falkordb.service.ts`:
- Around line 57-79: When ping() fails after a successful FalkorDB.connect()
(this.client is set), ensure you close/cleanup the existing client before
retrying: inside the catch of _initialize(), before incrementing this.retryCount
and calling _initialize() again, check if this.client exists and call its
close/disconnect method (the same cleanup used in redis.service.ts) and null out
this.client to avoid leaking connections; then proceed with the existing retry
logging (logger.warn), backoff, and recursive call, keeping the AppError/logging
path unchanged when maxRetries is exceeded.

In `@src/services/redis.service.ts`:
- Around line 16-28: The bug is that retryCount is never reset so once it
reaches maxRetries future calls immediately fail; to fix, reset retryCount to 0
at the start of the initialization flow (e.g., set this.retryCount = 0 either at
the top of initialize() before calling this._initialize() or at the start of
_initialize()), then preserve existing retry loop logic that increments
retryCount and compares against maxRetries; update the RedisService methods
initialize() and/or _initialize() where retryCount and maxRetries are referenced
to ensure retryCount is reinitialized on each new initialization attempt.
🧹 Nitpick comments (6)
src/utils/connection-parser.ts (1)

52-58: IPv6 host addresses would break the host:port split.

An IPv6 literal like [::1]:6379 contains multiple colons, so hostPort.split(':') would mis-parse. This is a pre-existing limitation and likely out of scope for this PR, but worth noting for future work.

src/utils/connection-parser.test.ts (1)

227-249: Mocking global.parseInt is fragile — any internal use of parseInt before the target line will also throw.

This works today because parseInt happens to be the first throwable call in the code path after the early returns, but it's a brittle coupling. Consider testing the error/catch path by passing an input that naturally triggers an exception, or by using a more targeted mock. Low priority since it does exercise the catch branch.

src/services/logger.service.ts (2)

123-135: Consider extracting the duplicated error-context construction.

The error() and errorSync() methods share identical logic for building errorContext from an Error object (lines 124–132 and 156–164). A small private helper would reduce duplication.

♻️ Suggested refactor
+  private buildErrorContext(error?: Error, context?: LogContext): LogContext | undefined {
+    if (!error) return context;
+    return {
+      name: error.name,
+      message: error.message,
+      stack: error.stack,
+      ...(error as any).isOperational !== undefined && {
+        isOperational: (error as any).isOperational
+      },
+      ...context
+    };
+  }
+
   async error(message: string, error?: Error, context?: LogContext): Promise<void> {
-    const errorContext = error ? {
-      name: error.name,
-      message: error.message,
-      stack: error.stack,
-      ...(error as any).isOperational !== undefined && { 
-        isOperational: (error as any).isOperational 
-      },
-      ...context
-    } : context;
-
-    await this.log('ERROR', message, errorContext);
+    await this.log('ERROR', message, this.buildErrorContext(error, context));
   }

Apply the same to errorSync.

Also applies to: 155-168


64-78: Synchronous file I/O on every log call could become a bottleneck.

appendFileSync blocks the event loop. For an MCP stdio server with moderate log volume this is likely acceptable, but worth noting if throughput becomes a concern. An async write buffer or appendFile (async) would be the improvement path.

src/services/redis.service.ts (1)

155-192: listKeys using SCAN is a good pattern, but be aware of production risks.

Using SCAN with COUNT: 1000 is preferable to KEYS *, but on a Redis instance with millions of keys, the unbounded accumulation into allKeys could cause high memory usage. For the current use case this is likely fine, but worth noting if the key space grows significantly.

.github/workflows/node.yml (1)

78-84: Consider adding --provenance to npm publish for supply chain transparency.

npm provenance links published packages to their source commit and build, improving supply chain security. This is a low-effort addition:

     - name: Publish to NPM
-      run: npm publish
+      run: npm publish --provenance --access public
       env:
         NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

This requires id-token: write permission on the job:

  publish:
    name: Publish to NPM
    needs: [test]
    runs-on: ubuntu-latest
+   permissions:
+     contents: read
+     id-token: write

Comment thread .github/workflows/node.yml Outdated
Comment thread src/services/falkordb.service.ts Outdated
The MCP SDK's registerTool method has deeply nested generic type
parameters (OutputArgs and InputArgs) that exceed TypeScript's type
instantiation recursion limit. Add @ts-expect-error directive to
suppress this known SDK issue.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 35 changed files in this pull request and generated 10 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +16
import { redisService } from './redis.service';
import { AppError, CommonErrors } from '../errors/AppError.js';

// Mock the logger service
jest.mock('./logger.service.js', () => ({
logger: {
info: jest.fn().mockResolvedValue(undefined),
warn: jest.fn().mockResolvedValue(undefined),
error: jest.fn().mockResolvedValue(undefined),
debug: jest.fn().mockResolvedValue(undefined),
}
}));

// Mock the config
jest.mock('../config/index.js', () => ({
config: {
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file uses ESM Jest (ts-jest/presets/default-esm), but it imports redisService before declaring jest.mock(...) for redis, config, and logger. In ESM, static imports are evaluated before the module body runs, so these mocks won’t affect the already-imported service (and createClient will remain the real implementation). Convert these mocks to jest.unstable_mockModule(...) + dynamic await import(...), or move to dependency injection so the service can be tested without module-level mocking.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +26
import { ErrorHandler, errorHandler } from './ErrorHandler';
import { AppError, CommonErrors } from './AppError';

// Mock the logger service
jest.mock('../services/logger.service.js', () => ({
logger: {
error: jest.fn().mockResolvedValue(undefined),
info: jest.fn().mockResolvedValue(undefined),
errorSync: jest.fn(),
}
}));

// Get mock logger from the mocked module
let mockLogger: any;

describe('ErrorHandler', () => {
let handler: ErrorHandler;

beforeEach(async () => {
handler = new ErrorHandler();
jest.clearAllMocks();
// Import the mock logger
const loggerModule = await import('../services/logger.service.js');
mockLogger = loggerModule.logger;
});

Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ErrorHandler is imported before mocking ../services/logger.service.js. With Jest ESM, the logger import inside ErrorHandler will already be resolved to the real module, so assertions against mockLogger can be unreliable and the test may perform real logging. Use jest.unstable_mockModule('../services/logger.service.js', ...) and await import('./ErrorHandler') after setting up mocks.

Suggested change
import { ErrorHandler, errorHandler } from './ErrorHandler';
import { AppError, CommonErrors } from './AppError';
// Mock the logger service
jest.mock('../services/logger.service.js', () => ({
logger: {
error: jest.fn().mockResolvedValue(undefined),
info: jest.fn().mockResolvedValue(undefined),
errorSync: jest.fn(),
}
}));
// Get mock logger from the mocked module
let mockLogger: any;
describe('ErrorHandler', () => {
let handler: ErrorHandler;
beforeEach(async () => {
handler = new ErrorHandler();
jest.clearAllMocks();
// Import the mock logger
const loggerModule = await import('../services/logger.service.js');
mockLogger = loggerModule.logger;
});
import type { ErrorHandler as ErrorHandlerType } from './ErrorHandler';
import { AppError, CommonErrors } from './AppError';
// References to the ErrorHandler exports, populated after mocking logger.service
let ErrorHandler: { new (): ErrorHandlerType };
let errorHandler: ErrorHandlerType;
// Get mock logger from the mocked module
let mockLogger: any;
describe('ErrorHandler', () => {
let handler: ErrorHandlerType;
beforeAll(async () => {
// Mock the logger service before importing ErrorHandler
await jest.unstable_mockModule('../services/logger.service.js', () => ({
logger: {
error: jest.fn().mockResolvedValue(undefined),
info: jest.fn().mockResolvedValue(undefined),
errorSync: jest.fn(),
},
}));
// Import the mock logger and the ErrorHandler module after setting up the mock
const loggerModule = await import('../services/logger.service.js');
mockLogger = loggerModule.logger;
const errorHandlerModule = await import('./ErrorHandler');
ErrorHandler = errorHandlerModule.ErrorHandler;
errorHandler = errorHandlerModule.errorHandler;
});
beforeEach(() => {
handler = new ErrorHandler();
jest.clearAllMocks();
});

Copilot uses AI. Check for mistakes.
Comment thread src/index.ts Outdated
Comment on lines +27 to +41
it('should return default options for undefined input', () => {
// Act
const result = parseFalkorDBConnectionString(undefined as any);

// Assert
expect(result).toEqual({
host: 'localhost',
port: 6379
});
});

it('should return default options for null input', () => {
// Act
const result = parseFalkorDBConnectionString(null as any);

Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation explicitly handles undefined/null by returning defaults, but the public signature is parseFalkorDBConnectionString(connectionString: string). The tests currently work around this by casting to any. Consider widening the function signature to string | undefined | null so callers don’t need unsafe casts and the behavior is encoded in the type system.

Copilot uses AI. Check for mistakes.
Comment thread src/mcp/tools.ts Outdated
Comment on lines 1 to +16
import { falkorDBService } from './falkordb.service';
import { AppError, CommonErrors } from '../errors/AppError.js';

// Mock the logger service
jest.mock('./logger.service.js', () => ({
logger: {
info: jest.fn().mockResolvedValue(undefined),
warn: jest.fn().mockResolvedValue(undefined),
error: jest.fn().mockResolvedValue(undefined),
debug: jest.fn().mockResolvedValue(undefined),
}
}));

// Mock the config
jest.mock('../config/index.js', () => ({
config: {
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same ESM mocking issue as above: falkorDBService is imported before the jest.mock('falkordb'|config|logger) declarations. Under Jest ESM, these mocks won’t apply to the already-evaluated imports, so FalkorDB.connect will be the real implementation during tests. Use jest.unstable_mockModule(...) with dynamic import (or dependency injection) so the mocks are effective.

Copilot uses AI. Check for mistakes.
Comment thread README.md Outdated
Comment on lines +66 to +75
describe('sampleMCPClientConfig', () => {
it('should have correct structure and values', () => {
// Assert
expect(sampleMCPClientConfig).toBeDefined();
expect(sampleMCPClientConfig.defaultServer).toBe('falkordb');
expect(sampleMCPClientConfig.servers).toBeDefined();
expect(sampleMCPClientConfig.servers.falkordb).toBeDefined();
expect(sampleMCPClientConfig.servers.falkordb.url).toBe('http://localhost:3000/api/mcp');
expect(sampleMCPClientConfig.servers.falkordb.apiKey).toBe('your_api_key_here');
});
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sampleMCPClientConfig assertions expect an HTTP endpoint (http://localhost:3000/api/mcp) and an apiKey, but the server entrypoint in this PR is an MCP stdio server (no Express/HTTP routes anymore). These sample configs (and therefore this test) appear out of date with the new architecture; consider updating the model/sample to the stdio-based mcpServers { command, args, env } format (as shown in the README) or removing the legacy HTTP client config entirely.

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +119
describe('sampleMCPServerConfig', () => {
it('should have correct structure and values', () => {
// Assert
expect(sampleMCPServerConfig).toBeDefined();
expect(sampleMCPServerConfig.mcpServers).toBeDefined();
expect(sampleMCPServerConfig.mcpServers.falkordb).toBeDefined();
expect(sampleMCPServerConfig.mcpServers.falkordb.command).toBe('docker');
expect(sampleMCPServerConfig.mcpServers.falkordb.args).toBeInstanceOf(Array);
});

it('should have correct docker arguments', () => {
// Arrange
const expectedArgs = [
'run',
'-i',
'--rm',
'-p', '3000:3000',
'--env-file', '.env',
'falkordb-mcpserver',
'falkordb://host.docker.internal:6379'
];

// Assert
expect(sampleMCPServerConfig.mcpServers.falkordb.args).toEqual(expectedArgs);
});
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test validates a Docker-based sampleMCPServerConfig, but the PR removes the Dockerfile and states Docker-based build/publish flow is removed. Keeping a Docker sample config here will mislead users/tests into thinking Docker is still supported. Update the sample to a node/npx stdio-based config or reintroduce documented Docker support.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +22
async initialize(): Promise<void> {
if (this.initializingPromise) {
return this.initializingPromise;
}

this.initializingPromise = this._initialize();

Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RedisService.initialize() does not reset retryCount before starting a new initialization sequence. If a previous initialization attempt exhausts retries and throws, subsequent calls to initialize() will start with retryCount already at maxRetries and will fail immediately without retrying. Reset retryCount (and any other state) at the start of initialize() similar to FalkorDBService.initialize().

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/services/falkordb.service.ts (1)

77-78: Flat 5 s retry delay with no backoff or jitter; recursion could be replaced with a loop.

The hardcoded 5 000 ms constant gives 25 s of total wait time across 5 retries and makes all retrying clients hit the server simultaneously after a shared outage. Exponential backoff with jitter (e.g. min(5000 * 2^retryCount, 30000) + Math.random() * 1000) would improve resilience. Separately, rewriting the retry as a while/for loop avoids five nested async stack frames, is easier to follow, and sidesteps the (benign here) tail-call non-optimization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/falkordb.service.ts` around lines 77 - 78, Replace the recursive
retry in _initialize with an iterative loop that caps retries (e.g., 5 attempts)
and uses exponential backoff with jitter instead of a flat 5000ms sleep; compute
delay as Math.min(5000 * 2 ** retryCount, 30000) + Math.random() * 1000 between
attempts, await the delay, and retry within the loop rather than calling return
this._initialize() to avoid nested async stack frames and synchronized client
retries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/services/falkordb.service.ts`:
- Line 37: Several logger.* calls in this module are invoked fire-and-forget
even though logger methods return Promise<void>, which can lose log delivery
when an error is thrown (e.g., logger.error(...) is called then throw appError).
Update the failure paths to await the logger.error promise before throwing or
returning (ensure the enclosing function is async or properly awaits), and for
other critical log sites (tests or shutdown paths) either await logger.* or add
a short flush/await helper; if you intentionally want fire-and-forget, add a
clear comment at each logger call (e.g., above logger.info/warn/debug)
explaining that behavior. Reference the module-level logger instance (logger)
and the error path that throws appError to locate and fix the calls.
- Around line 20-33: Add an idempotency guard to initialize() so it does not
overwrite an already-connected client: before resetting retryCount or assigning
initializingPromise, check if this.client exists and is connected (or otherwise
healthy) and simply return; keep the existing concurrent-call guard
(initializingPromise) intact. This prevents _initialize() from calling
FalkorDB.connect() and replacing a live this.client; do not reset
this.retryCount when skipping work. Ensure the logic references initialize(),
_initialize(), this.client, initializingPromise and retryCount so the
early-return condition is applied in initialize() prior to starting a new
initialization attempt.

---

Nitpick comments:
In `@src/services/falkordb.service.ts`:
- Around line 77-78: Replace the recursive retry in _initialize with an
iterative loop that caps retries (e.g., 5 attempts) and uses exponential backoff
with jitter instead of a flat 5000ms sleep; compute delay as Math.min(5000 * 2
** retryCount, 30000) + Math.random() * 1000 between attempts, await the delay,
and retry within the loop rather than calling return this._initialize() to avoid
nested async stack frames and synchronized client retries.

Comment thread src/services/falkordb.service.ts
Comment thread src/services/falkordb.service.ts Outdated
gkorland and others added 3 commits February 23, 2026 06:36
- Prevent Cypher injection in prompt templates by using parameterized
  query examples ($query) instead of string interpolation
- Validate relationship_depth as a bounded number (1-3) instead of string
- Strip credentials from Redis URL before logging to prevent secret exposure
- Reset retryCount in RedisService.initialize() for consistency with FalkorDBService
- Read version from package.json instead of hardcoding in MCP server config
- Add confirmDelete safety parameter to delete_graph and delete_key tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add idempotency guard to initialize() to prevent overwriting a
  connected client and leaking connections
- Replace recursive retry in _initialize() with iterative loop using
  exponential backoff with jitter (5s * 2^attempt, capped at 30s)
- Await logger.error() before throw on error paths to ensure log
  delivery is not lost
- Add fire-and-forget comments to non-critical logger calls (info,
  debug, warn) documenting intentional behavior

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Handle async errorHandler.handleError() rejection in uncaughtException
  handler with void + catch to prevent secondary unhandled rejection
- Truncate query strings in tools.ts error logs to first 100 chars,
  matching the pattern already used in FalkorDBService
- Update README Node.js prerequisite from 16+ to 18+ to match
  package.json engines.node >=18.0.0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
README.md (1)

116-121: Add language specifiers to fenced code blocks.

Several fenced code blocks at lines 116, 131, 138, and 188 lack a language specifier, which markdownlint flags as MD040. Since these blocks contain natural-language example prompts and a directory tree respectively, consider using ```text to silence the lint warning and improve rendering.

Also applies to: 131-135, 138-142, 188-209

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 116 - 121, The README has multiple fenced code blocks
containing natural-language prompts and a directory tree (e.g. the blocks with
"Show me all people who know each other", "Find the shortest path between two
nodes", "What relationships does John have?", "Run a read-only query on the
replica instance" and the directory tree block) that lack a language specifier
and trigger MD040; update each offending triple-backtick fence to use ```text
(or another appropriate non-code language) so the blocks are annotated (e.g.,
change ``` to ```text for the prompt blocks and the directory tree) to silence
the lint warning and improve rendering.
src/index.ts (1)

97-114: Top-level startServer() promise is unhandled.

startServer() on line 114 is not awaited or .catch()-ed. While the internal try/catch calls gracefulShutdown (which exits the process), a theoretical rejection from gracefulShutdown itself could surface as an unhandled rejection. Adding a .catch makes this explicit and silences Node's --unhandled-rejections=strict mode.

Proposed fix
-startServer();
+startServer().catch(() => process.exit(1));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 97 - 114, The call to startServer() is left
unhandled and could surface an unhandled rejection (e.g., if gracefulShutdown
rejects); update the invocation of startServer to handle promise
rejections—either wrap in a top-level async IIFE and await it with try/catch or
append .catch(...) to startServer() that logs the error via logger.error and
invokes gracefulShutdown or process.exit; reference the startServer function and
gracefulShutdown in your fix so any rejection is explicitly caught and logged.
src/mcp/tools.ts (1)

22-38: Consider extracting repeated validation into a shared helper.

The empty-string check pattern (if (!value?.trim()) { throw new AppError(...) }) is repeated across 6+ tool handlers for graphName, query, and key. A small helper like assertNonEmpty(value, fieldName) would reduce duplication and ensure consistent error messages.

Also applies to: 71-87, 148-154, 214-220, 260-266, 298-304

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp/tools.ts` around lines 22 - 38, Extract the repeated empty-string
validation into a helper named assertNonEmpty(value, fieldName) that throws new
AppError(CommonErrors.INVALID_INPUT, `${fieldName} is required and cannot be
empty`, true); add this helper to src/mcp/tools.ts (module scope or exported)
and replace the repeated checks for graphName, query, and key inside the tool
handlers (the async handlers that currently do if (!graphName?.trim()) / if
(!query?.trim()) / if (!key?.trim())) with calls to assertNonEmpty(graphName,
'Graph name'), assertNonEmpty(query, 'Query'), and assertNonEmpty(key, 'Key')
respectively to remove duplication and ensure consistent error messages.
src/services/redis.service.ts (1)

44-94: Inconsistent retry strategy compared to falkordb.service.ts.

falkordb.service.ts uses an iterative for loop with exponential backoff + jitter (Math.min(5000 * 2 ** retryCount, 30000) + Math.random() * 1000), while this file uses recursion with a flat 5-second delay. Consider aligning the two services for consistent retry behavior and to avoid unbounded delays diverging between services.

♻️ Sketch: iterative retry with exponential backoff
   private async _initialize(): Promise<void> {
-    try {
-      logger.info('Attempting to connect to Redis', {
-        url: this.sanitizeUrl(config.redis.url),
-        attempt: this.retryCount + 1
-      });
-
-      this.client = createClient({
-        url: config.redis.url,
-        username: config.redis.username,
-        password: config.redis.password,
-      });
-
-      await this.client.connect();
-      await this.client.ping();
-
-      logger.info('Successfully connected to Redis');
-      this.retryCount = 0;
-    } catch (error) {
-      // Clean up failed client before retrying or throwing
-      if (this.client) {
-        try {
-          await this.client.disconnect();
-        } catch {
-          // Ignore disconnect errors
+    for (;; this.retryCount++) {
+      try {
+        logger.info('Attempting to connect to Redis', {
+          url: this.sanitizeUrl(config.redis.url),
+          attempt: this.retryCount + 1
+        });
+
+        this.client = createClient({
+          url: config.redis.url,
+          username: config.redis.username,
+          password: config.redis.password,
+        });
+
+        await this.client.connect();
+        await this.client.ping();
+
+        logger.info('Successfully connected to Redis');
+        this.retryCount = 0;
+        return;
+      } catch (error) {
+        if (this.client) {
+          try {
+            await this.client.disconnect();
+          } catch {
+            // Ignore disconnect errors
+          }
+          this.client = null;
         }
-        this.client = null;
-      }
 
-      if (this.retryCount < this.maxRetries) {
-        this.retryCount++;
-        logger.warn('Failed to connect to Redis, retrying...', {
-          attempt: this.retryCount,
-          maxRetries: this.maxRetries,
-          error: error instanceof Error ? error.message : String(error)
-        });
-
-        await new Promise(resolve => setTimeout(resolve, 5000));
-        return this._initialize();
-      } else {
-        const appError = new AppError(
-          CommonErrors.CONNECTION_FAILED,
-          `Failed to connect to Redis after ${this.maxRetries} attempts: ${error instanceof Error ? error.message : String(error)}`,
-          true
-        );
-
-        logger.error('Redis connection failed permanently', appError);
-        throw appError;
+        if (this.retryCount < this.maxRetries) {
+          logger.warn('Failed to connect to Redis, retrying...', {
+            attempt: this.retryCount + 1,
+            maxRetries: this.maxRetries,
+            error: error instanceof Error ? error.message : String(error)
+          });
+
+          const delay = Math.min(5000 * 2 ** this.retryCount, 30000) + Math.random() * 1000;
+          await new Promise(resolve => setTimeout(resolve, delay));
+        } else {
+          const appError = new AppError(
+            CommonErrors.CONNECTION_FAILED,
+            `Failed to connect to Redis after ${this.maxRetries} attempts: ${error instanceof Error ? error.message : String(error)}`,
+            true
+          );
+
+          await logger.error('Redis connection failed permanently', appError);
+          throw appError;
+        }
       }
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/redis.service.ts` around lines 44 - 94, The _initialize method
currently uses recursion and a flat 5s delay; change it to an iterative retry
loop (use for or while) that attempts connection up to maxRetries, uses the same
exponential backoff + jitter as falkordb.service.ts (delay = Math.min(5000 * 2
** retryCount, 30000) + Math.random() * 1000), and preserves existing behavior:
log attempts with sanitizeUrl, construct createClient and await connect/ping,
cleanly disconnect and nullify client on failure, increment retryCount per
attempt, reset retryCount on success, and throw the AppError after exhausting
retries; update references to this._initialize recursion to instead loop inside
_initialize and ensure logging and error payloads remain the same.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/services/redis.service.ts`:
- Around line 62-93: The catch block in _initialize handles permanent Redis
failure but calls logger.error(...) without awaiting it, risking lost async logs
before the subsequent throw; change the call to await logger.error('Redis
connection failed permanently', appError) so the log completes before throwing
the AppError (keep the rest of the cleanup/retry logic intact and ensure this
await remains inside the same async catch scope that references this.client,
this.retryCount, this.maxRetries, AppError and CommonErrors.CONNECTION_FAILED).
- Around line 29-42: The initialize() method can overwrite a live Redis client
when called a second time, leaking the existing connection; add an idempotency
guard that returns immediately if this.client is already set (e.g., at the start
of initialize()), and keep the existing initializingPromise logic to handle
concurrent inits; ensure _initialize() does not replace this.client if a client
was created in the meantime (re-check this.client before assigning) so the
existing client is never closed/overwritten.

---

Duplicate comments:
In `@README.md`:
- Line 56: The README's git clone instruction still points to the fork URL ("git
clone https://github.com/SecKatie/falkordb-mcpserver.git"); update that clone
command to the upstream repository URL (replace the SecKatie URL with the
upstream repo URL) so the README shows the canonical clone source; locate the
git clone line in README.md and swap the URL accordingly.

In `@src/mcp/prompts.ts`:
- Around line 4-5: Update the stale inline comment inside function
registerUserSetupPrompt to accurately describe what the function does: replace
the incorrect "// Register graph_list resource" comment with a brief, correct
comment such as "// Register user_setup prompt" (or similar) so it reflects that
registerUserSetupPrompt registers the user_setup prompt for the McpServer.

---

Nitpick comments:
In `@README.md`:
- Around line 116-121: The README has multiple fenced code blocks containing
natural-language prompts and a directory tree (e.g. the blocks with "Show me all
people who know each other", "Find the shortest path between two nodes", "What
relationships does John have?", "Run a read-only query on the replica instance"
and the directory tree block) that lack a language specifier and trigger MD040;
update each offending triple-backtick fence to use ```text (or another
appropriate non-code language) so the blocks are annotated (e.g., change ``` to
```text for the prompt blocks and the directory tree) to silence the lint
warning and improve rendering.

In `@src/index.ts`:
- Around line 97-114: The call to startServer() is left unhandled and could
surface an unhandled rejection (e.g., if gracefulShutdown rejects); update the
invocation of startServer to handle promise rejections—either wrap in a
top-level async IIFE and await it with try/catch or append .catch(...) to
startServer() that logs the error via logger.error and invokes gracefulShutdown
or process.exit; reference the startServer function and gracefulShutdown in your
fix so any rejection is explicitly caught and logged.

In `@src/mcp/tools.ts`:
- Around line 22-38: Extract the repeated empty-string validation into a helper
named assertNonEmpty(value, fieldName) that throws new
AppError(CommonErrors.INVALID_INPUT, `${fieldName} is required and cannot be
empty`, true); add this helper to src/mcp/tools.ts (module scope or exported)
and replace the repeated checks for graphName, query, and key inside the tool
handlers (the async handlers that currently do if (!graphName?.trim()) / if
(!query?.trim()) / if (!key?.trim())) with calls to assertNonEmpty(graphName,
'Graph name'), assertNonEmpty(query, 'Query'), and assertNonEmpty(key, 'Key')
respectively to remove duplication and ensure consistent error messages.

In `@src/services/redis.service.ts`:
- Around line 44-94: The _initialize method currently uses recursion and a flat
5s delay; change it to an iterative retry loop (use for or while) that attempts
connection up to maxRetries, uses the same exponential backoff + jitter as
falkordb.service.ts (delay = Math.min(5000 * 2 ** retryCount, 30000) +
Math.random() * 1000), and preserves existing behavior: log attempts with
sanitizeUrl, construct createClient and await connect/ping, cleanly disconnect
and nullify client on failure, increment retryCount per attempt, reset
retryCount on success, and throw the AppError after exhausting retries; update
references to this._initialize recursion to instead loop inside _initialize and
ensure logging and error payloads remain the same.

Comment on lines +29 to +42
async initialize(): Promise<void> {
if (this.initializingPromise) {
return this.initializingPromise;
}

this.retryCount = 0;
this.initializingPromise = this._initialize();

try {
await this.initializingPromise;
} finally {
this.initializingPromise = null;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing idempotency guard — calling initialize() twice leaks the connected client.

falkordb.service.ts has an if (this.client) { return; } guard (lines 22-24) to prevent overwriting a live client, but redis.service.ts does not. A second initialize() call after a successful connection will overwrite this.client on line 51 without closing the existing one.

🐛 Proposed fix
   async initialize(): Promise<void> {
+    // Idempotency guard: skip if already connected
+    if (this.client) {
+      return;
+    }
+
     if (this.initializingPromise) {
       return this.initializingPromise;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/redis.service.ts` around lines 29 - 42, The initialize() method
can overwrite a live Redis client when called a second time, leaking the
existing connection; add an idempotency guard that returns immediately if
this.client is already set (e.g., at the start of initialize()), and keep the
existing initializingPromise logic to handle concurrent inits; ensure
_initialize() does not replace this.client if a client was created in the
meantime (re-check this.client before assigning) so the existing client is never
closed/overwritten.

Comment thread src/services/redis.service.ts Outdated
Comment on lines +62 to +93
} catch (error) {
// Clean up failed client before retrying or throwing
if (this.client) {
try {
await this.client.disconnect();
} catch {
// Ignore disconnect errors
}
this.client = null;
}

if (this.retryCount < this.maxRetries) {
this.retryCount++;
logger.warn('Failed to connect to Redis, retrying...', {
attempt: this.retryCount,
maxRetries: this.maxRetries,
error: error instanceof Error ? error.message : String(error)
});

await new Promise(resolve => setTimeout(resolve, 5000));
return this._initialize();
} else {
const appError = new AppError(
CommonErrors.CONNECTION_FAILED,
`Failed to connect to Redis after ${this.maxRetries} attempts: ${error instanceof Error ? error.message : String(error)}`,
true
);

logger.error('Redis connection failed permanently', appError);
throw appError;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

logger.error not awaited before throw — log may be lost.

On line 90, logger.error(...) is fire-and-forget (no await), but the very next line throws. Since logger.error is async (it sends MCP notifications), the log may never complete. The equivalent path in falkordb.service.ts (line 95) correctly uses await logger.error(...).

Proposed fix
-        logger.error('Redis connection failed permanently', appError);
+        await logger.error('Redis connection failed permanently', appError);
         throw appError;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
// Clean up failed client before retrying or throwing
if (this.client) {
try {
await this.client.disconnect();
} catch {
// Ignore disconnect errors
}
this.client = null;
}
if (this.retryCount < this.maxRetries) {
this.retryCount++;
logger.warn('Failed to connect to Redis, retrying...', {
attempt: this.retryCount,
maxRetries: this.maxRetries,
error: error instanceof Error ? error.message : String(error)
});
await new Promise(resolve => setTimeout(resolve, 5000));
return this._initialize();
} else {
const appError = new AppError(
CommonErrors.CONNECTION_FAILED,
`Failed to connect to Redis after ${this.maxRetries} attempts: ${error instanceof Error ? error.message : String(error)}`,
true
);
logger.error('Redis connection failed permanently', appError);
throw appError;
}
}
} catch (error) {
// Clean up failed client before retrying or throwing
if (this.client) {
try {
await this.client.disconnect();
} catch {
// Ignore disconnect errors
}
this.client = null;
}
if (this.retryCount < this.maxRetries) {
this.retryCount++;
logger.warn('Failed to connect to Redis, retrying...', {
attempt: this.retryCount,
maxRetries: this.maxRetries,
error: error instanceof Error ? error.message : String(error)
});
await new Promise(resolve => setTimeout(resolve, 5000));
return this._initialize();
} else {
const appError = new AppError(
CommonErrors.CONNECTION_FAILED,
`Failed to connect to Redis after ${this.maxRetries} attempts: ${error instanceof Error ? error.message : String(error)}`,
true
);
await logger.error('Redis connection failed permanently', appError);
throw appError;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/redis.service.ts` around lines 62 - 93, The catch block in
_initialize handles permanent Redis failure but calls logger.error(...) without
awaiting it, risking lost async logs before the subsequent throw; change the
call to await logger.error('Redis connection failed permanently', appError) so
the log completes before throwing the AppError (keep the rest of the
cleanup/retry logic intact and ensure this await remains inside the same async
catch scope that references this.client, this.retryCount, this.maxRetries,
AppError and CommonErrors.CONNECTION_FAILED).

Add configurable transport mode via MCP_TRANSPORT env var:
- 'stdio' (default): existing stdio transport for AI client integration
- 'http': Streamable HTTP transport for remote/networked access

HTTP mode features:
- Session management with per-session McpServer instances
- API key authentication via Authorization Bearer header
- Configurable port via MCP_PORT env var
- Graceful HTTP server shutdown

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add idempotency guard in RedisService.initialize() to prevent
  overwriting a live client connection
- Replace recursive retry in _initialize() with iterative loop using
  exponential backoff with jitter (consistent with FalkorDBService)
- Await logger.error() before throw in all error paths to prevent
  lost log delivery
- Fix stale comment in prompts.ts (was 'graph_list resource', now
  'user_setup prompt')
- Fix git clone URL in README to point to upstream FalkorDB repo
- Add language specifiers to fenced code blocks (MD040)
- Add .catch() to startServer() to handle unhandled rejections

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 35 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/index.ts
Comment on lines +233 to +238
function readRequestBody(req: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
req.on('end', () => resolve(body));
req.on('error', reject);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readRequestBody buffers the entire request body with no size limit. For the HTTP transport this makes the server vulnerable to memory exhaustion via large POST bodies. Add a maximum body size (and abort/return 413 when exceeded) while reading the stream.

Suggested change
function readRequestBody(req: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
req.on('end', () => resolve(body));
req.on('error', reject);
const MAX_REQUEST_BODY_SIZE_BYTES = 1024 * 1024; // 1 MiB
function readRequestBody(req: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
let body = '';
let totalBytes = 0;
let finished = false;
const cleanup = (): void => {
req.off('data', onData);
req.off('end', onEnd);
req.off('error', onError);
};
const onError = (err: Error): void => {
if (finished) {
return;
}
finished = true;
cleanup();
reject(err);
};
const onData = (chunk: Buffer): void => {
if (finished) {
return;
}
totalBytes += chunk.length;
if (totalBytes > MAX_REQUEST_BODY_SIZE_BYTES) {
finished = true;
cleanup();
const error = new Error('Request body too large');
(error as any).statusCode = 413;
// Stop reading further data from the client.
req.destroy(error);
reject(error);
return;
}
body += chunk.toString();
};
const onEnd = (): void => {
if (finished) {
return;
}
finished = true;
cleanup();
resolve(body);
};
req.on('data', onData);
req.on('end', onEnd);
req.on('error', onError);

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +36
async initialize(): Promise<void> {
// Idempotency guard: don't overwrite an already-connected client
if (this.client) {
return;
}

if (this.initializingPromise) {
return this.initializingPromise;
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RedisService.initialize() is not idempotent: if it’s called again after a successful connection, initializingPromise is null so it will create a new client and overwrite this.client without closing the existing connection. Add a guard similar to FalkorDBService (e.g., return early when this.client is already connected / isOpen), or close the existing client before recreating it.

Copilot uses AI. Check for mistakes.
Comment thread src/index.ts
Comment on lines +151 to +153
// Read the request body
const body = await readRequestBody(req);
const parsedBody = JSON.parse(body);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON.parse(body) is executed without guarding for invalid/empty JSON. A malformed request will throw and can crash the HTTP transport handler (potentially taking down the process). Wrap parsing in try/catch and return a 400 response on parse failures.

Suggested change
// Read the request body
const body = await readRequestBody(req);
const parsedBody = JSON.parse(body);
// Read and parse the request body safely
let parsedBody: unknown;
try {
const body = await readRequestBody(req);
if (!body || body.toString().trim().length === 0) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Bad Request: Empty request body' }));
return;
}
parsedBody = JSON.parse(body);
} catch (err) {
logger.warn('Failed to parse JSON request body', { error: err instanceof Error ? err.message : String(err) });
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Bad Request: Invalid JSON payload' }));
return;
}

Copilot uses AI. Check for mistakes.
Comment thread src/index.ts
logging: {},
},
});
logger.setMcpServer(sessionServer);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The singleton logger is re-pointed to each newly-created sessionServer via logger.setMcpServer(sessionServer). With concurrent HTTP sessions this will route MCP log notifications to whichever session connected most recently (and potentially leak logs across sessions). Consider making the logger session-scoped (new Logger per session) or having Logger support multiple MCP servers keyed by session/transport.

Suggested change
logger.setMcpServer(sessionServer);

Copilot uses AI. Check for mistakes.
gkorland and others added 3 commits February 23, 2026 07:18
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add multi-stage Dockerfile (node:18-alpine, defaults to HTTP transport)
- Add .dockerignore
- Add Docker and docker-compose usage instructions to README

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@gkorland gkorland merged commit 0445c94 into main Feb 23, 2026
8 checks passed
@gkorland gkorland deleted the rebase branch February 23, 2026 05:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants