Skip to content

trace tool throws TypeError: Cannot read properties of undefined (reading 'toLowerCase') #2547

Description

@Syakaraka

Area

gitnexus (CLI / core / indexing / MCP server)

Summary

The MCP trace tool throws a raw TypeError: Cannot read properties of undefined (reading 'toLowerCase') while every other per-repo tool (context, impact, query, route_map, check, explain, list_repos) works against the same indexed repository. The root cause is a missing null/undefined guard in isTestFilePath() that is called during BFS traversal when nodes lacking a filePath property (Community, Process, etc.) are encountered.

Context

I'm running the GitNexus MCP server from a Docker deployment (gitnexus image, version 1.6.10-rc.19) and driving it from a remote MCP client. While smoke-testing every per-repo tool against one indexed repository, trace is the only tool that fails — and it fails with a low-level JS TypeError wrapped in a structured error envelope rather than returning a meaningful, actionable error message.

The other tools all succeed against the same repo:

Tool Result
list_repos ✅ returns repo info normally
context ✅ returns symbol context (impl, callers, methods)
impact ✅ upstream analysis works
query ✅ semantic search works
trace {status: "error", error: "Cannot read properties of undefined (reading 'toLowerCase')"}
explain ✅ returns (no taint layer, as expected without --pdg)
route_map ✅ returns route info
check ✅ detects import cycles

Because trace is the only failing tool and the error is a bare TypeError rather than a meaningful error message, this is a missing null/undefined guard — the index itself is clearly serviceable (every other tool reads it fine).

Expected behavior

trace should either return a path result, a no_path / not_found / ambiguous structured envelope, or — if a repo/symbol can't be resolved — a clear, actionable error message. It should never surface a raw TypeError: Cannot read properties of undefined (reading 'toLowerCase') to the MCP client.

Actual behavior

trace fails with:

{
  "status": "error",
  "error": "Cannot read properties of undefined (reading 'toLowerCase')",
  "from": { "name": "<symbolA>" },
  "to": { "name": "<symbolB>" },
  "suggestion": "The graph query failed — try gitnexus context <symbol> to see connections, or check if an interface bridges them."
}

The error is caught by trace()'s own try/catch and returned as a structured {status:'error'} envelope, but the error field contains a raw JS TypeError message — not an actionable description of what went wrong. This makes the error opaque to the MCP client and the user.

Steps to reproduce

Reproduced against the Docker-hosted server via remote MCP:

// MCP call (remote, Docker-hosted gitnexus 1.6.10-rc.19)
trace({
  "from": "<symbolA>",
  "to": "<symbolB>"
  // repo omitted — only one repo is indexed on that server
})

Result: {status: "error", error: "Cannot read properties of undefined (reading 'toLowerCase')"}

The TypeError occurs with any from/to symbol pair where BFS traversal reaches nodes that lack a filePath property.

Root cause (confirmed)

1. isTestFilePath() has no null/undefined guard on its filePath parameter

local-backend.ts:207-208 (identical in both rc.19 and rc.44):

export function isTestFilePath(filePath: string): boolean {
  const p = filePath.toLowerCase().replace(/\\/g, '/');
  return (
    p.includes('.test.') || p.includes('.spec.') || ...
  );
}

The TypeScript type annotation says string, but the function is called with a value that may be undefined at runtime (see point 2 below). Calling undefined.toLowerCase() throws exactly the observed TypeError: Cannot read properties of undefined (reading 'toLowerCase').

2. BFS row decoding can produce undefined filePath

In _traceImpl(), the BFS traversal decodes rows from LadybugDB:

local-backend.ts:4723 (rc.19: line 4663):

const filePath = (row.filePath ?? row[4]) as string;

If m.filePath is null/undefined in the DB (some node types — e.g. Community, Process — do not have a filePath property), both row.filePath and row[4] will be null/undefined. The ?? operator falls through for both, so filePath ends up as undefined. This filePath is then passed to isTestFilePath(filePath) at line 4744 (rc.19: 4682):

if (!includeTests && isTestFilePath(filePath)) continue;

3. Confirmed: a significant number of nodes in the DB have NULL filePath

Cypher query against the remote LadybugDB:

MATCH (m) WHERE m.filePath IS NULL RETURN count(m) AS cnt

Returns a large count — these are primarily Community and Process nodes, plus other node types that lack a filePath property. The BFS traversal reaches these nodes via CALLS/HAS_METHOD/MEMBER_OF edges, triggering the crash.

4. trace()'s try/catch catches the TypeError but returns an opaque error message

local-backend.ts:4534-4548:

private async trace(repo: RepoHandle, params: TraceParams): Promise<any> {
  try {
    return await this._traceImpl(repo, params);
  } catch (err: any) {
    return {
      status: 'error',
      error: (err instanceof Error ? err.message : String(err)) || 'Trace analysis failed',
      // ...
    };
  }
}

The try/catch does catch the TypeError, but err.message is the raw JS error text "Cannot read properties of undefined (reading 'toLowerCase')", which is opaque and unactionable. The error is not logged to stdout/stderr (server.ts's top-level catch is not reached), so docker logs shows nothing.

5. Registry data is valid — the "bad registry entry" hypothesis is disproved

The remote server's registry.json contains exactly one valid entry with a valid name field. With only one valid repo, resolveRepoFromCache(undefined) returns directly via the single-repo branch (this.repos.size === 1) without hitting any unguarded .toLowerCase() calls in the repo-resolution path.

6. Other unguarded .toLowerCase() calls in repo-resolution are not triggered

The repo-resolution path (resolveReporesolveRepoFromCache) has several unguarded .toLowerCase() calls on handle.name and repoParam, but these are only reached when:

  • repoParam is provided (line 1278: repoParam.toLowerCase()), OR
  • Multiple repos exist and name matching is needed (lines 1303, 1327: handle.name.toLowerCase()), OR
  • No repo match is found and the "Available: …" error list is built (lines 1136, 1140: h.name.toLowerCase()).

None of these conditions apply when there is exactly one repo and repoParam is omitted.

Environment

  • GitNexus version (deployed, remote Docker): 1.6.10-rc.19
  • GitNexus source reviewed: v1.6.10-rc.19 @ d43c479a and main @ 3c36ab90 (v1.6.10-rc.44)
  • Deployment: Docker image, MCP server reached over HTTP (Streamable HTTP / SSE) from a remote MCP client
  • OS (server): Linux (Docker container, node:22-bookworm-slim)
  • Node.js (server): v22.22.2
  • Client: remote MCP client

Suggested fix

1. Add a null guard to isTestFilePath() — the confirmed crash point:

export function isTestFilePath(filePath: string | null | undefined): boolean {
  if (!filePath || typeof filePath !== 'string') return false;
  const p = filePath.toLowerCase().replace(/\\/g, '/');
  // ...
}

2. Add a null guard to BFS row decoding for filePath:

const filePath = (row.filePath ?? row[4] ?? '') as string;

3. Add null guards to resolveRepo / resolveRepoFromCache for handle.name and repoParam — even though they are not the current crash point, they are defensive gaps that could cause similar TypeErrors under different conditions (multi-repo setups, malformed registry entries):

const paramLower = (repoParam ?? '').toLowerCase();
const handleNameLower = (handle.name ?? '').toLowerCase();

A defensive fix across all three locations would ensure that any undefined/null value produces a clear, actionable error or graceful fallback instead of a bare TypeError — and would make trace no longer uniquely fragile relative to its sibling tools.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions