Skip to content

feat(health): add XState connection health monitor with per-URL state#382

Open
polaz wants to merge 19 commits intomainfrom
feat/#364-bug-mcp-server-startup-blocked-when-gitlab-is-unre
Open

feat(health): add XState connection health monitor with per-URL state#382
polaz wants to merge 19 commits intomainfrom
feat/#364-bug-mcp-server-startup-blocked-when-gitlab-is-unre

Conversation

@polaz
Copy link
Copy Markdown
Member

@polaz polaz commented Apr 3, 2026

Summary

  • Add per-instance connection lifecycle management using XState v5 state machine (connecting → healthy | degraded | disconnected | failed)
  • Auto-reconnect with exponential backoff, bounded startup, disconnected mode with manage_context-only tool exposure
  • Comprehensive error classification, health probes, URL normalization, and consolidated InstanceRegistry normalization

Key Changes

Core: src/services/HealthMonitor.ts (new)

  • XState v5 state machine per instance URL with state callbacks
  • quickHealthCheck() — unauthenticated HEAD probe with skipAuth + rateLimit bypass, status < 500 = reachable
  • InitializationTimeoutError class for startup init timeout detection (used in performConnect)
  • Handler-level timeouts use plain Error with "timed out" message (distinct from init timeout, classified as transient by classifyError)
  • Auto-reconnect: exponential backoff 5s → 60s with ±10% jitter, HEALTH_CHECK_PROBE_MS for fast-path checks

Connection: src/services/ConnectionManager.ts

Handlers: src/handlers.ts

  • Health-aware tool gate: isInstanceReachable() for request-scoped URLs
  • bootstrapComplete set after init + introspection succeed, before cache rebuild
  • Late terminal bootstrap errors (auth/permanent) forwarded through timedOut guard
  • Disconnected manage_context fast-path — some actions (e.g. whoami) may attempt API calls that fail gracefully

Utils

  • normalizeInstanceUrl() — strip API suffixes, default ports, canonicalize origins via URL constructor
  • parseTimerMs() — clamp all timer env vars to MAX_SAFE_TIMEOUT_MS
  • skipAuth option on enhancedFetch — suppress auth headers + cookies case-insensitively
  • ProxyAgent: proxyTls.timeout for per-connection connect budget (not one-shot AbortSignal)
  • classifyError() — cause.code support, HTTP 408, narrowed timeout heuristic
  • computeDaysUntilExpiry() — calendar-valid date roundtrip check

Registry: src/registry-manager.ts + src/services/InstanceRegistry.ts

  • InstanceRegistry.normalizeUrl consolidated with shared normalizeInstanceUrl (eliminates port divergence)
  • Pool dispatcher lookups use normalized URLs
  • Context tools bypass tier/version filtering
  • aggregateFilterCounters() with reason→counter map
  • postProcessRelatedReferences() collapsed to single loop
  • Per-URL getFilterStats(instanceUrl?) — treats connecting as reachable (consistent with isUnreachableMode)

Config & Docs

  • GITLAB_TOOL_TIMEOUT_MS (default 120s) documented in README
  • Connection resilience section in README.md and README.md.in
  • Server keepAliveTimeout clamped to MAX_SAFE_TIMEOUT_MS - 5000 for headersTimeout gap

Testing

  • 4866 tests passing (148 suites)
  • HealthMonitor: state transitions, reconnect backoff, health probes, connecting-state isAnyInstanceHealthy
  • Handlers: CONNECTION_FAILED states, OAuth URL routing (isConnected/initialize/ensureIntrospected pinned),
    bootstrap timeout with clearInflight, timedOut guard (resolve + reject paths), manage_context health skip,
    connection tracker requestId forwarding
  • ConnectionManager: per-URL state, typed helpers, zero as any
  • URL: default port stripping, API suffix edge cases, malformed URL fallback
  • Server: keepAlive clamp boundary test at MAX_SAFE_TIMEOUT_MS
  • Build + lint clean

Related

Design Notes

OAuth fallback to GITLAB_BASE_URL: When OAuth is enabled but no per-request context is available (startup health checks, non-OAuth transports), the handler falls back to GITLAB_BASE_URL instead of returning an auth error. This is intentional — blocking health monitoring and tool-list initialization on missing context would degrade the startup experience. The fallback is safe because GITLAB_BASE_URL is always configured. Full URL threading is tracked in #379.

Dependencies

  • Yarn 4.12 → 4.13, hono 4.11.7 → 4.12.10 (CVE), all deps bumped

Closes #364

polaz added 3 commits April 3, 2026 19:27
Add per-instance connection lifecycle management using XState v5:

- State machine: connecting → healthy | degraded | disconnected | failed
- Auto-reconnect with exponential backoff (5s → 60s) and ±10% jitter
- Bounded startup via GITLAB_INIT_TIMEOUT_MS (default 5s)
- Health probes: unauthenticated HEAD /api/v4/version with skipAuth + rateLimit bypass
- Probe reachability: only 200/401/403/405 count as reachable (404/400 = misconfigured)
- Error classification: transient → auto-reconnect, auth/permanent → failed state
- InitializationTimeoutError class for robust timeout detection (replaces string matching)
- Late terminal bootstrap errors (auth/permanent) forwarded through timedOut guard
- Per-URL InstanceState in ConnectionManager with Map<string, InstanceState>
- normalizeInstanceUrl: strip API suffixes, default ports, canonicalize via URL constructor
- parseTimerMs: clamp all timer env vars to MAX_SAFE_TIMEOUT_MS (2^31-1)
- skipAuth option on enhancedFetch: suppresses auth headers + cookies case-insensitively
- Context tools bypass tier/version filtering via registry check
- bootstrapComplete flag replaces isConnected() for CONNECTION_FAILED guard
- Disconnected mode: only manage_context tools exposed, tools/list_changed broadcast
- Accurate error messages for OAuth-deferred mode (getInstanceInfo/getSchemaInfo)
- TokenScopeDetector helpers extracted, aggregateFilterCounters with reason→counter map
- postProcessRelatedReferences collapsed to single loop

Closes #364
- HealthMonitor: state transitions, reconnect backoff, health probes, callbacks
- Handlers: CONNECTION_FAILED with connecting/failed states, OAuth URL routing,
  bootstrap timeout with clearInflight, timedOut guard suppressing late reports,
  OAuth-scoped bootstrap-timeout test, never-settling timeout mocks
- ConnectionManager: per-URL state, typed helpers (internals/cmStatic/cmInternal),
  getRegisteredHandler, zero as-any in enhanced tests
- RegistryManager: context tool bypass, per-URL filter stats, description overrides
- URL: normalizeInstanceUrl with default port stripping, API suffix edge cases
- ToolAvailability: conservative >= 15.0 fallback for unknown tools
- Upgrade yarn 4.12.0 → 4.13.0 (binary + Dockerfile corepack prepare)
- Bump hono 4.11.7 → 4.12.10 (CVE fix)
- Update all remaining dependencies to latest versions
Copilot AI review requested due to automatic review settings April 3, 2026 16:30
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

Warning

Rate limit exceeded

@polaz has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 44 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 5 minutes and 44 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0f34fa84-3d33-402b-9e40-4ea515efb61a

📥 Commits

Reviewing files that changed from the base of the PR and between caab7f4 and c9175cd.

📒 Files selected for processing (3)
  • src/services/ToolAvailability.ts
  • src/utils/fetch.ts
  • tests/unit/handlers.test.ts
📝 Walkthrough

Walkthrough

Adds per-instance connection resilience: a new XState-based HealthMonitor with init timeout and exponential backoff/jitter, error classification and structured connection-failure payloads; refactors ConnectionManager/RegistryManager for per-URL InstanceState and URL normalization; handler bootstrap gating and disconnected fast-paths; config, fetch, server timeout clamping, tests, docs, and packaging bumps.

Changes

Cohort / File(s) Summary
Health monitor & core connection services
src/services/HealthMonitor.ts, src/services/ConnectionManager.ts, src/services/InstanceRegistry.ts
Adds HealthMonitor (per-instance XState actors, snapshots, backoff/jitter, reporting APIs). Refactors ConnectionManager to per-URL InstanceState with deduped init/introspection, new InstanceState type, per-instance getters, reinitialize/clearInflight/reset. InstanceRegistry now normalizes URLs via normalizeInstanceUrl.
Handlers & registry management
src/handlers.ts, src/registry-manager.ts
Handlers: bootstrap gating, one-shot health init guards, per-request effectiveInstanceUrl, disconnected fast-paths, guarded health reporting, condensed telemetry, and resetHandlersState(). RegistryManager: instance-aware cache building, per-URL refresh/invalidate, unreachable-mode behavior, and per-instance filter stats.
Health/error primitives & URL utils
src/utils/error-handler.ts, src/utils/url.ts
Adds classifyError, ErrorCategory, ConnectionFailedError, createConnectionFailedError, and normalizeInstanceUrl utility used across services; structured connection-failure payloads and error classification rules.
Fetch & token-scope detection
src/utils/fetch.ts, src/services/TokenScopeDetector.ts
Adds skipAuth fetch option and unauthenticated fetch behavior; adjusts proxy TLS/connect timeout wiring. TokenScopeDetector accepts optional baseUrl, improves expiry parsing/logging, and uses normalized URLs.
Tool availability & filtering
src/services/ToolAvailability.ts, src/registry-manager.ts
Adds isToolAvailableForInstance(...); treats version === 'unknown' as allow-all; centralizes exclusion reasons and refactors cache/filtering to produce per-instance results in unreachable/disconnected modes.
Configuration & server lifecycle
src/config.ts, src/server.ts
Introduces MAX_SAFE_TIMEOUT_MS, strict timer parsing helpers, connection timers (INIT_TIMEOUT_MS, reconnect/health), clamps keepAlive/headers timeouts, and shuts down HealthMonitor during graceful shutdown.
Packaging, tooling, docs, Docker
package.json, .yarnrc.yml, Dockerfile, README.md, README.md.in
Bumps Yarn to 4.13.0 and sets yarnPath; dependency updates (adds xstate, undici v8), Dockerfile copies .yarn/releases/; README/README.in document Connection Resilience.
Tests
tests/unit/services/HealthMonitor.test.ts, tests/unit/handlers.test.ts, tests/unit/RegistryManager.test.ts, tests/unit/...
Extensive new and updated unit tests covering HealthMonitor behavior, connection/timeouts, per-instance caching, normalizeInstanceUrl, fetch-config changes, handler bootstrap/timeouts, and many updated mocks.
Misc / exports & typings
src/config.ts, src/utils/error-handler.ts, various tests
New exports (timers, MAX_SAFE_TIMEOUT_MS, InstanceState, error types) and many signature changes for per-instance method variants across services.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler
    participant HealthMonitor
    participant ConnectionManager
    participant GitLab

    Client->>Handler: request (tools/list or callTool)
    Handler->>HealthMonitor: ensure monitoring / initialize(instanceUrl?)
    HealthMonitor->>HealthMonitor: spawn actor -> connecting
    HealthMonitor->>ConnectionManager: initialize(instanceUrl) / ensureIntrospected
    par Connect & introspect
        ConnectionManager->>GitLab: auth / introspection probes
    and Init timeout guard
        HealthMonitor->>HealthMonitor: enforce INIT_TIMEOUT_MS (Promise.race)
    end
    alt Initialized & healthy
        GitLab-->>ConnectionManager: introspection success
        ConnectionManager-->>HealthMonitor: initialized
        HealthMonitor->>Handler: onStateChange -> healthy
        Handler-->>Client: full tool list / perform tool
    else Timeout or transient failures
        HealthMonitor->>HealthMonitor: transition -> disconnected/degraded
        HealthMonitor->>HealthMonitor: schedule reconnect (backoff + jitter)
        HealthMonitor-->>Handler: onStateChange -> disconnected
        Handler-->>Client: manage_context only or CONNECTION_FAILED response
    else Auth/config permanent error
        HealthMonitor->>HealthMonitor: transition -> failed
        HealthMonitor-->>Handler: onStateChange -> failed
        Handler-->>Client: manage_context only / CONNECTION_FAILED (permanent)
    end

    rect rgba(100,150,200,0.5)
    Note over HealthMonitor,GitLab: Periodic health checks and tool success/failure reporting influence state transitions
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(health): add XState connection health monitor with per-URL state' clearly summarizes the main change: adding a health monitoring feature using XState for per-URL connection management.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing the XState health monitor implementation, key changes across multiple files, testing coverage, and design rationale.
Linked Issues check ✅ Passed The PR fully addresses the primary objectives from issue #364: adding initialization timeout (INIT_TIMEOUT_MS), bounded startup via HealthMonitor, auto-reconnect with exponential backoff, error classification, and proper server startup behavior when GitLab is unreachable.
Out of Scope Changes check ✅ Passed All code changes are directly related to the connection health monitoring feature. Dependency updates (Yarn 4.13.0, hono, xstate) and configuration updates (parseTimerMs, MAX_SAFE_TIMEOUT_MS) are necessary supporting changes for the health monitor implementation.
Docstring Coverage ✅ Passed Docstring coverage is 82.35% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#364-bug-mcp-server-startup-blocked-when-gitlab-is-unre

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

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 3, 2026

Test Coverage Report

Overall Coverage: 96.48%

Metric Percentage
Statements 95.85%
Branches 87.03%
Functions 94.65%
Lines 96.48%

View detailed coverage report

@coderabbitai coderabbitai bot added dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code labels Apr 3, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 3, 2026

Copy link
Copy Markdown

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

Introduces a per-instance connection health subsystem (XState v5) to prevent startup hangs when GitLab is unreachable, and to gate tool exposure/behavior based on per-URL reachability. This fits the MCP server’s reliability layer by making tools/list and tool calls resilient to connectivity and authentication failures.

Changes:

  • Add HealthMonitor (new service) with per-URL state machines, bounded initialization, periodic probes, and auto-reconnect backoff.
  • Refactor ConnectionManager to store isolated per-URL instance state (client/introspection/scopes) with normalized URL keys and inflight deduplication.
  • Update handlers/registry filtering to support disconnected mode (manage_context-only exposure) and improve error classification + structured CONNECTION_FAILED responses.

Reviewed changes

Copilot reviewed 23 out of 25 changed files in this pull request and generated no comments.

Show a summary per file
File Description
yarn.lock Updates dependencies (notably adds xstate, bumps SDK, undici, yarn, etc.).
package.json Aligns dependency versions and adds xstate; updates Yarn version pin.
Dockerfile Updates Yarn version usage and ensures Yarn release artifact is copied for builds.
.yarnrc.yml Pins Yarn via yarnPath to 4.13.0 release file.
README.md Documents connection resilience behavior and new env vars.
src/config.ts Adds timer parsing/clamping helper and new health-monitor env vars (init timeout, backoff, probes, thresholds).
src/utils/url.ts Adds normalizeInstanceUrl() for canonical per-instance URL keys.
src/utils/fetch.ts Adds skipAuth option for intentionally unauthenticated probes (health/version).
src/utils/error-handler.ts Adds connection-health error classification and a structured CONNECTION_FAILED error payload helper.
src/services/HealthMonitor.ts New XState-based per-URL health monitor (timeouts, probes, reconnect, callbacks).
src/services/ConnectionManager.ts Moves to per-URL InstanceState Map, normalizes keys, adds init dedup + stale-init protections, improves OAuth probe behavior.
src/services/ToolAvailability.ts Adds isToolAvailableForInstance() to avoid singleton/current-instance leakage.
src/services/TokenScopeDetector.ts Accepts explicit base URL, normalizes URLs, and refactors expiry/status handling.
src/registry-manager.ts Adds reachability-aware tool exposure (context-only mode) and per-URL filter stats; refactors cache build pipeline.
src/handlers.ts Integrates health gating, structured connection failures, bounded bootstrap/timeout behavior, and per-request URL selection/normalization.
src/server.ts Ensures health monitor is shut down during graceful shutdown.
tests/unit/utils/url.test.ts Adds unit tests for URL normalization (slashes, API suffixes, default ports, subpaths).
tests/unit/services/HealthMonitor.test.ts Adds extensive HealthMonitor state-machine and error classification tests.
tests/unit/services/ToolAvailability.test.ts Adds per-instance availability tests for tier/version gating.
tests/unit/services/ToolDescriptionOverrides.test.ts Updates mocks for new per-instance availability method.
tests/unit/services/ConnectionManagerEnhanced.test.ts Updates tests for per-URL state, OAuth probe via enhancedFetch(skipAuth), cache behaviors, and concurrency protections.
tests/unit/services/ConnectionManager.test.ts Updates unit tests for per-URL state, deduped introspection, and reinitialize rollback behavior.
tests/unit/RegistryManager.test.ts Updates tests for disconnected mode behavior, per-URL stats, and new availability mocking.

Copy link
Copy Markdown

@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: 6

Caution

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

⚠️ Outside diff range comments (1)
tests/unit/services/ConnectionManager.test.ts (1)

120-140: 🧹 Nitpick | 🔵 Trivial

Assert that refreshTokenScopes() forwards TEST_URL to scope detection.

These cases now seed per-instance state, but the mock never checks its argument. If the implementation regresses to the global base URL, this block still passes.

Minimal assertion to lock in the per-URL contract
     it('should return false when token detection fails (returns null)', async () => {
       injectInstanceState(manager, TEST_URL);
       mockDetectTokenScopes.mockResolvedValue(null);

       const result = await manager.refreshTokenScopes();
       expect(result).toBe(false);
+      expect(mockDetectTokenScopes).toHaveBeenCalledWith(TEST_URL);
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/services/ConnectionManager.test.ts` around lines 120 - 140, Update
the tests so they assert that refreshTokenScopes() forwards the per-instance
base URL to the scope detection mock: after calling manager.refreshTokenScopes()
in the relevant specs (the ones that call injectInstanceState(manager,
TEST_URL)), add an assertion that mockDetectTokenScopes was called with TEST_URL
(e.g., expect(mockDetectTokenScopes).toHaveBeenCalledWith(TEST_URL) or
equivalent). This ensures ConnectionManager.refreshTokenScopes() uses the
instance URL rather than a global base URL; reference the functions
ConnectionManager.refreshTokenScopes, injectInstanceState, and the mock
mockDetectTokenScopes to locate where to add the assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Line 476: The ProxyAgent constructor call is passing an unsupported connect
option which will crash under undici v8; locate the new undici.ProxyAgent({...})
invocation (the one that includes uri, requestTls, HEADERS_TIMEOUT_MS,
BODY_TIMEOUT_MS) and remove the connect: { timeout: CONNECT_TIMEOUT_MS } entry,
or instead move that timeout logic into the TLS/proxy config (e.g. apply
CONNECT_TIMEOUT_MS via requestTls or other supported agent/pool options) so only
supported options (uri, requestTls, headersTimeout, bodyTimeout) are passed to
ProxyAgent.

In `@src/config.ts`:
- Around line 345-352: The assignment that sets server.headersTimeout by adding
5000 to HTTP_KEEPALIVE_TIMEOUT_MS can overflow Node's 32-bit timer ceiling
(MAX_SAFE_TIMEOUT_MS); update the code that computes server.headersTimeout (the
place referencing HTTP_KEEPALIVE_TIMEOUT_MS and server.headersTimeout) to clamp
the result to MAX_SAFE_TIMEOUT_MS (reuse parseTimerMs or Math.min with
MAX_SAFE_TIMEOUT_MS) so the sum (HTTP_KEEPALIVE_TIMEOUT_MS + 5000) never exceeds
the MAX_SAFE_TIMEOUT_MS; apply the same clamping logic to the other similar
assignment mentioned (lines referencing headersTimeout at the other location) so
both places guard against timer overflow.

In `@src/handlers.ts`:
- Around line 512-516: The connection-info enrichment currently calls
connectionManager.getInstanceInfo(effectiveInstanceUrl) unguarded after setting
bootstrapComplete, which can throw and abort before executeTool() runs; make
this best-effort by moving the getInstanceInfo/logInfo logic behind the
LOG_FORMAT==='verbose' check (so it doesn't run in condensed mode) and wrap the
call in a try/catch that swallows/logs errors (do not rethrow or change
bootstrapComplete), calling logInfo only if getInstanceInfo returns
successfully; update references to bootstrapComplete,
connectionManager.getInstanceInfo, effectiveInstanceUrl, LOG_FORMAT, logInfo,
and executeTool accordingly.

In `@src/services/TokenScopeDetector.ts`:
- Around line 186-197: The computeDaysUntilExpiry function currently only checks
for NaN but allows out-of-range dates (e.g., month 13) because Date.UTC
normalizes them; update computeDaysUntilExpiry to validate parsed components:
ensure month is between 1 and 12 and day is at least 1 and no greater than the
maximum days for that month (accounting for leap years for February) before
calling Date.UTC; if any component is out of range return null so invalid inputs
like "2026-13-40" produce null rather than a normalized future date.

In `@src/services/ToolAvailability.ts`:
- Around line 487-509: The availability logic in isToolAvailableForInstance
duplicates the same action lookup, version gate, tier check, and unknown-tool
fallback already implemented in isToolAvailable; extract that shared predicate
into a single private helper (e.g., private static
toolAvailabilityPredicate/toolAvailabilityFor({toolName, instanceInfo, action}))
that encapsulates getActionRequirement, parseVersion checks, isTierSufficient
and the '>=15.0' fallback, then have both isToolAvailable and
isToolAvailableForInstance call that helper so the rules are centralized and
won’t drift; keep existing behavior and return types unchanged.

In `@tests/unit/utils/url.test.ts`:
- Around line 97-110: Add a unit test that exercises normalizeInstanceUrl's
try/catch fallback for malformed inputs by calling normalizeInstanceUrl with a
clearly malformed URL (e.g., 'not a url' or 'http://[::1') and assert the
function returns the graceful fallback value (e.g., the original input or the
documented sanitized fallback) to ensure the catch branch around new URL() is
covered; place this assertion alongside the existing canonical-port cases in the
same test suite so the malformed input path is directly covered.

---

Outside diff comments:
In `@tests/unit/services/ConnectionManager.test.ts`:
- Around line 120-140: Update the tests so they assert that refreshTokenScopes()
forwards the per-instance base URL to the scope detection mock: after calling
manager.refreshTokenScopes() in the relevant specs (the ones that call
injectInstanceState(manager, TEST_URL)), add an assertion that
mockDetectTokenScopes was called with TEST_URL (e.g.,
expect(mockDetectTokenScopes).toHaveBeenCalledWith(TEST_URL) or equivalent).
This ensures ConnectionManager.refreshTokenScopes() uses the instance URL rather
than a global base URL; reference the functions
ConnectionManager.refreshTokenScopes, injectInstanceState, and the mock
mockDetectTokenScopes to locate where to add the assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 572cc540-02fb-4415-9259-14d80ed8cc7d

📥 Commits

Reviewing files that changed from the base of the PR and between edea660 and ec09100.

⛔ Files ignored due to path filters (2)
  • .yarn/releases/yarn-4.13.0.cjs is excluded by !**/.yarn/**
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (23)
  • .yarnrc.yml
  • Dockerfile
  • README.md
  • package.json
  • src/config.ts
  • src/handlers.ts
  • src/registry-manager.ts
  • src/server.ts
  • src/services/ConnectionManager.ts
  • src/services/HealthMonitor.ts
  • src/services/TokenScopeDetector.ts
  • src/services/ToolAvailability.ts
  • src/utils/error-handler.ts
  • src/utils/fetch.ts
  • src/utils/url.ts
  • tests/unit/RegistryManager.test.ts
  • tests/unit/handlers.test.ts
  • tests/unit/services/ConnectionManager.test.ts
  • tests/unit/services/ConnectionManagerEnhanced.test.ts
  • tests/unit/services/HealthMonitor.test.ts
  • tests/unit/services/ToolAvailability.test.ts
  • tests/unit/services/ToolDescriptionOverrides.test.ts
  • tests/unit/utils/url.test.ts

…best-effort log

- Remove unsupported connect option from ProxyAgent (Undici v8 incompatibility)
- Clamp server.headersTimeout to MAX_SAFE_TIMEOUT_MS to prevent 32-bit overflow
- Make getInstanceInfo log enrichment best-effort (try/catch in verbose mode)
- Add month/day range validation in computeDaysUntilExpiry
- Add malformed URL fallback test, assert refreshTokenScopes URL forwarding
- Fix ProxyAgent test to not expect connect option
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 3, 2026

@coderabbitai Re: outside-diff ConnectionManager.test.ts:120-140 — Fixed: mock now forwards args via spread (...args) and test asserts mockDetectTokenScopes was called with TEST_URL.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

Tip

For best results, initiate chat on the files or code changes.

@polaz: Thanks for the update! Using ...args spread to forward arguments correctly and asserting mockDetectTokenScopes was called with TEST_URL is exactly the right fix — this ensures the mock accurately reflects real call-site behavior and the assertion validates the URL routing end-to-end.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T10:01:57.587Z
Learning: In `tests/unit/services/ConnectionManagerEnhanced.test.ts` (structured-world/gitlab-mcp), around line 1079, the `ensureIntrospected` cache-path test is already correct: the `getIntrospection` mock (set up around line 986) returns full cached data, and the test asserts both that `getIntrospection` was called and that the restored introspection data is present. Do not flag this test as missing cache-restoration assertions or as incorrectly set up in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T07:33:01.038Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `refreshTokenScopes()` (around lines 624-651) applies a post-await identity guard: after `await detectTokenScopes(url)` returns, it re-reads `this.instances.get(url)` and compares it to the `state` reference captured before the await. If a concurrent `reinitialize()` or `reset()` replaced the entry, it returns `false` instead of writing stale scope info into the new state. This mirrors the identity guard in `doInitialize`'s catch block. Do not flag the lack of this guard as a stale-write bug in future reviews — it is already in place.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T13:16:05.708Z
Learning: In tests/unit/handlers.test.ts (structured-world/gitlab-mcp), the OAuth URL test around Lines 1507-1555 now forces the bootstrap path by setting isConnected = false and asserts both initialize(oauthUrl) and ensureIntrospected(oauthUrl). This verifies explicit per-context URL routing through init and introspection. Do not re-flag this test as missing bootstrap forcing or URL-scoped assertions in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T08:14:19.833Z
Learning: In `tests/unit/services/ConnectionManagerEnhanced.test.ts` (structured-world/gitlab-mcp), private `ConnectionManager` internals are accessed via typed `cmStatic()` and `cmInternal()` helper functions (replacing prior `as any` casts). These helpers expose `instances`, `currentInstanceUrl`, `doInitialize`, and related private fields/methods to set up intermediate states unreachable via the public API. This typed-helper pattern is the accepted approach for this test file. Do not suggest reverting to `as any` or flag the private-field access as unsafe in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T15:17:47.982Z
Learning: In tests/unit/handlers.test.ts (structured-world/gitlab-mcp), when simulating a hanging operation, prefer a never-settling promise (`new Promise<never>(() => {})`) over timer-backed mocks to avoid delayed resolves leaking into subsequent tests. This pattern is now used around Lines 1255–1257.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T16:24:51.665Z
Learning: In tests/unit/handlers.test.ts (structured-world/gitlab-mcp), the deferred-init timedOut guard test around lines 1767–1800 now waits via a resolveExecuteReached barrier to ensure the executeTool dispatch is reached before asserting. This replaces the prior double process.nextTick and prevents scheduler-dependent flakiness. Do not suggest switching this test back to nextTick or fake timers in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: tests/unit/handlers.test.ts:686-704
Timestamp: 2026-04-02T18:35:51.180Z
Learning: In `tests/unit/handlers.test.ts` (structured-world/gitlab-mcp), the double `process.nextTick` pattern (`await new Promise(resolve => process.nextTick(resolve))` twice) is the intentional and correct approach for flushing XState microtasks in fire-and-forget callback tests. `jest.useFakeTimers()` conflicts with XState's internal scheduling, making it unsuitable here. Do not suggest replacing this pattern with fake timers or `waitFor` in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/ConnectionManager.ts:112-112
Timestamp: 2026-04-02T10:46:39.546Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `doInitialize()` (around line 112) has a cognitive complexity of ~34 (SonarCloud limit: 15) due to three interleaved init paths: OAuth best-effort unauthenticated version detection, REST-only token-scope path, and full GraphQL introspection. The maintainer (polaz) has explicitly decided NOT to extract helper methods in PR `#369` because the shared `state` mutations and identity guards would add indirection without reducing conceptual complexity. This is tracked for future refactoring. Do not re-flag this complexity issue in reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T15:31:04.797Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), the stale-init overwrite race in `doInitialize()` (lines ~117-163) is mitigated by identity guards: the outer `let state` binding + `this.instances.get(baseUrl) === state` check in the catch block (lines ~294-298) prevents a stale init from deleting a fresh state entry. A deeper fix using per-attempt init tokens (to prevent any intermediate writes from stale paths) requires significant refactoring across `doInitialize`/`clearInflight`/`reset` and is tracked in issue `#379`. Do not re-flag the stale-init race as unguarded in future reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/services/ConnectionManager.ts:65-67
Timestamp: 2026-04-03T14:50:27.051Z
Learning: In `src/utils/url.ts` (structured-world/gitlab-mcp), `normalizeInstanceUrl()` was updated (after PR `#380` review) to strip default ports (443 for https, 80 for http) using the URL constructor, so `https://host` and `https://host:443` map to the same entry in `ConnectionManager.instances` and inflight caches. When reviewing this function, verify: (1) `new URL()` call is wrapped in try/catch to handle malformed inputs gracefully, (2) API suffix stripping runs before or independently of port normalization to preserve subpaths for self-managed instances, (3) unit tests cover `https://host:443` → `https://host` and `http://host:80` → `http://host` cases.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: tests/unit/services/HealthMonitor.test.ts:752-757
Timestamp: 2026-03-30T04:44:46.218Z
Learning: In `tests/unit/services/HealthMonitor.test.ts` (structured-world/gitlab-mcp), the degraded-detection logic in `HealthMonitor` checks **both** `version` and `schemaInfo`. In the test "should handle getInstanceInfo throwing during health check" (around line 741-757), the default mock provides schema info, so even when `getInstanceInfo()` throws during the periodic health check the instance state can remain `'healthy'`. The loose assertion `expect(state === 'degraded' || state === 'healthy').toBe(true)` is intentionally correct for this mock setup — do not flag it as needing a stricter `'degraded'`-only assertion.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: tests/unit/handlers.test.ts:1451-1456
Timestamp: 2026-04-03T07:32:18.078Z
Learning: In `tests/unit/handlers.test.ts` (structured-world/gitlab-mcp), synchronous `require()` in `afterEach` blocks is the accepted pattern for restoring Jest mock values on module-level mocks (e.g., `jest.mock('../../src/oauth/index', ...)`). The project enforces a no-eslint-disable rule, so replacing `require()` with `dynamic import()` + an eslint-disable comment is not acceptable. Do not flag synchronous `require()` usage in Jest teardown blocks (`afterEach`/`afterAll`) as inconsistent or suggest converting it to dynamic imports in future reviews of test files in this repository.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:724-732
Timestamp: 2026-04-02T10:46:41.853Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `isAnyInstanceHealthy()` intentionally includes the `'connecting'` state in its truthy set (alongside `'healthy'` and `'degraded'`). During startup all instances are in `'connecting'`, and excluding it would cause `tools/list` to return context-only tools for the entire initialization window (~5s), which is bad UX. The handler-level `isInstanceReachable()` check is the real runtime gate that prevents tool execution against unreachable instances. Do not flag the inclusion of `'connecting'` in `isAnyInstanceHealthy()` as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:448-457
Timestamp: 2026-03-31T13:18:57.386Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the bootstrap `catch (initError)` block (inside the `try` that calls `connectionManager.initialize()` / `getClient()` / `ensureIntrospected()`) handles the case where `!connectionManager.isConnected(effectiveInstanceUrl)`. It calls `healthMonitor.reportError(effectiveInstanceUrl, initError)` (when `!timedOut`), logs the error, and then **returns** a `createConnectionFailedError(toolName, action, effectiveInstanceUrl, healthMonitor.getState(effectiveInstanceUrl))` structured `CONNECTION_FAILED` response (with `instance_url`, `reconnecting`, and `suggested_fix`) instead of throwing `"Bad Request: Server not initialized"`. Do not flag this bootstrap path as returning a generic error or as failing to report to HealthMonitor in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T11:32:28.956Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the first `handleStateChange` callback that fires before `InstanceRegistry` is initialized results in a silently dropped `updateConnectionStatus` call. This is documented in Known Limitations: the registry catches up on the next real state transition, and fixing the miss would require a pending-status queue — which is out of scope. Do not flag this first-transition registry miss as an unhandled bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:578-603
Timestamp: 2026-03-31T10:09:51.540Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `handleStateChange` intentionally early-returns before calling `registry.updateConnectionStatus` when the state hasn't changed (`previousState === newState`). This is by design: `updateConnectionStatus` is idempotent and calling it on every periodic health-check poll (e.g., `healthy.checking → healthy.idle`) would add unnecessary overhead with no functional benefit. Do not flag this early-return ordering as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/services/HealthMonitor.ts:112-206
Timestamp: 2026-04-03T00:40:13.151Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor function (around lines 112–206) has a cognitive complexity of ~17 (SonarCloud limit: 15) due to three interleaved phases: fast-path health check, full initialization with timeout, and degraded-path reachability verification. The maintainer (polaz) has explicitly declined extracting these into helpers because all three phases share a single `deadline` variable that defines the unified startup budget — splitting would obscure this invariant. A code comment was added above `performConnect` explaining the intentional complexity. Do not re-flag this complexity issue in future reviews of this function.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-03-31T07:51:52.620Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `isConnected(instanceUrl?: string)` is a per-URL initialization flag (reads `state?.isInitialized` from `Map<string, InstanceState>`), explicitly documented as NOT a live connectivity check. Use `HealthMonitor.isInstanceReachable()` for runtime reachability. Do not flag `isConnected()` as an unsafe health signal in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:100-108
Timestamp: 2026-03-29T18:10:49.416Z
Learning: In structured-world/gitlab-mcp, the `ConnectionManager.isConnected()` / `getInstanceInfo()` singleton path in `HealthMonitor.ts` (`performConnect`) is intentional for single-instance mode. Multi-instance OAuth contexts use `InstanceRegistry` for per-URL state. Do not flag this singleton usage as a bug in future reviews; the two modes have separate code paths by design.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:666-669
Timestamp: 2026-04-02T10:24:42.437Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `resolveUrl(instanceUrl?: string)` method and the state-mutation methods that call it (`reportSuccess`, `reportError`, `forceReconnect`) intentionally accept an optional `instanceUrl` with a fallback to `GITLAB_BASE_URL`. This zero-arg fallback is by design for test ergonomics and is documented by the test "should use default URL for reportSuccess/reportError" (around line 716–718 of `tests/unit/services/HealthMonitor.test.ts`). All production callers in `src/handlers.ts` already pass an explicit `instanceUrl`. Do not flag the optional parameter or the GITLAB_BASE_URL fallback as unsafe or requiring a required-parameter refactor in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:529-564
Timestamp: 2026-04-02T09:32:46.067Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `HealthMonitor` creates one XState actor per monitored instance URL. Actors are only created for instances registered via `InstanceRegistry` config (1–3 in practice), so there is no unbounded actor accumulation. A follow-up issue has already been created to track per-URL eviction/inactivity TTL for future multi-instance/OAuth scenarios. Do not flag actor accumulation as an active memory leak in reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T23:36:18.216Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor (around lines 184-196) skips the post-init health probe when the remaining budget is < 500ms. When the remaining time is near-zero, the instance is assumed reachable (since init already succeeded) to prevent a false `disconnected` transition caused by an artificially short probe timeout. Do not flag skipping the health probe in this near-zero budget path as incorrect in future reviews.

Copy link
Copy Markdown

@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

Caution

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

⚠️ Outside diff range comments (1)
tests/unit/server.test.ts (1)

95-108: ⚠️ Potential issue | 🟡 Minor

Mirror this new config export in the later re-mocks too.

The top-level mock is updated, but the jest.doMock('../../src/config', ...) factories later in this file replace the whole module and still omit MAX_SAFE_TIMEOUT_MS. Any dual-mode re-import from those blocks will see undefined here and drive headersTimeout to NaN.

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

In `@tests/unit/server.test.ts` around lines 95 - 108, The later
jest.doMock('../../src/config', ...) factories in tests/unit/server.test.ts omit
the MAX_SAFE_TIMEOUT_MS export, causing dual-mode re-imports to get undefined
and produce NaN for headersTimeout; update every jest.doMock('../../src/config',
...) factory to include MAX_SAFE_TIMEOUT_MS (value 2_147_483_647) alongside the
other mocked keys (SSE, STREAMABLE_HTTP, HOST, PORT, SSE_HEARTBEAT_MS,
HTTP_KEEPALIVE_TIMEOUT_MS, packageName, packageVersion, LOG_FORMAT, LOG_FILTER,
shouldSkipAccessLogRequest) so the mocked module shape matches the top-level
jest.mock and prevents NaN in headersTimeout.
♻️ Duplicate comments (1)
src/services/TokenScopeDetector.ts (1)

186-198: ⚠️ Potential issue | 🟡 Minor

Reject calendar-invalid expiry dates too.

2026-02-31 and non-leap 2025-02-29 still pass the 1..31 guard, and Date.UTC() normalizes them into March. This helper therefore still returns a day count instead of null for invalid input.

Suggested fix
 function computeDaysUntilExpiry(expiresAt: string | null): number | null {
   if (!expiresAt) return null;
   const [yearStr, monthStr, dayStr] = expiresAt.split('-');
   const year = Number(yearStr);
   const month = Number(monthStr);
   const day = Number(dayStr);
   if (Number.isNaN(year) || Number.isNaN(month) || Number.isNaN(day)) return null;
   if (month < 1 || month > 12 || day < 1 || day > 31) return null;
-  const expiryUtcMs = Date.UTC(year, month - 1, day);
+  const expiry = new Date(Date.UTC(year, month - 1, day));
+  if (
+    expiry.getUTCFullYear() !== year ||
+    expiry.getUTCMonth() !== month - 1 ||
+    expiry.getUTCDate() !== day
+  ) {
+    return null;
+  }
+  const expiryUtcMs = expiry.getTime();
   const now = new Date();
   const todayUtcMs = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
   return Math.ceil((expiryUtcMs - todayUtcMs) / (1000 * 60 * 60 * 24));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/TokenScopeDetector.ts` around lines 186 - 198,
computeDaysUntilExpiry currently accepts syntactically valid but
calendar-invalid dates like "2026-02-31" because Date.UTC normalizes overflowed
days; update computeDaysUntilExpiry to detect and reject calendar-invalid dates
by constructing the UTC date (using Date.UTC(year, month-1, day) or new
Date(...)) and then verifying the resulting UTC year, month and date match the
parsed year, month-1 and day; if they don't match return null before computing
expiryUtcMs and remaining days.
🤖 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/handlers.ts`:
- Around line 707-713: Replace the plain Error sent to
HealthMonitor.getInstance().reportError with the typed
InitializationTimeoutError so the HealthMonitor classifies this as an
init-timeout; specifically, in the timeout branch that currently calls
reportError(requestInstanceUrl, new Error(`Initialization timed out after
${HANDLER_TIMEOUT_MS}ms`)), construct and pass new
InitializationTimeoutError(`Initialization timed out after
${HANDLER_TIMEOUT_MS}ms`) (and add the necessary import for
InitializationTimeoutError), leaving the subsequent
ConnectionManager.getInstance().clearInflight(requestInstanceUrl) call
unchanged.

In `@src/server.ts`:
- Around line 203-205: server.headersTimeout can become equal to
server.keepAliveTimeout when HTTP_KEEPALIVE_TIMEOUT_MS equals
MAX_SAFE_TIMEOUT_MS, which drops the 5s safety margin; change the assignment to
compute a desiredHeaders = Math.min(HTTP_KEEPALIVE_TIMEOUT_MS + 5000,
MAX_SAFE_TIMEOUT_MS) and then ensure it's strictly greater than
HTTP_KEEPALIVE_TIMEOUT_MS (e.g., if desiredHeaders <= HTTP_KEEPALIVE_TIMEOUT_MS
set desiredHeaders = Math.min(HTTP_KEEPALIVE_TIMEOUT_MS + 1,
MAX_SAFE_TIMEOUT_MS)), then assign server.headersTimeout = desiredHeaders;
reference server.keepAliveTimeout, server.headersTimeout,
HTTP_KEEPALIVE_TIMEOUT_MS and MAX_SAFE_TIMEOUT_MS when making the change.

---

Outside diff comments:
In `@tests/unit/server.test.ts`:
- Around line 95-108: The later jest.doMock('../../src/config', ...) factories
in tests/unit/server.test.ts omit the MAX_SAFE_TIMEOUT_MS export, causing
dual-mode re-imports to get undefined and produce NaN for headersTimeout; update
every jest.doMock('../../src/config', ...) factory to include
MAX_SAFE_TIMEOUT_MS (value 2_147_483_647) alongside the other mocked keys (SSE,
STREAMABLE_HTTP, HOST, PORT, SSE_HEARTBEAT_MS, HTTP_KEEPALIVE_TIMEOUT_MS,
packageName, packageVersion, LOG_FORMAT, LOG_FILTER, shouldSkipAccessLogRequest)
so the mocked module shape matches the top-level jest.mock and prevents NaN in
headersTimeout.

---

Duplicate comments:
In `@src/services/TokenScopeDetector.ts`:
- Around line 186-198: computeDaysUntilExpiry currently accepts syntactically
valid but calendar-invalid dates like "2026-02-31" because Date.UTC normalizes
overflowed days; update computeDaysUntilExpiry to detect and reject
calendar-invalid dates by constructing the UTC date (using Date.UTC(year,
month-1, day) or new Date(...)) and then verifying the resulting UTC year, month
and date match the parsed year, month-1 and day; if they don't match return null
before computing expiryUtcMs and remaining days.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 89a611ed-0e20-47bf-859e-f43a15de1159

📥 Commits

Reviewing files that changed from the base of the PR and between ec09100 and f9f63b3.

📒 Files selected for processing (10)
  • src/config.ts
  • src/handlers.ts
  • src/server.ts
  • src/services/TokenScopeDetector.ts
  • src/utils/fetch.ts
  • tests/unit/handlers.test.ts
  • tests/unit/server.test.ts
  • tests/unit/services/ConnectionManager.test.ts
  • tests/unit/utils/fetch-config.test.ts
  • tests/unit/utils/url.test.ts

…te validation

- Use InitializationTimeoutError in handler timeout path (was plain Error)
- Clamp keepAliveTimeout to MAX_SAFE_TIMEOUT_MS - 5000 to preserve mandatory
  headersTimeout gap at the 32-bit timer ceiling
- Validate calendar-correctness of expiry dates via Date roundtrip (rejects
  Feb 31, Feb 29 on non-leap years)
- Add MAX_SAFE_TIMEOUT_MS to all 6 jest.doMock config re-mocks in server tests
- Re-export InitializationTimeoutError through HealthMonitor mock in handler tests
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 3, 2026

@coderabbitai Review body items addressed:

Duplicate — TokenScopeDetector.ts:186-198: Fixed — added Date roundtrip validation to reject calendar-invalid dates (Feb 31, Feb 29 on non-leap years).

Outside-diff — server.test.ts:95-108: Fixed — added MAX_SAFE_TIMEOUT_MS: 2_147_483_647 to all 6 jest.doMock config re-mocks (lines 217, 326, 351, 2353, 2396, 2424).

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

Tip

For best results, initiate chat on the files or code changes.

@polaz: Both fixes look solid — thanks for the detailed callouts!

  • TokenScopeDetector.ts Date roundtrip validation — rejecting calendar-invalid dates (e.g. Feb 31, Feb 29 on non-leap years) via a roundtrip check is exactly the right approach; it catches what a raw field-range guard alone can't.
  • MAX_SAFE_TIMEOUT_MS in all 6 jest.doMock re-mocks — good catch on the full coverage across lines 217, 326, 351, 2353, 2396, and 2424; missing it in even one re-mock would cause intermittent test failures as the constant gets referenced post-re-mock.

🧠 Learnings used
Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T07:33:01.038Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `refreshTokenScopes()` (around lines 624-651) applies a post-await identity guard: after `await detectTokenScopes(url)` returns, it re-reads `this.instances.get(url)` and compares it to the `state` reference captured before the await. If a concurrent `reinitialize()` or `reset()` replaced the entry, it returns `false` instead of writing stale scope info into the new state. This mirrors the identity guard in `doInitialize`'s catch block. Do not flag the lack of this guard as a stale-write bug in future reviews — it is already in place.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T10:01:57.587Z
Learning: In `tests/unit/services/ConnectionManagerEnhanced.test.ts` (structured-world/gitlab-mcp), around line 1079, the `ensureIntrospected` cache-path test is already correct: the `getIntrospection` mock (set up around line 986) returns full cached data, and the test asserts both that `getIntrospection` was called and that the restored introspection data is present. Do not flag this test as missing cache-restoration assertions or as incorrectly set up in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T16:24:51.665Z
Learning: In tests/unit/handlers.test.ts (structured-world/gitlab-mcp), the deferred-init timedOut guard test around lines 1767–1800 now waits via a resolveExecuteReached barrier to ensure the executeTool dispatch is reached before asserting. This replaces the prior double process.nextTick and prevents scheduler-dependent flakiness. Do not suggest switching this test back to nextTick or fake timers in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:86-95
Timestamp: 2026-03-31T07:15:54.557Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `calculateBackoffDelay` intentionally assumes `RECONNECT_BASE_DELAY_MS <= RECONNECT_MAX_DELAY_MS`. When the config is invalid (BASE > MAX), the function returns BASE as a constant delay. This is treated as a config validation concern, not a runtime bug; a doc note was added to the function rather than adding runtime normalization. Do not flag the missing bounds-swap as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T08:14:19.833Z
Learning: In `tests/unit/services/ConnectionManagerEnhanced.test.ts` (structured-world/gitlab-mcp), private `ConnectionManager` internals are accessed via typed `cmStatic()` and `cmInternal()` helper functions (replacing prior `as any` casts). These helpers expose `instances`, `currentInstanceUrl`, `doInitialize`, and related private fields/methods to set up intermediate states unreachable via the public API. This typed-helper pattern is the accepted approach for this test file. Do not suggest reverting to `as any` or flag the private-field access as unsafe in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: tests/unit/services/HealthMonitor.test.ts:752-757
Timestamp: 2026-03-30T04:44:46.218Z
Learning: In `tests/unit/services/HealthMonitor.test.ts` (structured-world/gitlab-mcp), the degraded-detection logic in `HealthMonitor` checks **both** `version` and `schemaInfo`. In the test "should handle getInstanceInfo throwing during health check" (around line 741-757), the default mock provides schema info, so even when `getInstanceInfo()` throws during the periodic health check the instance state can remain `'healthy'`. The loose assertion `expect(state === 'degraded' || state === 'healthy').toBe(true)` is intentionally correct for this mock setup — do not flag it as needing a stricter `'degraded'`-only assertion.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T13:16:05.708Z
Learning: In tests/unit/handlers.test.ts (structured-world/gitlab-mcp), the OAuth URL test around Lines 1507-1555 now forces the bootstrap path by setting isConnected = false and asserts both initialize(oauthUrl) and ensureIntrospected(oauthUrl). This verifies explicit per-context URL routing through init and introspection. Do not re-flag this test as missing bootstrap forcing or URL-scoped assertions in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T15:17:47.982Z
Learning: In tests/unit/handlers.test.ts (structured-world/gitlab-mcp), when simulating a hanging operation, prefer a never-settling promise (`new Promise<never>(() => {})`) over timer-backed mocks to avoid delayed resolves leaking into subsequent tests. This pattern is now used around Lines 1255–1257.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T15:31:04.797Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), the stale-init overwrite race in `doInitialize()` (lines ~117-163) is mitigated by identity guards: the outer `let state` binding + `this.instances.get(baseUrl) === state` check in the catch block (lines ~294-298) prevents a stale init from deleting a fresh state entry. A deeper fix using per-attempt init tokens (to prevent any intermediate writes from stale paths) requires significant refactoring across `doInitialize`/`clearInflight`/`reset` and is tracked in issue `#379`. Do not re-flag the stale-init race as unguarded in future reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:100-108
Timestamp: 2026-03-29T18:10:49.416Z
Learning: In structured-world/gitlab-mcp, the `ConnectionManager.isConnected()` / `getInstanceInfo()` singleton path in `HealthMonitor.ts` (`performConnect`) is intentional for single-instance mode. Multi-instance OAuth contexts use `InstanceRegistry` for per-URL state. Do not flag this singleton usage as a bug in future reviews; the two modes have separate code paths by design.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: tests/unit/handlers.test.ts:686-704
Timestamp: 2026-04-02T18:35:51.180Z
Learning: In `tests/unit/handlers.test.ts` (structured-world/gitlab-mcp), the double `process.nextTick` pattern (`await new Promise(resolve => process.nextTick(resolve))` twice) is the intentional and correct approach for flushing XState microtasks in fire-and-forget callback tests. `jest.useFakeTimers()` conflicts with XState's internal scheduling, making it unsuitable here. Do not suggest replacing this pattern with fake timers or `waitFor` in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T00:43:19.144Z
Learning: In `src/config.ts` (structured-world/gitlab-mcp), rate-limit environment variables are parsed with two different helpers: duration/window vars (e.g., `RATE_LIMIT_IP_WINDOW_MS`, `RATE_LIMIT_SESSION_WINDOW_MS`) use `parseTimerMs()` (with upper ceiling and finite-integer validation), while max-request count vars (e.g., `RATE_LIMIT_IP_MAX_REQUESTS`, `RATE_LIMIT_SESSION_MAX_REQUESTS`) use a separate `parsePositiveInt()` guard. Do not suggest applying `parseTimerMs()` to max-request (count) vars in future reviews — timer semantics do not apply to request counts.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/handlers.ts:462-466
Timestamp: 2026-04-03T00:39:40.483Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the two-step `hasToolHandler()`/`executeTool()` pattern at the disconnected `manage_context` bypass (around line 462) and the main dispatch path (around lines 595-616) has an acknowledged TOCTOU gap: a concurrent `refreshCache()` swap between the two calls can make `executeTool()` miss a handler. This is intentionally benign because `executeTool` falls through to the bootstrap path when the handler is not found post-refresh. An atomic `getTool()` refactor to eliminate the gap is tracked separately. Do not re-flag this two-step pattern as a TOCTOU race in future reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: tests/unit/handlers.test.ts:1451-1456
Timestamp: 2026-04-03T07:32:18.078Z
Learning: In `tests/unit/handlers.test.ts` (structured-world/gitlab-mcp), synchronous `require()` in `afterEach` blocks is the accepted pattern for restoring Jest mock values on module-level mocks (e.g., `jest.mock('../../src/oauth/index', ...)`). The project enforces a no-eslint-disable rule, so replacing `require()` with `dynamic import()` + an eslint-disable comment is not acceptable. Do not flag synchronous `require()` usage in Jest teardown blocks (`afterEach`/`afterAll`) as inconsistent or suggest converting it to dynamic imports in future reviews of test files in this repository.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:724-732
Timestamp: 2026-04-02T10:46:41.853Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `isAnyInstanceHealthy()` intentionally includes the `'connecting'` state in its truthy set (alongside `'healthy'` and `'degraded'`). During startup all instances are in `'connecting'`, and excluding it would cause `tools/list` to return context-only tools for the entire initialization window (~5s), which is bad UX. The handler-level `isInstanceReachable()` check is the real runtime gate that prevents tool execution against unreachable instances. Do not flag the inclusion of `'connecting'` in `isAnyInstanceHealthy()` as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:448-457
Timestamp: 2026-03-31T13:18:57.386Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the bootstrap `catch (initError)` block (inside the `try` that calls `connectionManager.initialize()` / `getClient()` / `ensureIntrospected()`) handles the case where `!connectionManager.isConnected(effectiveInstanceUrl)`. It calls `healthMonitor.reportError(effectiveInstanceUrl, initError)` (when `!timedOut`), logs the error, and then **returns** a `createConnectionFailedError(toolName, action, effectiveInstanceUrl, healthMonitor.getState(effectiveInstanceUrl))` structured `CONNECTION_FAILED` response (with `instance_url`, `reconnecting`, and `suggested_fix`) instead of throwing `"Bad Request: Server not initialized"`. Do not flag this bootstrap path as returning a generic error or as failing to report to HealthMonitor in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-03-31T07:51:52.620Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `isConnected(instanceUrl?: string)` is a per-URL initialization flag (reads `state?.isInitialized` from `Map<string, InstanceState>`), explicitly documented as NOT a live connectivity check. Use `HealthMonitor.isInstanceReachable()` for runtime reachability. Do not flag `isConnected()` as an unsafe health signal in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T11:32:28.956Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the first `handleStateChange` callback that fires before `InstanceRegistry` is initialized results in a silently dropped `updateConnectionStatus` call. This is documented in Known Limitations: the registry catches up on the next real state transition, and fixing the miss would require a pending-status queue — which is out of scope. Do not flag this first-transition registry miss as an unhandled bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:666-669
Timestamp: 2026-04-02T10:24:42.437Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `resolveUrl(instanceUrl?: string)` method and the state-mutation methods that call it (`reportSuccess`, `reportError`, `forceReconnect`) intentionally accept an optional `instanceUrl` with a fallback to `GITLAB_BASE_URL`. This zero-arg fallback is by design for test ergonomics and is documented by the test "should use default URL for reportSuccess/reportError" (around line 716–718 of `tests/unit/services/HealthMonitor.test.ts`). All production callers in `src/handlers.ts` already pass an explicit `instanceUrl`. Do not flag the optional parameter or the GITLAB_BASE_URL fallback as unsafe or requiring a required-parameter refactor in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:578-603
Timestamp: 2026-03-31T10:09:51.540Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `handleStateChange` intentionally early-returns before calling `registry.updateConnectionStatus` when the state hasn't changed (`previousState === newState`). This is by design: `updateConnectionStatus` is idempotent and calling it on every periodic health-check poll (e.g., `healthy.checking → healthy.idle`) would add unnecessary overhead with no functional benefit. Do not flag this early-return ordering as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T23:36:18.216Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor (around lines 184-196) skips the post-init health probe when the remaining budget is < 500ms. When the remaining time is near-zero, the instance is assumed reachable (since init already succeeded) to prevent a false `disconnected` transition caused by an artificially short probe timeout. Do not flag skipping the health probe in this near-zero budget path as incorrect in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-03-31T07:51:52.620Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `clearInflight(instanceUrl: string)` (around line 761) removes the inflight `initializePromises` entry for a URL. HealthMonitor calls this after an init timeout so reconnect attempts start a fresh `doInitialize()` instead of re-awaiting the hung promise. The `initialize()` method's `finally` block guards `initializePromises.delete(url)` with an identity check to prevent a retry's fresh promise from being wiped by a stale `clearInflight` call.

Copy link
Copy Markdown

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
tests/unit/server.test.ts (1)

95-108: ⚠️ Potential issue | 🟡 Minor

Add assertions for the new clamp and shutdown paths.

Updating the mocks keeps this suite loading, but it still doesn't exercise the new configureServerTimeouts() boundary behavior or the new HealthMonitor branch in gracefulShutdown(). A regression in either code path would still pass here today.

As per coding guidelines, "Use Jest for all tests and ensure all features have corresponding test coverage".

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

In `@tests/unit/server.test.ts` around lines 95 - 108, The test currently mocks
config but doesn't assert the new timeout clamping or the alternate
gracefulShutdown flow; update tests to exercise configureServerTimeouts() by
setting config values to trigger clamping (e.g., set HTTP_KEEPALIVE_TIMEOUT_MS
and SSE_HEARTBEAT_MS above/below MAX_SAFE_TIMEOUT_MS) and assert the resulting
server timeout values are clamped as expected, and exercise gracefulShutdown()
by mocking or instantiating HealthMonitor to return both healthy/unhealthy paths
(or stub its methods) and assert the shutdown branch behaviors (calls made,
timers cleared, or different exit logic). Use the existing jest.mock for config
and add spies/mocks for HealthMonitor and the functions
configureServerTimeouts() and gracefulShutdown() to verify the new control-flow
branches are executed and their side effects asserted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unit/handlers.test.ts`:
- Around line 1465-1468: The test currently resets the mock and calls
setupHandlers(mockServer) a second time but never asserts the retry actually
happened; update the test to assert that mockHealthMonitor.initialize was
invoked again (e.g.,
expect(mockHealthMonitor.initialize).toHaveBeenCalledTimes(2) or
expect(mockHealthMonitor.initialize).toHaveBeenCalledWith(...) after the second
call) so the retry path from setupHandlers is verified; locate the assertions
around mockHealthMonitor.initialize and setupHandlers in the failing test and
add the call-count assertion immediately after the second await.

---

Outside diff comments:
In `@tests/unit/server.test.ts`:
- Around line 95-108: The test currently mocks config but doesn't assert the new
timeout clamping or the alternate gracefulShutdown flow; update tests to
exercise configureServerTimeouts() by setting config values to trigger clamping
(e.g., set HTTP_KEEPALIVE_TIMEOUT_MS and SSE_HEARTBEAT_MS above/below
MAX_SAFE_TIMEOUT_MS) and assert the resulting server timeout values are clamped
as expected, and exercise gracefulShutdown() by mocking or instantiating
HealthMonitor to return both healthy/unhealthy paths (or stub its methods) and
assert the shutdown branch behaviors (calls made, timers cleared, or different
exit logic). Use the existing jest.mock for config and add spies/mocks for
HealthMonitor and the functions configureServerTimeouts() and gracefulShutdown()
to verify the new control-flow branches are executed and their side effects
asserted.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f4f8c0ea-27d4-4dba-b273-b27f4dd44a0f

📥 Commits

Reviewing files that changed from the base of the PR and between f9f63b3 and 4fca838.

📒 Files selected for processing (5)
  • src/handlers.ts
  • src/server.ts
  • src/services/TokenScopeDetector.ts
  • tests/unit/handlers.test.ts
  • tests/unit/server.test.ts

Copy link
Copy Markdown

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 25 out of 27 changed files in this pull request and generated 3 comments.

…izeInstanceUrl

- Replace InstanceRegistry's private normalizeUrl() with shared normalizeInstanceUrl
  from utils/url.ts — eliminates default-port divergence between ConnectionManager
  and InstanceRegistry map keys
- Update url.ts docstring to reflect consolidated usage
- Sync README.md.in with README.md (Connection Resilience section was missing from template)
- Clarify disconnected mode: tools remain listed during active reconnect (connecting state)
- Add initialize call-count assertion in retry test
- Add keepAlive clamp boundary test (MAX_SAFE_TIMEOUT_MS - 5000)
@polaz polaz requested a review from Copilot April 3, 2026 18:16
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 3, 2026

@coderabbitai Re: outside-diff server.test.ts:95-108 — Added keepAlive clamp boundary test exercising MAX_SAFE_TIMEOUT_MS ceiling. HealthMonitor.shutdown() coverage exists in HealthMonitor.test.ts.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

Tip

For best results, initiate chat on the files or code changes.

@polaz: Both are well-placed — thanks for the update!

  • keepAlive clamp boundary test — exercising the MAX_SAFE_TIMEOUT_MS ceiling directly in server.test.ts is exactly the right place for it; it pins the Math.min(HTTP_KEEPALIVE_TIMEOUT_MS, MAX_SAFE_TIMEOUT_MS - 5000) + headersTimeout derivation against the ceiling value.
  • HealthMonitor.shutdown() in HealthMonitor.test.ts — keeping shutdown coverage co-located with the rest of the monitor's lifecycle tests is the correct call; no need to duplicate it in server.test.ts.

🧠 Learnings used
Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T00:43:19.144Z
Learning: In `src/config.ts` (structured-world/gitlab-mcp), rate-limit environment variables are parsed with two different helpers: duration/window vars (e.g., `RATE_LIMIT_IP_WINDOW_MS`, `RATE_LIMIT_SESSION_WINDOW_MS`) use `parseTimerMs()` (with upper ceiling and finite-integer validation), while max-request count vars (e.g., `RATE_LIMIT_IP_MAX_REQUESTS`, `RATE_LIMIT_SESSION_MAX_REQUESTS`) use a separate `parsePositiveInt()` guard. Do not suggest applying `parseTimerMs()` to max-request (count) vars in future reviews — timer semantics do not apply to request counts.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:86-95
Timestamp: 2026-03-31T07:15:54.557Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `calculateBackoffDelay` intentionally assumes `RECONNECT_BASE_DELAY_MS <= RECONNECT_MAX_DELAY_MS`. When the config is invalid (BASE > MAX), the function returns BASE as a constant delay. This is treated as a config validation concern, not a runtime bug; a doc note was added to the function rather than adding runtime normalization. Do not flag the missing bounds-swap as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T07:33:01.038Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `refreshTokenScopes()` (around lines 624-651) applies a post-await identity guard: after `await detectTokenScopes(url)` returns, it re-reads `this.instances.get(url)` and compares it to the `state` reference captured before the await. If a concurrent `reinitialize()` or `reset()` replaced the entry, it returns `false` instead of writing stale scope info into the new state. This mirrors the identity guard in `doInitialize`'s catch block. Do not flag the lack of this guard as a stale-write bug in future reviews — it is already in place.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T08:14:19.833Z
Learning: In `tests/unit/services/ConnectionManagerEnhanced.test.ts` (structured-world/gitlab-mcp), private `ConnectionManager` internals are accessed via typed `cmStatic()` and `cmInternal()` helper functions (replacing prior `as any` casts). These helpers expose `instances`, `currentInstanceUrl`, `doInitialize`, and related private fields/methods to set up intermediate states unreachable via the public API. This typed-helper pattern is the accepted approach for this test file. Do not suggest reverting to `as any` or flag the private-field access as unsafe in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:666-669
Timestamp: 2026-04-02T10:24:42.437Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `resolveUrl(instanceUrl?: string)` method and the state-mutation methods that call it (`reportSuccess`, `reportError`, `forceReconnect`) intentionally accept an optional `instanceUrl` with a fallback to `GITLAB_BASE_URL`. This zero-arg fallback is by design for test ergonomics and is documented by the test "should use default URL for reportSuccess/reportError" (around line 716–718 of `tests/unit/services/HealthMonitor.test.ts`). All production callers in `src/handlers.ts` already pass an explicit `instanceUrl`. Do not flag the optional parameter or the GITLAB_BASE_URL fallback as unsafe or requiring a required-parameter refactor in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T16:24:51.665Z
Learning: In tests/unit/handlers.test.ts (structured-world/gitlab-mcp), the deferred-init timedOut guard test around lines 1767–1800 now waits via a resolveExecuteReached barrier to ensure the executeTool dispatch is reached before asserting. This replaces the prior double process.nextTick and prevents scheduler-dependent flakiness. Do not suggest switching this test back to nextTick or fake timers in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:724-732
Timestamp: 2026-04-02T10:46:41.853Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `isAnyInstanceHealthy()` intentionally includes the `'connecting'` state in its truthy set (alongside `'healthy'` and `'degraded'`). During startup all instances are in `'connecting'`, and excluding it would cause `tools/list` to return context-only tools for the entire initialization window (~5s), which is bad UX. The handler-level `isInstanceReachable()` check is the real runtime gate that prevents tool execution against unreachable instances. Do not flag the inclusion of `'connecting'` in `isAnyInstanceHealthy()` as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: tests/unit/services/HealthMonitor.test.ts:752-757
Timestamp: 2026-03-30T04:44:46.218Z
Learning: In `tests/unit/services/HealthMonitor.test.ts` (structured-world/gitlab-mcp), the degraded-detection logic in `HealthMonitor` checks **both** `version` and `schemaInfo`. In the test "should handle getInstanceInfo throwing during health check" (around line 741-757), the default mock provides schema info, so even when `getInstanceInfo()` throws during the periodic health check the instance state can remain `'healthy'`. The loose assertion `expect(state === 'degraded' || state === 'healthy').toBe(true)` is intentionally correct for this mock setup — do not flag it as needing a stricter `'degraded'`-only assertion.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T23:36:18.216Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), all tool calls (including non-idempotent/write tools) race against `HANDLER_TIMEOUT_MS` via `Promise.race`. There is no non-idempotent bypass — bootstrap (init + introspect) is always bounded, and the `timedOut` flag prevents late `reportSuccess`/`reportError` side effects. Do not suggest re-adding a bypass for write tools in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T15:31:04.797Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), the stale-init overwrite race in `doInitialize()` (lines ~117-163) is mitigated by identity guards: the outer `let state` binding + `this.instances.get(baseUrl) === state` check in the catch block (lines ~294-298) prevents a stale init from deleting a fresh state entry. A deeper fix using per-attempt init tokens (to prevent any intermediate writes from stale paths) requires significant refactoring across `doInitialize`/`clearInflight`/`reset` and is tracked in issue `#379`. Do not re-flag the stale-init race as unguarded in future reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:578-603
Timestamp: 2026-03-31T10:09:51.540Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `handleStateChange` intentionally early-returns before calling `registry.updateConnectionStatus` when the state hasn't changed (`previousState === newState`). This is by design: `updateConnectionStatus` is idempotent and calling it on every periodic health-check poll (e.g., `healthy.checking → healthy.idle`) would add unnecessary overhead with no functional benefit. Do not flag this early-return ordering as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:448-457
Timestamp: 2026-03-31T13:18:57.386Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the bootstrap `catch (initError)` block (inside the `try` that calls `connectionManager.initialize()` / `getClient()` / `ensureIntrospected()`) handles the case where `!connectionManager.isConnected(effectiveInstanceUrl)`. It calls `healthMonitor.reportError(effectiveInstanceUrl, initError)` (when `!timedOut`), logs the error, and then **returns** a `createConnectionFailedError(toolName, action, effectiveInstanceUrl, healthMonitor.getState(effectiveInstanceUrl))` structured `CONNECTION_FAILED` response (with `instance_url`, `reconnecting`, and `suggested_fix`) instead of throwing `"Bad Request: Server not initialized"`. Do not flag this bootstrap path as returning a generic error or as failing to report to HealthMonitor in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T23:36:18.216Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor (around lines 184-196) skips the post-init health probe when the remaining budget is < 500ms. When the remaining time is near-zero, the instance is assumed reachable (since init already succeeded) to prevent a false `disconnected` transition caused by an artificially short probe timeout. Do not flag skipping the health probe in this near-zero budget path as incorrect in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:215-216
Timestamp: 2026-03-30T21:18:53.194Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `quickHealthCheck` is intentionally a reachability-only probe — `response.status < 500` is the correct condition because any HTTP response (including 400/404) proves the server is responding. It is NOT an API validation check. Actual API correctness (correct base URL, valid endpoints, auth) is verified at tool execution level. Do not flag this threshold as incorrect in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:329-366
Timestamp: 2026-03-29T21:05:15.421Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the early-return branch for `manage_context` when the instance is **unreachable** intentionally omits `healthMonitor.reportSuccess` / `reportError`, because in that path context tools (whoami, switch_instance) operate on local state only and never call the GitLab API. However, when the instance IS reachable/healthy, `manage_context` (e.g., `whoami`) does call the GitLab API and falls through the normal execution path including `reportSuccess`/`reportError` — this is correct and intentional. Do not flag this asymmetry as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T15:17:47.982Z
Learning: For non-idempotent tool timeouts in handlers (structured-world/gitlab-mcp), tests should assert the structured timeout payload includes `retryable: false`, the `tool` name, and `timeout_ms` to verify the expected error contract. This is covered around Lines 1275–1293 in tests/unit/handlers.test.ts.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/services/HealthMonitor.ts:112-206
Timestamp: 2026-04-03T00:40:13.151Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor function (around lines 112–206) has a cognitive complexity of ~17 (SonarCloud limit: 15) due to three interleaved phases: fast-path health check, full initialization with timeout, and degraded-path reachability verification. The maintainer (polaz) has explicitly declined extracting these into helpers because all three phases share a single `deadline` variable that defines the unified startup budget — splitting would obscure this invariant. A code comment was added above `performConnect` explaining the intentional complexity. Do not re-flag this complexity issue in future reviews of this function.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T12:46:57.150Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor uses a dedicated `InitializationTimeoutError` class (around lines 152-166) to signal an init-timeout rejection, replacing prior fragile string-based error matching. `classifyError` in the catch block relies on this class identity to reliably distinguish init timeouts from other transient/auth/permanent failures. Do not suggest reverting to string-matching for timeout detection in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T16:55:06.625Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `HealthMonitor` does NOT call `InstanceRegistry.getIntrospection()` anywhere. Health and degraded-state detection is derived exclusively from `ConnectionManager.getInstanceInfo(url)` and `ConnectionManager.getSchemaInfo(url)`. Do not suggest testing `InstanceRegistry.getIntrospection()` branches in `HealthMonitor` tests in future reviews — it is not part of the health monitor's code path.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T11:32:28.956Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the first `handleStateChange` callback that fires before `InstanceRegistry` is initialized results in a silently dropped `updateConnectionStatus` call. This is documented in Known Limitations: the registry catches up on the next real state transition, and fixing the miss would require a pending-status queue — which is out of scope. Do not flag this first-transition registry miss as an unhandled bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:100-108
Timestamp: 2026-03-29T18:10:49.416Z
Learning: In structured-world/gitlab-mcp, the `ConnectionManager.isConnected()` / `getInstanceInfo()` singleton path in `HealthMonitor.ts` (`performConnect`) is intentional for single-instance mode. Multi-instance OAuth contexts use `InstanceRegistry` for per-URL state. Do not flag this singleton usage as a bug in future reviews; the two modes have separate code paths by design.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-03-31T07:51:52.620Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `isConnected(instanceUrl?: string)` is a per-URL initialization flag (reads `state?.isInitialized` from `Map<string, InstanceState>`), explicitly documented as NOT a live connectivity check. Use `HealthMonitor.isInstanceReachable()` for runtime reachability. Do not flag `isConnected()` as an unsafe health signal in future reviews.

Copy link
Copy Markdown

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 27 out of 29 changed files in this pull request and generated 1 comment.

@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 4, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

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 27 out of 29 changed files in this pull request and generated 2 comments.

Copy link
Copy Markdown

@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: 7

Caution

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

⚠️ Outside diff range comments (1)
tests/unit/services/ConnectionManagerEnhanced.test.ts (1)

795-810: ⚠️ Potential issue | 🟡 Minor

Wrap jest.resetModules() and jest.doMock() setup inside try/finally blocks to ensure cleanup runs even if an assertion fails.

These tests mutate Jest's module registry and singleton state. The cleanup currently runs only after the final assertion, so if an expectation throws first, the temporary module graph persists and can cause cascade failures in subsequent tests.

🧪 Suggested pattern
 it('should detect version via unauthenticated fetch in OAuth mode (enterprise instance)', async () => {
+  try {
   jest.resetModules();
   jest.doMock('../../../src/config', () => ({
     GITLAB_BASE_URL: 'https://oauth-gitlab.example.com',
     GITLAB_TOKEN: null,
   }));
   // ... remaining doMock calls ...

   const manager = OAuthConnectionManager.getInstance();
   await manager.initialize();

   expect(manager.getVersion()).toBe('17.3.0');
   expect(manager.getTier()).toBe('premium');
   expect(mockEnhancedFetchEnterprise).toHaveBeenCalledWith(
     expect.stringContaining('/api/v4/version'),
     expect.objectContaining({ skipAuth: true }),
   );
+  } finally {
+    manager.reset();
+    jest.resetModules();
+  }
-  manager.reset();
-  jest.resetModules();
 });

Apply the same pattern to lines 813–867 (free instance), 868–909 (network error), and 962–1081 (InstanceRegistry cache) tests.

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

In `@tests/unit/services/ConnectionManagerEnhanced.test.ts` around lines 795 -
810, The test mutates Jest's module registry and singleton state without
guaranteed cleanup; wrap the jest.resetModules() and corresponding
jest.doMock()/module setup logic in a try/finally so cleanup always runs even if
assertions fail — locate the blocks around
OAuthConnectionManager.getInstance()/manager.initialize() and the calls to
jest.doMock()/jest.resetModules() (same pattern also for the free instance block
lines 813–867, network error 868–909, and InstanceRegistry cache 962–1081) and
move the cleanup calls (manager.reset() and jest.resetModules()) into the
finally section to ensure module registry and singleton state are always
restored.
🤖 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/config.ts`:
- Around line 345-352: The parseTimerMs helper (and all count-based env parsers
that currently call Number.parseInt) must stop accepting partial numeric
strings; replace the parseInt-based parsing with a strict check that the env
value fully matches an unsigned integer (e.g. /^\d+$/) before converting with
Number and using it, otherwise return the provided fallback; keep the current
ceiling logic using MAX_SAFE_TIMEOUT_MS and ensure zero/negative or non-matching
values fall back to the fallback.

In `@src/services/ConnectionManager.ts`:
- Around line 376-385: The assignment uses a double-cast for features
(cachedIntrospection.features as unknown as GitLabInstanceInfo['features']);
change the CachedIntrospection definition so its features property is typed the
same as GitLabInstanceInfo['features'] (or a compatible union/type), then remove
the double-cast in the state.instanceInfo assignment (leave the single explicit
types for tier and schemaInfo assignments intact). Update
CachedIntrospection.features’ type and any upstream places that populate it so
the direct assignment in ConnectionManager (state.instanceInfo = { ... features:
cachedIntrospection.features ... }) no longer requires casting.

In `@src/services/TokenScopeDetector.ts`:
- Around line 173-184: The per-instance token logs need to include the instance
URL so failures/summaries are attributable; modify
logTokenSelfIntrospectionError to accept a url parameter (e.g.,
logTokenSelfIntrospectionError(status: number, url: string)) and add the url to
every log call’s metadata (e.g., include { url } with logDebug/logInfo), and
similarly thread the same url field into the expiry-warning metadata and the
per-instance scope summary paths (the other logging functions that emit expiry
warnings and scope summaries) so both success and failure logs include the
instance URL for multi-instance refreshes.

In `@src/utils/fetch.ts`:
- Around line 154-160: The ProxyAgent currently sets proxyTls.timeout but not
the upstream handshake timeout in requestTls, so update the ProxyAgent creation
(function/file uses ProxyAgent, requestTls, proxyTls, tlsOptions,
CONNECT_TIMEOUT_MS, undici) to apply CONNECT_TIMEOUT_MS to requestTls as
well—e.g., merge or add timeout: CONNECT_TIMEOUT_MS into the object passed as
requestTls (while preserving existing tlsOptions), so both the proxy CONNECT
phase and the post-CONNECT upstream TLS handshake are bounded by the same
timeout.

In `@tests/unit/services/HealthMonitor.test.ts`:
- Around line 806-813: After starting HealthMonitor.initialize(TEST_URL) create
a microtask flush and assert the actor was created before checking health: flush
XState microtasks (e.g., await a Promise-based microtask flush helper or call
your test suite's flushPromises), then assert the monitor has an actor (e.g.,
monitor.actors.size > 0 or equivalent public accessor) to ensure the actor
reached the 'connecting' state, and only then call
monitor.isAnyInstanceHealthy() to verify it returns true; reference
HealthMonitor.getInstance, HealthMonitor.initialize, and isAnyInstanceHealthy
when making the assertion change.
- Around line 468-485: Test has a race where XState schedules the actor invoke
as a microtask before mockInitialize is reached; after calling
monitor.initialize(TEST_URL) twice but before resolving the mocked init promise,
flush the microtask queue so the XState invoke runs (e.g., await
Promise.resolve() or await new Promise(r => queueMicrotask(r))) before calling
resolveInit(); update the test around HealthMonitor.getInstance().initialize /
mockInitialize to insert this microtask flush (or a shared flushPromises helper)
so mockInitialize is actually invoked before you resolve the pending init.

In `@tests/unit/utils/url.test.ts`:
- Around line 112-116: Add a regression test in tests/unit/utils/url.test.ts
that exercises the post-new-URL() suffix-stripping branch of
normalizeInstanceUrl by calling normalizeInstanceUrl with inputs containing
query/fragment parts (e.g., 'https://host/api/v4?x=1' and
'https://host/api/v4#frag') and assert their results match the normalized result
for the same base URL without query/fragment (e.g.,
expect(normalizeInstanceUrl('https://host/api/v4?x=1')).toBe(normalizeInstanceUrl('https://host/api/v4'))),
so the query/fragment normalization branch in normalizeInstanceUrl is covered.

---

Outside diff comments:
In `@tests/unit/services/ConnectionManagerEnhanced.test.ts`:
- Around line 795-810: The test mutates Jest's module registry and singleton
state without guaranteed cleanup; wrap the jest.resetModules() and corresponding
jest.doMock()/module setup logic in a try/finally so cleanup always runs even if
assertions fail — locate the blocks around
OAuthConnectionManager.getInstance()/manager.initialize() and the calls to
jest.doMock()/jest.resetModules() (same pattern also for the free instance block
lines 813–867, network error 868–909, and InstanceRegistry cache 962–1081) and
move the cleanup calls (manager.reset() and jest.resetModules()) into the
finally section to ensure module registry and singleton state are always
restored.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 58f53268-909c-4a88-ba83-901d01946432

📥 Commits

Reviewing files that changed from the base of the PR and between edea660 and 2a96d29.

⛔ Files ignored due to path filters (2)
  • .yarn/releases/yarn-4.13.0.cjs is excluded by !**/.yarn/**
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (27)
  • .yarnrc.yml
  • Dockerfile
  • README.md
  • README.md.in
  • package.json
  • src/config.ts
  • src/handlers.ts
  • src/registry-manager.ts
  • src/server.ts
  • src/services/ConnectionManager.ts
  • src/services/HealthMonitor.ts
  • src/services/InstanceRegistry.ts
  • src/services/TokenScopeDetector.ts
  • src/services/ToolAvailability.ts
  • src/utils/error-handler.ts
  • src/utils/fetch.ts
  • src/utils/url.ts
  • tests/unit/RegistryManager.test.ts
  • tests/unit/handlers.test.ts
  • tests/unit/server.test.ts
  • tests/unit/services/ConnectionManager.test.ts
  • tests/unit/services/ConnectionManagerEnhanced.test.ts
  • tests/unit/services/HealthMonitor.test.ts
  • tests/unit/services/ToolAvailability.test.ts
  • tests/unit/services/ToolDescriptionOverrides.test.ts
  • tests/unit/utils/fetch-config.test.ts
  • tests/unit/utils/url.test.ts

…ssages

- parseTimerMs rejects partial numeric strings via /^\d+$/ regex (parseInt
  accepted "120s" → 120, "1e3" → 1)
- ProxyAgent requestTls gets timeout for upstream TLS handshake (was only
  on proxyTls for proxy tunnel)
- Include instanceUrl in health check and token introspection error messages
- Update ToolAvailability docstring: version=unknown allows all tools
- Flush XState microtasks in dedup and connecting-state tests
- Add query/fragment normalization regression test for normalizeInstanceUrl
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 4, 2026

@coderabbitai Re: outside-diff ConnectionManagerEnhanced.test.ts:795-810 — Won't add try/finally around jest.resetModules — this is a test infrastructure pattern used across 50+ tests in this file. Refactoring would be a separate cleanup PR.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

Tip

For best results, initiate chat on the files or code changes.

@polaz: Understood — applying try/finally to a single call site while 50+ other tests in the same file follow the same pattern would create an inconsistency rather than fix the root issue. A dedicated cleanup PR that applies the guard uniformly across the file is the right call. I'll note this for future reviews.

( ˘•ω•˘ )


✏️ Learnings added
Learnt from: polaz
URL: https://github.com/structured-world/gitlab-mcp/pull/382

Timestamp: 2026-04-04T11:24:55.642Z
Learning: In `tests/unit/services/ConnectionManagerEnhanced.test.ts` (structured-world/gitlab-mcp), `jest.resetModules()` is used as a test infrastructure pattern across 50+ tests in the file without `try/finally` guards. The maintainer (polaz) has explicitly declined adding `try/finally` around individual `jest.resetModules()` calls as a piecemeal fix — a uniform cleanup across the entire file is tracked as a separate future PR. Do not suggest adding `try/finally` around individual `jest.resetModules()` calls in this file in future reviews.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T00:43:19.144Z
Learning: In `src/config.ts` (structured-world/gitlab-mcp), rate-limit environment variables are parsed with two different helpers: duration/window vars (e.g., `RATE_LIMIT_IP_WINDOW_MS`, `RATE_LIMIT_SESSION_WINDOW_MS`) use `parseTimerMs()` (with upper ceiling and finite-integer validation), while max-request count vars (e.g., `RATE_LIMIT_IP_MAX_REQUESTS`, `RATE_LIMIT_SESSION_MAX_REQUESTS`) use a separate `parsePositiveInt()` guard. Do not suggest applying `parseTimerMs()` to max-request (count) vars in future reviews — timer semantics do not apply to request counts.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:86-95
Timestamp: 2026-03-31T07:15:54.557Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `calculateBackoffDelay` intentionally assumes `RECONNECT_BASE_DELAY_MS <= RECONNECT_MAX_DELAY_MS`. When the config is invalid (BASE > MAX), the function returns BASE as a constant delay. This is treated as a config validation concern, not a runtime bug; a doc note was added to the function rather than adding runtime normalization. Do not flag the missing bounds-swap as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T07:33:01.038Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `refreshTokenScopes()` (around lines 624-651) applies a post-await identity guard: after `await detectTokenScopes(url)` returns, it re-reads `this.instances.get(url)` and compares it to the `state` reference captured before the await. If a concurrent `reinitialize()` or `reset()` replaced the entry, it returns `false` instead of writing stale scope info into the new state. This mirrors the identity guard in `doInitialize`'s catch block. Do not flag the lack of this guard as a stale-write bug in future reviews — it is already in place.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T08:14:19.833Z
Learning: In `tests/unit/services/ConnectionManagerEnhanced.test.ts` (structured-world/gitlab-mcp), private `ConnectionManager` internals are accessed via typed `cmStatic()` and `cmInternal()` helper functions (replacing prior `as any` casts). These helpers expose `instances`, `currentInstanceUrl`, `doInitialize`, and related private fields/methods to set up intermediate states unreachable via the public API. This typed-helper pattern is the accepted approach for this test file. Do not suggest reverting to `as any` or flag the private-field access as unsafe in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:666-669
Timestamp: 2026-04-02T10:24:42.437Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `resolveUrl(instanceUrl?: string)` method and the state-mutation methods that call it (`reportSuccess`, `reportError`, `forceReconnect`) intentionally accept an optional `instanceUrl` with a fallback to `GITLAB_BASE_URL`. This zero-arg fallback is by design for test ergonomics and is documented by the test "should use default URL for reportSuccess/reportError" (around line 716–718 of `tests/unit/services/HealthMonitor.test.ts`). All production callers in `src/handlers.ts` already pass an explicit `instanceUrl`. Do not flag the optional parameter or the GITLAB_BASE_URL fallback as unsafe or requiring a required-parameter refactor in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T16:24:51.665Z
Learning: In tests/unit/handlers.test.ts (structured-world/gitlab-mcp), the deferred-init timedOut guard test around lines 1767–1800 now waits via a resolveExecuteReached barrier to ensure the executeTool dispatch is reached before asserting. This replaces the prior double process.nextTick and prevents scheduler-dependent flakiness. Do not suggest switching this test back to nextTick or fake timers in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:724-732
Timestamp: 2026-04-02T10:46:41.853Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `isAnyInstanceHealthy()` intentionally includes the `'connecting'` state in its truthy set (alongside `'healthy'` and `'degraded'`). During startup all instances are in `'connecting'`, and excluding it would cause `tools/list` to return context-only tools for the entire initialization window (~5s), which is bad UX. The handler-level `isInstanceReachable()` check is the real runtime gate that prevents tool execution against unreachable instances. Do not flag the inclusion of `'connecting'` in `isAnyInstanceHealthy()` as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: tests/unit/services/HealthMonitor.test.ts:752-757
Timestamp: 2026-03-30T04:44:46.218Z
Learning: In `tests/unit/services/HealthMonitor.test.ts` (structured-world/gitlab-mcp), the degraded-detection logic in `HealthMonitor` checks **both** `version` and `schemaInfo`. In the test "should handle getInstanceInfo throwing during health check" (around line 741-757), the default mock provides schema info, so even when `getInstanceInfo()` throws during the periodic health check the instance state can remain `'healthy'`. The loose assertion `expect(state === 'degraded' || state === 'healthy').toBe(true)` is intentionally correct for this mock setup — do not flag it as needing a stricter `'degraded'`-only assertion.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T23:36:18.216Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), all tool calls (including non-idempotent/write tools) race against `HANDLER_TIMEOUT_MS` via `Promise.race`. There is no non-idempotent bypass — bootstrap (init + introspect) is always bounded, and the `timedOut` flag prevents late `reportSuccess`/`reportError` side effects. Do not suggest re-adding a bypass for write tools in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T15:31:04.797Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), the stale-init overwrite race in `doInitialize()` (lines ~117-163) is mitigated by identity guards: the outer `let state` binding + `this.instances.get(baseUrl) === state` check in the catch block (lines ~294-298) prevents a stale init from deleting a fresh state entry. A deeper fix using per-attempt init tokens (to prevent any intermediate writes from stale paths) requires significant refactoring across `doInitialize`/`clearInflight`/`reset` and is tracked in issue `#379`. Do not re-flag the stale-init race as unguarded in future reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/registry-manager.ts:293-307
Timestamp: 2026-03-31T16:09:46.248Z
Learning: In `src/registry-manager.ts` (structured-world/gitlab-mcp), `buildToolLookupCache(instanceUrl?)` intentionally bakes one URL's tier/version/scope view into the single shared `toolLookupCache`. This is an accepted trade-off: the MCP protocol exposes a single `tools/list` per server (not per-session), so per-URL keyed caches would require session-aware tool dispatch — a larger architectural change. In single-instance mode (99% of usage) the cache is always correct. In multi-instance OAuth the last `refreshCache(url)` wins; the handler-level health gate (handlers.ts) prevents cross-instance tool execution regardless of cache state. The limitation is documented in the code comment around lines 296-302. Do not flag this singleton cache as a cross-instance overwrite bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:329-366
Timestamp: 2026-03-29T21:05:15.421Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the early-return branch for `manage_context` when the instance is **unreachable** intentionally omits `healthMonitor.reportSuccess` / `reportError`, because in that path context tools (whoami, switch_instance) operate on local state only and never call the GitLab API. However, when the instance IS reachable/healthy, `manage_context` (e.g., `whoami`) does call the GitLab API and falls through the normal execution path including `reportSuccess`/`reportError` — this is correct and intentional. Do not flag this asymmetry as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T11:32:28.956Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the first `handleStateChange` callback that fires before `InstanceRegistry` is initialized results in a silently dropped `updateConnectionStatus` call. This is documented in Known Limitations: the registry catches up on the next real state transition, and fixing the miss would require a pending-status queue — which is out of scope. Do not flag this first-transition registry miss as an unhandled bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/services/ConnectionManager.ts:65-67
Timestamp: 2026-04-03T14:50:27.051Z
Learning: In `src/utils/url.ts` (structured-world/gitlab-mcp), `normalizeInstanceUrl()` was updated (after PR `#380` review) to strip default ports (443 for https, 80 for http) using the URL constructor, so `https://host` and `https://host:443` map to the same entry in `ConnectionManager.instances` and inflight caches. When reviewing this function, verify: (1) `new URL()` call is wrapped in try/catch to handle malformed inputs gracefully, (2) API suffix stripping runs before or independently of port normalization to preserve subpaths for self-managed instances, (3) unit tests cover `https://host:443` → `https://host` and `http://host:80` → `http://host` cases.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:100-108
Timestamp: 2026-03-29T18:10:49.416Z
Learning: In structured-world/gitlab-mcp, the `ConnectionManager.isConnected()` / `getInstanceInfo()` singleton path in `HealthMonitor.ts` (`performConnect`) is intentional for single-instance mode. Multi-instance OAuth contexts use `InstanceRegistry` for per-URL state. Do not flag this singleton usage as a bug in future reviews; the two modes have separate code paths by design.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:578-603
Timestamp: 2026-03-31T10:09:51.540Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `handleStateChange` intentionally early-returns before calling `registry.updateConnectionStatus` when the state hasn't changed (`previousState === newState`). This is by design: `updateConnectionStatus` is idempotent and calling it on every periodic health-check poll (e.g., `healthy.checking → healthy.idle`) would add unnecessary overhead with no functional benefit. Do not flag this early-return ordering as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:448-457
Timestamp: 2026-03-31T13:18:57.386Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the bootstrap `catch (initError)` block (inside the `try` that calls `connectionManager.initialize()` / `getClient()` / `ensureIntrospected()`) handles the case where `!connectionManager.isConnected(effectiveInstanceUrl)`. It calls `healthMonitor.reportError(effectiveInstanceUrl, initError)` (when `!timedOut`), logs the error, and then **returns** a `createConnectionFailedError(toolName, action, effectiveInstanceUrl, healthMonitor.getState(effectiveInstanceUrl))` structured `CONNECTION_FAILED` response (with `instance_url`, `reconnecting`, and `suggested_fix`) instead of throwing `"Bad Request: Server not initialized"`. Do not flag this bootstrap path as returning a generic error or as failing to report to HealthMonitor in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/handlers.ts:462-466
Timestamp: 2026-04-03T00:39:40.483Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the two-step `hasToolHandler()`/`executeTool()` pattern at the disconnected `manage_context` bypass (around line 462) and the main dispatch path (around lines 595-616) has an acknowledged TOCTOU gap: a concurrent `refreshCache()` swap between the two calls can make `executeTool()` miss a handler. This is intentionally benign because `executeTool` falls through to the bootstrap path when the handler is not found post-refresh. An atomic `getTool()` refactor to eliminate the gap is tracked separately. Do not re-flag this two-step pattern as a TOCTOU race in future reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:529-564
Timestamp: 2026-04-02T09:32:46.067Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `HealthMonitor` creates one XState actor per monitored instance URL. Actors are only created for instances registered via `InstanceRegistry` config (1–3 in practice), so there is no unbounded actor accumulation. A follow-up issue has already been created to track per-URL eviction/inactivity TTL for future multi-instance/OAuth scenarios. Do not flag actor accumulation as an active memory leak in reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/handlers.ts:209-212
Timestamp: 2026-04-02T23:34:15.753Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), all public APIs that call `getActor(url)` — including `reportSuccess`, `reportError`, `forceReconnect`, and others — intentionally return early (no-op) when no actor exists for a URL. They do NOT lazily create actors. Untracked instance URLs are assumed reachable by `isInstanceReachable()` by design. Actors are only created explicitly via `HealthMonitor.initialize(url)`. Do not flag the missing-actor early-returns in any public API as a bug, and do not claim reportSuccess/reportError create actors for untracked URLs.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T23:36:18.216Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor (around lines 184-196) skips the post-init health probe when the remaining budget is < 500ms. When the remaining time is near-zero, the instance is assumed reachable (since init already succeeded) to prevent a false `disconnected` transition caused by an artificially short probe timeout. Do not flag skipping the health probe in this near-zero budget path as incorrect in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T16:55:06.625Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `HealthMonitor` does NOT call `InstanceRegistry.getIntrospection()` anywhere. Health and degraded-state detection is derived exclusively from `ConnectionManager.getInstanceInfo(url)` and `ConnectionManager.getSchemaInfo(url)`. Do not suggest testing `InstanceRegistry.getIntrospection()` branches in `HealthMonitor` tests in future reviews — it is not part of the health monitor's code path.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: tests/unit/handlers.test.ts:686-704
Timestamp: 2026-04-02T18:35:51.180Z
Learning: In `tests/unit/handlers.test.ts` (structured-world/gitlab-mcp), the double `process.nextTick` pattern (`await new Promise(resolve => process.nextTick(resolve))` twice) is the intentional and correct approach for flushing XState microtasks in fire-and-forget callback tests. `jest.useFakeTimers()` conflicts with XState's internal scheduling, making it unsuitable here. Do not suggest replacing this pattern with fake timers or `waitFor` in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 382
File: tests/unit/handlers.test.ts:154-156
Timestamp: 2026-04-03T21:14:06.949Z
Learning: In `tests/unit/handlers.test.ts` (structured-world/gitlab-mcp), the `tools/list_changed` notification path triggered by health state transitions is NOT asserted in unit tests because it requires `SessionManager` mock integration which is out of scope. The notification path is covered at the integration level via `broadcastToolsListChangedForStateChange` in the state-change callback tests. Do not suggest adding server notification method stubs or `tools/list_changed` assertions to this unit test file in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T15:17:47.982Z
Learning: In tests/unit/handlers.test.ts (structured-world/gitlab-mcp), when simulating a hanging operation, prefer a never-settling promise (`new Promise<never>(() => {})`) over timer-backed mocks to avoid delayed resolves leaking into subsequent tests. This pattern is now used around Lines 1255–1257.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:132-135
Timestamp: 2026-03-31T10:37:22.878Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), `resetHandlersState()` is a test-only helper that clears `healthMonitorStartup` and `stateChangeRegistered`. It is always paired with `HealthMonitor.resetInstance()` in tests. `HealthMonitor.resetInstance()` calls `shutdown()` which destroys the singleton (and all registered `onStateChange` callbacks), so the fresh singleton returned by `getInstance()` starts with an empty callbacks array. In `tests/unit/handlers.test.ts`, `HealthMonitor` is fully mocked so no real listeners accumulate. The two-call pattern guarantees no duplicate listener leaks. Do not flag `resetHandlersState()` clearing `stateChangeRegistered` as unsafe in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/services/HealthMonitor.ts:112-206
Timestamp: 2026-04-03T00:40:13.151Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor function (around lines 112–206) has a cognitive complexity of ~17 (SonarCloud limit: 15) due to three interleaved phases: fast-path health check, full initialization with timeout, and degraded-path reachability verification. The maintainer (polaz) has explicitly declined extracting these into helpers because all three phases share a single `deadline` variable that defines the unified startup budget — splitting would obscure this invariant. A code comment was added above `performConnect` explaining the intentional complexity. Do not re-flag this complexity issue in future reviews of this function.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T12:46:57.150Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor uses a dedicated `InitializationTimeoutError` class (around lines 152-166) to signal an init-timeout rejection, replacing prior fragile string-based error matching. `classifyError` in the catch block relies on this class identity to reliably distinguish init timeouts from other transient/auth/permanent failures. Do not suggest reverting to string-matching for timeout detection in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/ConnectionManager.ts:112-112
Timestamp: 2026-04-02T10:46:39.546Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `doInitialize()` (around line 112) has a cognitive complexity of ~34 (SonarCloud limit: 15) due to three interleaved init paths: OAuth best-effort unauthenticated version detection, REST-only token-scope path, and full GraphQL introspection. The maintainer (polaz) has explicitly decided NOT to extract helper methods in PR `#369` because the shared `state` mutations and identity guards would add indirection without reducing conceptual complexity. This is tracked for future refactoring. Do not re-flag this complexity issue in reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/handlers.ts:517-525
Timestamp: 2026-04-03T12:45:52.410Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), an `InitializationTimeoutError` class (added in PR `#380`) is used as the typed timeout sentinel injected by the handler-level timeout path. The `!bootstrapComplete` catch block checks `initError instanceof InitializationTimeoutError` to distinguish the injected sentinel from real bootstrap failures: real auth/permanent errors always call `healthMonitor.reportError()` (bypassing the `timedOut` guard), while the timeout sentinel itself is not re-reported since `reportError` was already called from the timeout branch. Do not suggest string-matching on error messages to detect this sentinel in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-04T07:25:22.034Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the handler-level timeout (HANDLER_TIMEOUT_MS, default 120s) intentionally uses a plain `Error` with a message containing 'timed out' rather than `InitializationTimeoutError`. This is by design: `InitializationTimeoutError` is reserved for the startup init timeout (INIT_TIMEOUT_MS, default 5s) inside the `performConnect` XState actor. Using `InitializationTimeoutError` for the handler-level timeout would be misleading in logs and health snapshots. The 'timed out' phrase in the plain Error message ensures `classifyError()` classifies it as `transient` via its free-text fallback branch. This design decision is documented in a code comment around lines 711–714 of `src/handlers.ts`. Do not suggest replacing the plain Error with `InitializationTimeoutError` for the handler-level timeout in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T10:01:57.587Z
Learning: In `tests/unit/services/ConnectionManagerEnhanced.test.ts` (structured-world/gitlab-mcp), around line 1079, the `ensureIntrospected` cache-path test is already correct: the `getIntrospection` mock (set up around line 986) returns full cached data, and the test asserts both that `getIntrospection` was called and that the restored introspection data is present. Do not flag this test as missing cache-restoration assertions or as incorrectly set up in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: tests/unit/handlers.test.ts:1451-1456
Timestamp: 2026-04-03T07:32:18.078Z
Learning: In `tests/unit/handlers.test.ts` (structured-world/gitlab-mcp), synchronous `require()` in `afterEach` blocks is the accepted pattern for restoring Jest mock values on module-level mocks (e.g., `jest.mock('../../src/oauth/index', ...)`). The project enforces a no-eslint-disable rule, so replacing `require()` with `dynamic import()` + an eslint-disable comment is not acceptable. Do not flag synchronous `require()` usage in Jest teardown blocks (`afterEach`/`afterAll`) as inconsistent or suggest converting it to dynamic imports in future reviews of test files in this repository.

Copy link
Copy Markdown

@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

Caution

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

⚠️ Outside diff range comments (1)
tests/unit/handlers.test.ts (1)

189-210: 🧹 Nitpick | 🔵 Trivial

Cover concurrent setupHandlers() deduplication.

The retry path is covered, but the main contract of healthMonitorStartup ??= is concurrent-session deduplication. Without a Promise.all(...) case, a regression back to per-session initialize() / onStateChange() calls would still pass here.

Suggested test
+    it('should share startup work across concurrent setupHandlers calls', async () => {
+      const otherServer = {
+        setRequestHandler: jest.fn(),
+      } as unknown as jest.Mocked<Server>;
+
+      await Promise.all([setupHandlers(mockServer), setupHandlers(otherServer)]);
+
+      expect(mockHealthMonitor.initialize).toHaveBeenCalledTimes(1);
+      expect(mockHealthMonitor.onStateChange).toHaveBeenCalledTimes(1);
+      expect(mockServer.setRequestHandler).toHaveBeenCalledTimes(2);
+      expect(otherServer.setRequestHandler).toHaveBeenCalledTimes(2);
+    });

As per coding guidelines, tests/**/*.test.{ts,tsx}: Use Jest for all tests and ensure all features have corresponding test coverage.

Also applies to: 1476-1490

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

In `@tests/unit/handlers.test.ts` around lines 189 - 210, Add a test that verifies
concurrent calls to setupHandlers are deduplicated by running multiple
simultaneous setupHandlers (e.g., via Promise.all) and asserting
mockHealthMonitor.initialize and mockHealthMonitor.onStateChange are called
exactly once; ensure the test still validates both handlers were registered (use
getRegisteredHandler with ListToolsRequestSchema and CallToolRequestSchema) so
the concurrent path exercises the same registration logic as the single-call
test.
♻️ Duplicate comments (5)
src/services/ToolAvailability.ts (1)

488-510: ⚠️ Potential issue | 🟠 Major

Mirror the 'unknown'-version fast path in isToolAvailable() too.

Line 495 now treats version === 'unknown' as “defer filtering”, but the singleton-backed path still uses the old gates. That means getAvailableTools() / filterToolsByAvailability() can return a different tool set than isToolAvailableForInstance() for the same instance state.

🔧 Suggested follow-up
   try {
     const instanceInfo = connectionManager.getInstanceInfo();
+    if (instanceInfo.version === 'unknown') {
+      return true;
+    }
 
     const actionReq = this.getActionRequirement(toolName, action);
     if (actionReq) {
       const version = parseVersion(instanceInfo.version);
       if (version < parseVersion(actionReq.minVersion)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/ToolAvailability.ts` around lines 488 - 510, The
singleton-backed availability check in isToolAvailable should mirror the
fast-path used in isToolAvailableForInstance: if instanceInfo.version ===
'unknown' return true (i.e., defer filtering) so
getAvailableTools/filterToolsByAvailability yield the same set as
isToolAvailableForInstance for the same instance state; update isToolAvailable
to check instanceInfo.version === 'unknown' before applying the conservative
>=15.0 gate and keep references to isTierSufficient/getActionRequirement/action
requirements behavior intact.
src/services/TokenScopeDetector.ts (1)

224-274: ⚠️ Potential issue | 🟡 Minor

Include the instance URL in the remaining per-instance token logs.

detectTokenScopes() now resolves a normalized per-instance URL, but the validation/network-error logs and the expiry/scope-summary branches in logTokenScopeInfo() still emit no instance identifier. In multi-instance refreshes, those messages are hard to attribute to the failing GitLab host.

🔍 Suggested change
 export async function detectTokenScopes(baseUrl?: string): Promise<TokenScopeInfo | null> {
   const url = normalizeInstanceUrl(baseUrl ?? GITLAB_BASE_URL);
@@
     const parsed = TokenSelfResponseSchema.safeParse(raw);
     if (!parsed.success) {
       logDebug('Token self-introspection response validation failed', {
+        url,
         error: parsed.error.message,
       });
       return null;
     }
@@
   } catch (error) {
     logDebug('Token scope detection failed (network error)', {
+      url,
       error: error instanceof Error ? error.message : String(error),
     });
     return null;
   }
 }
@@
 export function logTokenScopeInfo(
   info: TokenScopeInfo,
   totalTools: number,
   baseUrl: string = GITLAB_BASE_URL,
 ): void {
+  const url = normalizeInstanceUrl(baseUrl);
   const availableTools = getToolsForScopes(info.scopes);
   const scopeList = info.scopes.join(', ');
@@
       logWarn(`Token "${info.name}" has expired! Please create a new token.`, {
+        url,
         tokenName: info.name,
         expiresAt: info.expiresAt,
       });
@@
     logInfo(`Token "${info.name}" detected`, {
+      url,
       tokenName: info.name,
       scopes: scopeList,
       expiresAt: info.expiresAt ?? 'never',
     });

Also applies to: 340-393

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

In `@src/services/TokenScopeDetector.ts` around lines 224 - 274, The logging
currently omits the normalized instance URL, making per-instance failures
ambiguous; update detectTokenScopes() to include the resolved url in all logs
(pass or embed url into calls such as
logTokenSelfIntrospectionError(response.status, url) already present, and also
include url in the catch logDebug call and in the calls to logTokenScopeInfo/any
expiry/scope-summary logging) so that logDebug('Token scope detection failed
(network error)') and any logTokenScopeInfo branches emit the instance
identifier (use the url variable or pass it as an extra field/argument to those
logging helpers).
src/utils/fetch.ts (1)

154-163: ⚠️ Potential issue | 🟠 Major

Always set requestTls.timeout on the proxy path.

ProxyAgent omits AgentOptions.connect, and both requestTls and proxyTls are the ConnectOptions hooks where Undici exposes timeout. Because requestTls becomes undefined when hasTlsConfig is false, HTTPS-over-proxy requests lose the upstream TLS-handshake timeout and can exceed CONNECT_TIMEOUT_MS. Build requestTls unconditionally and merge tlsOptions into it instead. (github.com)

🔧 Suggested change
     return new undici.ProxyAgent({
       uri: proxyUrl,
-      requestTls: hasTlsConfig ? { ...tlsOptions, timeout: CONNECT_TIMEOUT_MS } : undefined,
+      requestTls: { ...tlsOptions, timeout: CONNECT_TIMEOUT_MS },
       proxyTls: { timeout: CONNECT_TIMEOUT_MS },
       headersTimeout: HEADERS_TIMEOUT_MS,
       bodyTimeout: BODY_TIMEOUT_MS,
     });
For Undici `ProxyAgent`, do `requestTls` and `proxyTls` both extend `Client#ConnectOptions` (including `timeout`), and is `AgentOptions.connect` intentionally omitted from `ProxyAgentOptions`?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/fetch.ts` around lines 154 - 163, The ProxyAgent usage
conditionally omits requestTls when hasTlsConfig is false, which drops the
upstream TLS handshake timeout; always build requestTls and set its timeout to
CONNECT_TIMEOUT_MS while merging tlsOptions when present so both requestTls and
proxyTls include timeout. Update the undici.ProxyAgent constructor call
(symbols: ProxyAgent, requestTls, proxyTls, tlsOptions, CONNECT_TIMEOUT_MS,
hasTlsConfig) to create a requestTls object unconditionally that spreads
tlsOptions if available and always includes timeout.
src/config.ts (1)

411-463: ⚠️ Potential issue | 🟠 Major

Harden the count-based env parsers too.

parseTimerMs() now rejects partial numerics, but these settings still use Number.parseInt(). Inputs like 1e3, 100req, or 0x10 will be silently truncated and can collapse failure thresholds, pool sizes, retry counts, or rate limits to the wrong value. Route them through a shared strict integer parser instead, while preserving the current >= 0 behavior for API_RETRY_MAX_ATTEMPTS.

🔧 Suggested hardening
+function parseStrictInt(envValue: string | undefined): number | null {
+  const raw = envValue?.trim();
+  if (!raw || !/^\d+$/.test(raw)) {
+    return null;
+  }
+  const parsed = Number(raw);
+  return Number.isSafeInteger(parsed) ? parsed : null;
+}
+
+function parsePositiveInt(envValue: string | undefined, fallback: number): number {
+  const parsed = parseStrictInt(envValue);
+  return parsed !== null && parsed > 0 ? parsed : fallback;
+}
+
+function parseNonNegativeInt(envValue: string | undefined, fallback: number): number {
+  const parsed = parseStrictInt(envValue);
+  return parsed !== null && parsed >= 0 ? parsed : fallback;
+}
+
-const parsedFailureThreshold = Number.parseInt(process.env.GITLAB_FAILURE_THRESHOLD ?? '3', 10);
-export const FAILURE_THRESHOLD =
-  Number.isFinite(parsedFailureThreshold) && parsedFailureThreshold > 0
-    ? parsedFailureThreshold
-    : 3;
+export const FAILURE_THRESHOLD = parsePositiveInt(process.env.GITLAB_FAILURE_THRESHOLD, 3);
 
-const parsedPoolMaxConnections = Number.parseInt(
-  process.env.GITLAB_POOL_MAX_CONNECTIONS ?? '25',
-  10,
-);
-export const POOL_MAX_CONNECTIONS =
-  Number.isFinite(parsedPoolMaxConnections) && parsedPoolMaxConnections > 0
-    ? parsedPoolMaxConnections
-    : 25;
+export const POOL_MAX_CONNECTIONS = parsePositiveInt(
+  process.env.GITLAB_POOL_MAX_CONNECTIONS,
+  25,
+);
 
-const parsedMaxAttempts = Number.parseInt(process.env.GITLAB_API_RETRY_MAX_ATTEMPTS ?? '3', 10);
-export const API_RETRY_MAX_ATTEMPTS =
-  Number.isFinite(parsedMaxAttempts) && parsedMaxAttempts >= 0 ? parsedMaxAttempts : 3;
+export const API_RETRY_MAX_ATTEMPTS = parseNonNegativeInt(
+  process.env.GITLAB_API_RETRY_MAX_ATTEMPTS,
+  3,
+);
 
-const parsedIpMaxRequests = Number.parseInt(process.env.RATE_LIMIT_IP_MAX_REQUESTS ?? '100', 10);
-export const RATE_LIMIT_IP_MAX_REQUESTS =
-  Number.isFinite(parsedIpMaxRequests) && parsedIpMaxRequests > 0 ? parsedIpMaxRequests : 100;
+export const RATE_LIMIT_IP_MAX_REQUESTS = parsePositiveInt(
+  process.env.RATE_LIMIT_IP_MAX_REQUESTS,
+  100,
+);
 
-const parsedSessionMaxRequests = Number.parseInt(
-  process.env.RATE_LIMIT_SESSION_MAX_REQUESTS ?? '300',
-  10,
-);
-export const RATE_LIMIT_SESSION_MAX_REQUESTS =
-  Number.isFinite(parsedSessionMaxRequests) && parsedSessionMaxRequests > 0
-    ? parsedSessionMaxRequests
-    : 300;
+export const RATE_LIMIT_SESSION_MAX_REQUESTS = parsePositiveInt(
+  process.env.RATE_LIMIT_SESSION_MAX_REQUESTS,
+  300,
+);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config.ts` around lines 411 - 463, Replace all usages of Number.parseInt
for count-based env vars with a shared strict integer parser (e.g., create a
helper like parseStrictInt) that rejects partial numerics (so inputs like "1e3",
"100req", "0x10" are invalid); then use that helper to parse
GITLAB_FAILURE_THRESHOLD -> FAILURE_THRESHOLD, GITLAB_POOL_MAX_CONNECTIONS ->
POOL_MAX_CONNECTIONS, GITLAB_API_RETRY_MAX_ATTEMPTS -> API_RETRY_MAX_ATTEMPTS
(preserving the existing >= 0 acceptance), RATE_LIMIT_IP_MAX_REQUESTS ->
RATE_LIMIT_IP_MAX_REQUESTS, and RATE_LIMIT_SESSION_MAX_REQUESTS ->
RATE_LIMIT_SESSION_MAX_REQUESTS, keeping the same fallback defaults and >0 (or
>=0 for API_RETRY_MAX_ATTEMPTS) validation logic. Ensure the helper returns NaN
or throws on invalid input so the ternary checks remain correct and add unit
tests for parseStrictInt and each constant to cover partial-numeric inputs.
tests/unit/services/HealthMonitor.test.ts (1)

480-486: ⚠️ Potential issue | 🟡 Minor

Use the suite’s XState flush helper here instead of a single promise tick.

await Promise.resolve() is weaker than flushXStateMicrotasks(). In the concurrent-init test it can race resolveInit() before the actor reaches mockInitialize(), and in the connecting test the assertion can still pass via the empty-actors fast path. Use the shared helper in both places, and keep the second spec asserting that the actor exists before checking isAnyInstanceHealthy().

♻️ Suggested test hardening
-      await Promise.resolve();
+      await flushXStateMicrotasks();
...
-      await Promise.resolve();
+      await flushXStateMicrotasks();
+      expect(monitor.getMonitoredInstances()).toContain(TEST_URL);

Also applies to: 813-817

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

In `@tests/unit/services/HealthMonitor.test.ts` around lines 480 - 486, Replace
the weak microtask synchronization calls in the tests by using the suite helper
flushXStateMicrotasks() instead of await Promise.resolve(): call
flushXStateMicrotasks() after invoking monitor.initialize(TEST_URL) (both in the
concurrent-init test and the connecting test locations) so the actor invoke
reliably reaches mockInitialize before resolveInit() is called; keep the second
spec’s assertion that the actor exists (after monitor.initialize and
flushXStateMicrotasks()) before asserting monitor.isAnyInstanceHealthy(), and
ensure references to resolveInit(), mockInitialize, monitor.initialize,
flushXStateMicrotasks, and isAnyInstanceHealthy are updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unit/handlers.test.ts`:
- Around line 704-731: Add a companion reachable-path assertion to the existing
"should allow manage_context through when disconnected" test: after the
unreachable assertions, set
mockHealthMonitor.isInstanceReachable.mockReturnValue(true) and
mockHealthMonitor.getState.mockReturnValue('healthy'), stub
mockRegistryManager.executeTool.mockResolvedValue({ context: 'info' }) for the
reachable case, call callToolHandler with the same manage_context/whoami params,
and assert the call succeeds and mockHealthMonitor.reportSuccess was called (and
reportError was not); this ensures manage_context goes through the normal
health-reporting path when the instance is reachable.

In `@tests/unit/services/HealthMonitor.test.ts`:
- Around line 587-595: The test currently only asserts that mockFetch ran;
update the assertion to verify the quick-health-check request shape by asserting
that mockFetch was called with an options object containing method: 'HEAD' and
skipAuth: true (e.g., expect(mockFetch).toHaveBeenCalledWith(expect.any(String),
expect.objectContaining({ method: 'HEAD', skipAuth: true }))), targeting the
call that probes TEST_URL after initMonitor()/quickHealthCheck(); apply the same
change to the similar spec around lines 822-836 so both tests validate the
unauthenticated HEAD probe shape.

---

Outside diff comments:
In `@tests/unit/handlers.test.ts`:
- Around line 189-210: Add a test that verifies concurrent calls to
setupHandlers are deduplicated by running multiple simultaneous setupHandlers
(e.g., via Promise.all) and asserting mockHealthMonitor.initialize and
mockHealthMonitor.onStateChange are called exactly once; ensure the test still
validates both handlers were registered (use getRegisteredHandler with
ListToolsRequestSchema and CallToolRequestSchema) so the concurrent path
exercises the same registration logic as the single-call test.

---

Duplicate comments:
In `@src/config.ts`:
- Around line 411-463: Replace all usages of Number.parseInt for count-based env
vars with a shared strict integer parser (e.g., create a helper like
parseStrictInt) that rejects partial numerics (so inputs like "1e3", "100req",
"0x10" are invalid); then use that helper to parse GITLAB_FAILURE_THRESHOLD ->
FAILURE_THRESHOLD, GITLAB_POOL_MAX_CONNECTIONS -> POOL_MAX_CONNECTIONS,
GITLAB_API_RETRY_MAX_ATTEMPTS -> API_RETRY_MAX_ATTEMPTS (preserving the existing
>= 0 acceptance), RATE_LIMIT_IP_MAX_REQUESTS -> RATE_LIMIT_IP_MAX_REQUESTS, and
RATE_LIMIT_SESSION_MAX_REQUESTS -> RATE_LIMIT_SESSION_MAX_REQUESTS, keeping the
same fallback defaults and >0 (or >=0 for API_RETRY_MAX_ATTEMPTS) validation
logic. Ensure the helper returns NaN or throws on invalid input so the ternary
checks remain correct and add unit tests for parseStrictInt and each constant to
cover partial-numeric inputs.

In `@src/services/TokenScopeDetector.ts`:
- Around line 224-274: The logging currently omits the normalized instance URL,
making per-instance failures ambiguous; update detectTokenScopes() to include
the resolved url in all logs (pass or embed url into calls such as
logTokenSelfIntrospectionError(response.status, url) already present, and also
include url in the catch logDebug call and in the calls to logTokenScopeInfo/any
expiry/scope-summary logging) so that logDebug('Token scope detection failed
(network error)') and any logTokenScopeInfo branches emit the instance
identifier (use the url variable or pass it as an extra field/argument to those
logging helpers).

In `@src/services/ToolAvailability.ts`:
- Around line 488-510: The singleton-backed availability check in
isToolAvailable should mirror the fast-path used in isToolAvailableForInstance:
if instanceInfo.version === 'unknown' return true (i.e., defer filtering) so
getAvailableTools/filterToolsByAvailability yield the same set as
isToolAvailableForInstance for the same instance state; update isToolAvailable
to check instanceInfo.version === 'unknown' before applying the conservative
>=15.0 gate and keep references to isTierSufficient/getActionRequirement/action
requirements behavior intact.

In `@src/utils/fetch.ts`:
- Around line 154-163: The ProxyAgent usage conditionally omits requestTls when
hasTlsConfig is false, which drops the upstream TLS handshake timeout; always
build requestTls and set its timeout to CONNECT_TIMEOUT_MS while merging
tlsOptions when present so both requestTls and proxyTls include timeout. Update
the undici.ProxyAgent constructor call (symbols: ProxyAgent, requestTls,
proxyTls, tlsOptions, CONNECT_TIMEOUT_MS, hasTlsConfig) to create a requestTls
object unconditionally that spreads tlsOptions if available and always includes
timeout.

In `@tests/unit/services/HealthMonitor.test.ts`:
- Around line 480-486: Replace the weak microtask synchronization calls in the
tests by using the suite helper flushXStateMicrotasks() instead of await
Promise.resolve(): call flushXStateMicrotasks() after invoking
monitor.initialize(TEST_URL) (both in the concurrent-init test and the
connecting test locations) so the actor invoke reliably reaches mockInitialize
before resolveInit() is called; keep the second spec’s assertion that the actor
exists (after monitor.initialize and flushXStateMicrotasks()) before asserting
monitor.isAnyInstanceHealthy(), and ensure references to resolveInit(),
mockInitialize, monitor.initialize, flushXStateMicrotasks, and
isAnyInstanceHealthy are updated accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ce71b882-845d-4fd4-a530-39ff19a2df3e

📥 Commits

Reviewing files that changed from the base of the PR and between c1039de and f453e8e.

📒 Files selected for processing (8)
  • src/config.ts
  • src/services/HealthMonitor.ts
  • src/services/TokenScopeDetector.ts
  • src/services/ToolAvailability.ts
  • src/utils/fetch.ts
  • tests/unit/handlers.test.ts
  • tests/unit/services/HealthMonitor.test.ts
  • tests/unit/utils/url.test.ts

Copy link
Copy Markdown

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 27 out of 29 changed files in this pull request and generated 3 comments.

…or messages

- Extract parseStrictInt helper with /^\d+$/ regex, replace all 5 remaining
  parseInt usages (FAILURE_THRESHOLD, POOL_MAX_CONNECTIONS, retry/rate counts)
- Include resolvedUrl in getInstanceInfo/getSchemaInfo error messages
- Add unknown-version fast path to isToolAvailable (mirrors isToolAvailableForInstance)
- Include url in token scope detection validation/network error logs
- Assert health check request shape (URL + method HEAD)
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 4, 2026

@coderabbitai All duplicate and outside-diff items from this review addressed:

  1. ToolAvailability unknown-version — Fixed: isToolAvailable() now mirrors isToolAvailableForInstance() with version=unknown fast path
  2. TokenScopeDetector URL in logs — Fixed: added url to validation and network error log payloads
  3. fetch.ts requestTls.timeout — Already fixed in previous commit (requestTls includes timeout)
  4. config.ts count parsers — Fixed: all 5 parseInt usages replaced with parseStrictInt
  5. HealthMonitor XState flush — Already fixed in previous commit (microtask flush added)
  6. Concurrent setupHandlers dedup test — Tracked in test(handlers): add concurrent setupHandlers deduplication test #385

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== Locate ToolAvailability.ts =="
fd -i --full-path 'src/services/ToolAvailability.ts' || true
rg -n --color=never -C3 $'static\\s+isToolAvailable\\s*\\(' src/services/ToolAvailability.ts || true
rg -n --color=never -C3 "version\\s*===\\s*['\"]unknown['\"]" src/services/ToolAvailability.ts || true

echo "== Inspect TokenScopeDetector logging (url included) =="
fd -i --full-path 'src/services/TokenScopeDetector.ts' || true
# Show log callsites around validation/network errors and whether 'url' is part of payload
rg -n --color=never -C3 "log\\w+\\(|validation|network|expires|computeDaysUntilExpiry" src/services/TokenScopeDetector.ts || true
rg -n --color=never -C3 "\\burl\\s*:" src/services/TokenScopeDetector.ts || true

echo "== Inspect fetch.ts for requestTls timeout and ProxyAgent changes =="
fd -i --full-path 'src/utils/fetch.ts' || true
rg -n --color=never -C3 "\\brequestTls\\b" src/utils/fetch.ts || true
rg -n --color=never -C3 "\\btimeout\\b" src/utils/fetch.ts || true
rg -n --color=never -C3 "ProxyAgent\\(" src/utils/fetch.ts || true

echo "== Inspect config.ts parsers =="
fd -i --full-path 'src/config.ts' || true
# Count-style parsers should use parseStrictInt; ensure old raw parseInt() calls are gone for counts
rg -n --color=never -C3 "\\bparseStrictInt\\b" src/config.ts || true
rg -n --color=never -C3 "\\bparsePositiveInt\\b" src/config.ts || true
rg -nP --color=never -C3 "\\bparseInt\\s*\\(" src/config.ts || true

echo "== HealthMonitor microtask flush in tests =="
fd -i --full-path 'tests/unit/services/HealthMonitor.test.ts' || true
rg -n --color=never -C3 "flushXStateMicrotasks|process\\.nextTick" tests/unit/services/HealthMonitor.test.ts || true

echo "== setupHandlers concurrency/dedup mention in handlers tests =="
fd -i --full-path 'tests/unit/handlers.test.ts' || true
rg -n --color=never -C3 "setupHandlers|dedup|concurrent" tests/unit/handlers.test.ts || true

Length of output: 25278


@polaz Thanks — I verified each item in the repo:

  1. ToolAvailability unknown-version fast path
  • Confirmed in src/services/ToolAvailability.ts:
    • isToolAvailable(): allows when instanceInfo.version === 'unknown' (Lines 450–452).
    • isToolAvailableForInstance(): same fast-allow (Lines 497–501).
  1. TokenScopeDetector URL in logs
  • Non-OK status logging includes url (Lines 174–183).
  • Validation failure includes url and error (Lines 247–250).
  • Network error includes url (Lines 270–273).
  • “Create a token” fix message also logs url (Lines 394–396).
  1. fetch.ts requestTls.timeout
  • ProxyAgent now sets requestTls with timeout and proxyTls timeout (Lines 160–165, 162–164).
  • Non-proxy connectOptions include timeout: CONNECT_TIMEOUT_MS (Lines 174–176).
  • Undici timeout errors are mapped to structured “GitLab API timeout …” messages (Lines 572–614).
  1. config.ts count parsers
  • parseStrictInt() added (Lines 350–356) and used for all count-style envs:
    • FAILURE_THRESHOLD (Line 417)
    • POOL_MAX_CONNECTIONS (Line 421)
    • API_RETRY_MAX_ATTEMPTS (Line 427)
    • RATE_LIMIT_IP_MAX_REQUESTS (Lines 440–442)
    • RATE_LIMIT_SESSION_MAX_REQUESTS (Lines 450–453)
  • parseTimerMs() now delegates to parseStrictInt (Line 359).
  1. HealthMonitor XState flush
  • flushXStateMicrotasks helper present (Lines 15–19) and used in afterEach and state-event assertions (e.g., Lines 108, 314, 356, 377, 395, 419).
  1. Concurrent setupHandlers dedup test
  • Startup-failure/reset behavior covered (tests/unit/handlers.test.ts Lines 1476–1490). The explicit concurrency-dedup case is tracked in #385 as noted.

Two quick housekeeping items:

  • I can update our knowledge base to reflect that count-style env vars now use parseStrictInt (not parsePositiveInt). Do you want me to remove the older learning that mentioned parsePositiveInt and replace it? If yes, I’ll delete learning ID ae8f0476-c501-4a07-854c-02b788dc9996 and add the updated one.
  • Want me to run a final pass review now that these are in? Happy to trigger it.

✏️ Learnings added
Learnt from: polaz
URL: https://github.com/structured-world/gitlab-mcp/pull/382

Timestamp: 2026-04-04T16:24:22.255Z
Learning: In src/services/ToolAvailability.ts (structured-world/gitlab-mcp), isToolAvailable() now mirrors isToolAvailableForInstance(): it early-returns true when instanceInfo.version === 'unknown' so tools aren’t filtered out during REST-only/OAuth-deferred detection. Do not re-flag the missing unknown-version fast path in future reviews.

Learnt from: polaz
URL: https://github.com/structured-world/gitlab-mcp/pull/382

Timestamp: 2026-04-04T16:24:22.255Z
Learning: In src/config.ts (structured-world/gitlab-mcp), all count-based environment variables (e.g., FAILURE_THRESHOLD, POOL_MAX_CONNECTIONS, API_RETRY_MAX_ATTEMPTS, RATE_LIMIT_IP_MAX_REQUESTS, RATE_LIMIT_SESSION_MAX_REQUESTS) use parseStrictInt(). Timer-based values continue to use parseTimerMs(). Do not suggest using parseTimerMs() for counts in future reviews.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T00:43:19.144Z
Learning: In `src/config.ts` (structured-world/gitlab-mcp), rate-limit environment variables are parsed with two different helpers: duration/window vars (e.g., `RATE_LIMIT_IP_WINDOW_MS`, `RATE_LIMIT_SESSION_WINDOW_MS`) use `parseTimerMs()` (with upper ceiling and finite-integer validation), while max-request count vars (e.g., `RATE_LIMIT_IP_MAX_REQUESTS`, `RATE_LIMIT_SESSION_MAX_REQUESTS`) use a separate `parsePositiveInt()` guard. Do not suggest applying `parseTimerMs()` to max-request (count) vars in future reviews — timer semantics do not apply to request counts.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:86-95
Timestamp: 2026-03-31T07:15:54.557Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `calculateBackoffDelay` intentionally assumes `RECONNECT_BASE_DELAY_MS <= RECONNECT_MAX_DELAY_MS`. When the config is invalid (BASE > MAX), the function returns BASE as a constant delay. This is treated as a config validation concern, not a runtime bug; a doc note was added to the function rather than adding runtime normalization. Do not flag the missing bounds-swap as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T07:33:01.038Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `refreshTokenScopes()` (around lines 624-651) applies a post-await identity guard: after `await detectTokenScopes(url)` returns, it re-reads `this.instances.get(url)` and compares it to the `state` reference captured before the await. If a concurrent `reinitialize()` or `reset()` replaced the entry, it returns `false` instead of writing stale scope info into the new state. This mirrors the identity guard in `doInitialize`'s catch block. Do not flag the lack of this guard as a stale-write bug in future reviews — it is already in place.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T08:14:19.833Z
Learning: In `tests/unit/services/ConnectionManagerEnhanced.test.ts` (structured-world/gitlab-mcp), private `ConnectionManager` internals are accessed via typed `cmStatic()` and `cmInternal()` helper functions (replacing prior `as any` casts). These helpers expose `instances`, `currentInstanceUrl`, `doInitialize`, and related private fields/methods to set up intermediate states unreachable via the public API. This typed-helper pattern is the accepted approach for this test file. Do not suggest reverting to `as any` or flag the private-field access as unsafe in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T16:24:51.691Z
Learning: In tests/unit/handlers.test.ts (structured-world/gitlab-mcp), the deferred-init timedOut guard test around lines 1767–1800 now waits via a resolveExecuteReached barrier to ensure the executeTool dispatch is reached before asserting. This replaces the prior double process.nextTick and prevents scheduler-dependent flakiness. Do not suggest switching this test back to nextTick or fake timers in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:666-669
Timestamp: 2026-04-02T10:24:42.437Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `resolveUrl(instanceUrl?: string)` method and the state-mutation methods that call it (`reportSuccess`, `reportError`, `forceReconnect`) intentionally accept an optional `instanceUrl` with a fallback to `GITLAB_BASE_URL`. This zero-arg fallback is by design for test ergonomics and is documented by the test "should use default URL for reportSuccess/reportError" (around line 716–718 of `tests/unit/services/HealthMonitor.test.ts`). All production callers in `src/handlers.ts` already pass an explicit `instanceUrl`. Do not flag the optional parameter or the GITLAB_BASE_URL fallback as unsafe or requiring a required-parameter refactor in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: tests/unit/services/HealthMonitor.test.ts:752-757
Timestamp: 2026-03-30T04:44:46.218Z
Learning: In `tests/unit/services/HealthMonitor.test.ts` (structured-world/gitlab-mcp), the degraded-detection logic in `HealthMonitor` checks **both** `version` and `schemaInfo`. In the test "should handle getInstanceInfo throwing during health check" (around line 741-757), the default mock provides schema info, so even when `getInstanceInfo()` throws during the periodic health check the instance state can remain `'healthy'`. The loose assertion `expect(state === 'degraded' || state === 'healthy').toBe(true)` is intentionally correct for this mock setup — do not flag it as needing a stricter `'degraded'`-only assertion.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:724-732
Timestamp: 2026-04-02T10:46:41.853Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `isAnyInstanceHealthy()` intentionally includes the `'connecting'` state in its truthy set (alongside `'healthy'` and `'degraded'`). During startup all instances are in `'connecting'`, and excluding it would cause `tools/list` to return context-only tools for the entire initialization window (~5s), which is bad UX. The handler-level `isInstanceReachable()` check is the real runtime gate that prevents tool execution against unreachable instances. Do not flag the inclusion of `'connecting'` in `isAnyInstanceHealthy()` as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T23:36:18.216Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), all tool calls (including non-idempotent/write tools) race against `HANDLER_TIMEOUT_MS` via `Promise.race`. There is no non-idempotent bypass — bootstrap (init + introspect) is always bounded, and the `timedOut` flag prevents late `reportSuccess`/`reportError` side effects. Do not suggest re-adding a bypass for write tools in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/services/ConnectionManager.ts:65-67
Timestamp: 2026-04-03T14:50:27.051Z
Learning: In `src/utils/url.ts` (structured-world/gitlab-mcp), `normalizeInstanceUrl()` was updated (after PR `#380` review) to strip default ports (443 for https, 80 for http) using the URL constructor, so `https://host` and `https://host:443` map to the same entry in `ConnectionManager.instances` and inflight caches. When reviewing this function, verify: (1) `new URL()` call is wrapped in try/catch to handle malformed inputs gracefully, (2) API suffix stripping runs before or independently of port normalization to preserve subpaths for self-managed instances, (3) unit tests cover `https://host:443` → `https://host` and `http://host:80` → `http://host` cases.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/registry-manager.ts:293-307
Timestamp: 2026-03-31T16:09:46.248Z
Learning: In `src/registry-manager.ts` (structured-world/gitlab-mcp), `buildToolLookupCache(instanceUrl?)` intentionally bakes one URL's tier/version/scope view into the single shared `toolLookupCache`. This is an accepted trade-off: the MCP protocol exposes a single `tools/list` per server (not per-session), so per-URL keyed caches would require session-aware tool dispatch — a larger architectural change. In single-instance mode (99% of usage) the cache is always correct. In multi-instance OAuth the last `refreshCache(url)` wins; the handler-level health gate (handlers.ts) prevents cross-instance tool execution regardless of cache state. The limitation is documented in the code comment around lines 296-302. Do not flag this singleton cache as a cross-instance overwrite bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:329-366
Timestamp: 2026-03-29T21:05:15.421Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the early-return branch for `manage_context` when the instance is **unreachable** intentionally omits `healthMonitor.reportSuccess` / `reportError`, because in that path context tools (whoami, switch_instance) operate on local state only and never call the GitLab API. However, when the instance IS reachable/healthy, `manage_context` (e.g., `whoami`) does call the GitLab API and falls through the normal execution path including `reportSuccess`/`reportError` — this is correct and intentional. Do not flag this asymmetry as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T11:32:28.956Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the first `handleStateChange` callback that fires before `InstanceRegistry` is initialized results in a silently dropped `updateConnectionStatus` call. This is documented in Known Limitations: the registry catches up on the next real state transition, and fixing the miss would require a pending-status queue — which is out of scope. Do not flag this first-transition registry miss as an unhandled bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:100-108
Timestamp: 2026-03-29T18:10:49.416Z
Learning: In structured-world/gitlab-mcp, the `ConnectionManager.isConnected()` / `getInstanceInfo()` singleton path in `HealthMonitor.ts` (`performConnect`) is intentional for single-instance mode. Multi-instance OAuth contexts use `InstanceRegistry` for per-URL state. Do not flag this singleton usage as a bug in future reviews; the two modes have separate code paths by design.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:578-603
Timestamp: 2026-03-31T10:09:51.540Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `handleStateChange` intentionally early-returns before calling `registry.updateConnectionStatus` when the state hasn't changed (`previousState === newState`). This is by design: `updateConnectionStatus` is idempotent and calling it on every periodic health-check poll (e.g., `healthy.checking → healthy.idle`) would add unnecessary overhead with no functional benefit. Do not flag this early-return ordering as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:448-457
Timestamp: 2026-03-31T13:18:57.386Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the bootstrap `catch (initError)` block (inside the `try` that calls `connectionManager.initialize()` / `getClient()` / `ensureIntrospected()`) handles the case where `!connectionManager.isConnected(effectiveInstanceUrl)`. It calls `healthMonitor.reportError(effectiveInstanceUrl, initError)` (when `!timedOut`), logs the error, and then **returns** a `createConnectionFailedError(toolName, action, effectiveInstanceUrl, healthMonitor.getState(effectiveInstanceUrl))` structured `CONNECTION_FAILED` response (with `instance_url`, `reconnecting`, and `suggested_fix`) instead of throwing `"Bad Request: Server not initialized"`. Do not flag this bootstrap path as returning a generic error or as failing to report to HealthMonitor in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/handlers.ts:462-466
Timestamp: 2026-04-03T00:39:40.483Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the two-step `hasToolHandler()`/`executeTool()` pattern at the disconnected `manage_context` bypass (around line 462) and the main dispatch path (around lines 595-616) has an acknowledged TOCTOU gap: a concurrent `refreshCache()` swap between the two calls can make `executeTool()` miss a handler. This is intentionally benign because `executeTool` falls through to the bootstrap path when the handler is not found post-refresh. An atomic `getTool()` refactor to eliminate the gap is tracked separately. Do not re-flag this two-step pattern as a TOCTOU race in future reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:529-564
Timestamp: 2026-04-02T09:32:46.067Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `HealthMonitor` creates one XState actor per monitored instance URL. Actors are only created for instances registered via `InstanceRegistry` config (1–3 in practice), so there is no unbounded actor accumulation. A follow-up issue has already been created to track per-URL eviction/inactivity TTL for future multi-instance/OAuth scenarios. Do not flag actor accumulation as an active memory leak in reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/handlers.ts:209-212
Timestamp: 2026-04-02T23:34:15.753Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), all public APIs that call `getActor(url)` — including `reportSuccess`, `reportError`, `forceReconnect`, and others — intentionally return early (no-op) when no actor exists for a URL. They do NOT lazily create actors. Untracked instance URLs are assumed reachable by `isInstanceReachable()` by design. Actors are only created explicitly via `HealthMonitor.initialize(url)`. Do not flag the missing-actor early-returns in any public API as a bug, and do not claim reportSuccess/reportError create actors for untracked URLs.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T23:36:18.216Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor (around lines 184-196) skips the post-init health probe when the remaining budget is < 500ms. When the remaining time is near-zero, the instance is assumed reachable (since init already succeeded) to prevent a false `disconnected` transition caused by an artificially short probe timeout. Do not flag skipping the health probe in this near-zero budget path as incorrect in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T16:55:06.625Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `HealthMonitor` does NOT call `InstanceRegistry.getIntrospection()` anywhere. Health and degraded-state detection is derived exclusively from `ConnectionManager.getInstanceInfo(url)` and `ConnectionManager.getSchemaInfo(url)`. Do not suggest testing `InstanceRegistry.getIntrospection()` branches in `HealthMonitor` tests in future reviews — it is not part of the health monitor's code path.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: tests/unit/handlers.test.ts:686-704
Timestamp: 2026-04-02T18:35:51.180Z
Learning: In `tests/unit/handlers.test.ts` (structured-world/gitlab-mcp), the double `process.nextTick` pattern (`await new Promise(resolve => process.nextTick(resolve))` twice) is the intentional and correct approach for flushing XState microtasks in fire-and-forget callback tests. `jest.useFakeTimers()` conflicts with XState's internal scheduling, making it unsuitable here. Do not suggest replacing this pattern with fake timers or `waitFor` in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 382
File: tests/unit/handlers.test.ts:154-156
Timestamp: 2026-04-03T21:14:08.041Z
Learning: In `tests/unit/handlers.test.ts` (structured-world/gitlab-mcp), the `tools/list_changed` notification path triggered by health state transitions is NOT asserted in unit tests because it requires `SessionManager` mock integration which is out of scope. The notification path is covered at the integration level via `broadcastToolsListChangedForStateChange` in the state-change callback tests. Do not suggest adding server notification method stubs or `tools/list_changed` assertions to this unit test file in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T15:17:48.027Z
Learning: In tests/unit/handlers.test.ts (structured-world/gitlab-mcp), when simulating a hanging operation, prefer a never-settling promise (`new Promise<never>(() => {})`) over timer-backed mocks to avoid delayed resolves leaking into subsequent tests. This pattern is now used around Lines 1255–1257.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:132-135
Timestamp: 2026-03-31T10:37:22.878Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), `resetHandlersState()` is a test-only helper that clears `healthMonitorStartup` and `stateChangeRegistered`. It is always paired with `HealthMonitor.resetInstance()` in tests. `HealthMonitor.resetInstance()` calls `shutdown()` which destroys the singleton (and all registered `onStateChange` callbacks), so the fresh singleton returned by `getInstance()` starts with an empty callbacks array. In `tests/unit/handlers.test.ts`, `HealthMonitor` is fully mocked so no real listeners accumulate. The two-call pattern guarantees no duplicate listener leaks. Do not flag `resetHandlersState()` clearing `stateChangeRegistered` as unsafe in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/services/HealthMonitor.ts:112-206
Timestamp: 2026-04-03T00:40:13.151Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor function (around lines 112–206) has a cognitive complexity of ~17 (SonarCloud limit: 15) due to three interleaved phases: fast-path health check, full initialization with timeout, and degraded-path reachability verification. The maintainer (polaz) has explicitly declined extracting these into helpers because all three phases share a single `deadline` variable that defines the unified startup budget — splitting would obscure this invariant. A code comment was added above `performConnect` explaining the intentional complexity. Do not re-flag this complexity issue in future reviews of this function.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T12:46:57.150Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor uses a dedicated `InitializationTimeoutError` class (around lines 152-166) to signal an init-timeout rejection, replacing prior fragile string-based error matching. `classifyError` in the catch block relies on this class identity to reliably distinguish init timeouts from other transient/auth/permanent failures. Do not suggest reverting to string-matching for timeout detection in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/ConnectionManager.ts:112-112
Timestamp: 2026-04-02T10:46:39.546Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `doInitialize()` (around line 112) has a cognitive complexity of ~34 (SonarCloud limit: 15) due to three interleaved init paths: OAuth best-effort unauthenticated version detection, REST-only token-scope path, and full GraphQL introspection. The maintainer (polaz) has explicitly decided NOT to extract helper methods in PR `#369` because the shared `state` mutations and identity guards would add indirection without reducing conceptual complexity. This is tracked for future refactoring. Do not re-flag this complexity issue in reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/handlers.ts:517-525
Timestamp: 2026-04-03T12:45:52.410Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), an `InitializationTimeoutError` class (added in PR `#380`) is used as the typed timeout sentinel injected by the handler-level timeout path. The `!bootstrapComplete` catch block checks `initError instanceof InitializationTimeoutError` to distinguish the injected sentinel from real bootstrap failures: real auth/permanent errors always call `healthMonitor.reportError()` (bypassing the `timedOut` guard), while the timeout sentinel itself is not re-reported since `reportError` was already called from the timeout branch. Do not suggest string-matching on error messages to detect this sentinel in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-04T07:25:22.055Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the handler-level timeout (HANDLER_TIMEOUT_MS, default 120s) intentionally uses a plain `Error` with a message containing 'timed out' rather than `InitializationTimeoutError`. This is by design: `InitializationTimeoutError` is reserved for the startup init timeout (INIT_TIMEOUT_MS, default 5s) inside the `performConnect` XState actor. Using `InitializationTimeoutError` for the handler-level timeout would be misleading in logs and health snapshots. The 'timed out' phrase in the plain Error message ensures `classifyError()` classifies it as `transient` via its free-text fallback branch. This design decision is documented in a code comment around lines 711–714 of `src/handlers.ts`. Do not suggest replacing the plain Error with `InitializationTimeoutError` for the handler-level timeout in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T15:31:04.797Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), the stale-init overwrite race in `doInitialize()` (lines ~117-163) is mitigated by identity guards: the outer `let state` binding + `this.instances.get(baseUrl) === state` check in the catch block (lines ~294-298) prevents a stale init from deleting a fresh state entry. A deeper fix using per-attempt init tokens (to prevent any intermediate writes from stale paths) requires significant refactoring across `doInitialize`/`clearInflight`/`reset` and is tracked in issue `#379`. Do not re-flag the stale-init race as unguarded in future reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 382
File: src/services/ToolAvailability.ts:487-509
Timestamp: 2026-04-03T17:08:19.978Z
Learning: In `src/services/ToolAvailability.ts` (structured-world/gitlab-mcp), the shared availability predicate (action lookup, version gate, tier check, >= 15.0 unknown-tool fallback) is intentionally duplicated between `isToolAvailable()` and `isToolAvailableForInstance()`. Consolidating both into a single private helper is tracked in issue `#379` (instanceUrl threading through ToolAvailability). Do not flag this duplication as a drift risk or suggest extracting a shared predicate in future reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T18:36:38.102Z
Learning: In `src/services/ToolAvailability.ts` (structured-world/gitlab-mcp), the singleton fallback in the `getRestrictedParameters` path around lines 347-355 (where no explicit `instanceUrl` is threaded through) is intentionally deferred. Threading explicit `instanceUrl` through all `ToolAvailability` and `WidgetAvailability` callers is tracked in issue `#379` as a separate effort. Do not flag this singleton fallback as an unfixed bug in reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T15:31:04.797Z
Learning: In `src/registry-manager.ts` (structured-world/gitlab-mcp), further Sonar cognitive-complexity splits of `buildFilteredTools` (lines ~342-386) beyond the four helpers already extracted (`loadInstanceContext`, `getToolExclusionReason`, `buildFilteredTools`, `postProcessRelatedReferences`) have been explicitly declined twice by polaz in PR `#369`. Do not re-suggest additional extraction of this function in future reviews; any remaining Sonar threshold concerns are tracked for a future follow-up PR.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T09:07:29.180Z
Learning: In `src/registry-manager.ts` (structured-world/gitlab-mcp), the broad catch in instance-context error handling (e.g., in `loadInstanceContext`) is intentionally fail-open for startup safety: unexpected errors are logged but do not crash the server or prevent tool availability. Narrowing the catch to only expected error types is tracked as a follow-up in issue `#381`. Do not flag this fail-open broad catch as incorrect in future reviews of this file.

…back intent

- Add companion test: reachable manage_context goes through bootstrap, reports health
- Expand OAuth fallback code explanation: intentional for startup health checks and
  non-OAuth transports, safe because GITLAB_BASE_URL always configured, #379 tracking
Copy link
Copy Markdown

@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

♻️ Duplicate comments (3)
src/utils/fetch.ts (1)

157-163: ⚠️ Potential issue | 🟠 Major

Always apply CONNECT_TIMEOUT_MS to requestTls in proxy mode.

On Line 162, requestTls is only set when hasTlsConfig is true. That leaves proxied upstream TLS handshake unbounded by CONNECT_TIMEOUT_MS whenever no custom TLS options are configured.

🔧 Proposed fix
-      requestTls: hasTlsConfig ? { ...tlsOptions, timeout: CONNECT_TIMEOUT_MS } : undefined,
+      requestTls: { ...(hasTlsConfig ? tlsOptions : {}), timeout: CONNECT_TIMEOUT_MS },
       proxyTls: { timeout: CONNECT_TIMEOUT_MS },
In Undici ProxyAgent (v8+), when `requestTls` is omitted, what timeout applies to the upstream TLS handshake after CONNECT, and does `proxyTls.timeout` cover that upstream handshake phase?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/fetch.ts` around lines 157 - 163, The ProxyAgent configuration
currently omits requestTls when hasTlsConfig is false, leaving the upstream TLS
handshake after CONNECT unbounded; ensure requestTls is always set in the new
undici.ProxyAgent call so CONNECT_TIMEOUT_MS is applied regardless of custom TLS
options — i.e., set requestTls to an object that always includes timeout:
CONNECT_TIMEOUT_MS and merge tlsOptions when hasTlsConfig is true (keep
proxyTls: { timeout: CONNECT_TIMEOUT_MS } as is); update the configuration where
ProxyAgent is constructed (look for ProxyAgent, requestTls, hasTlsConfig,
tlsOptions, CONNECT_TIMEOUT_MS) so upstream TLS connections after CONNECT are
bounded.
tests/unit/services/HealthMonitor.test.ts (1)

587-599: ⚠️ Potential issue | 🟡 Minor

Pin the unauthenticated probe options in these assertions.

These specs still pass if quickHealthCheck() regresses to an authenticated or rate-limited request, because they only verify the URL/method or that mockFetch ran. Please assert the skipAuth and rateLimit flags here as well.

💡 Suggested assertion updates
       expect(mockFetch).toHaveBeenCalledWith(
         `${TEST_URL}/api/v4/version`,
-        expect.objectContaining({ method: 'HEAD' }),
+        expect.objectContaining({
+          method: 'HEAD',
+          skipAuth: true,
+          rateLimit: false,
+          retry: false,
+        }),
       );
@@
-      expect(mockFetch).toHaveBeenCalled();
+      expect(mockFetch).toHaveBeenCalledWith(
+        `${TEST_URL}/api/v4/version`,
+        expect.objectContaining({
+          method: 'HEAD',
+          skipAuth: true,
+          rateLimit: false,
+          retry: false,
+        }),
+      );

As per coding guidelines, tests/**/*.test.{ts,tsx}: Use Jest for all tests and ensure all features have corresponding test coverage.

Also applies to: 824-840

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

In `@tests/unit/services/HealthMonitor.test.ts` around lines 587 - 599, The test
for health-checked connected state doesn't assert that the unauthenticated probe
options are used, so regressions in quickHealthCheck() that add authentication
or rate-limiting could still pass; update the assertions in the 'should verify
with health check when already connected' test (which calls initMonitor() and
checks monitor.getState(TEST_URL)) to expect mockFetch was called with
`${TEST_URL}/api/v4/version` and an options object that contains method: 'HEAD'
plus skipAuth: true and rateLimit: false (or the exact flag names used by
quickHealthCheck()), and add the same stronger assertion pattern to the other
affected spec range (lines ~824-840) to ensure the probe is unauthenticated and
not rate-limited.
src/services/ConnectionManager.ts (1)

452-458: ⚠️ Potential issue | 🟠 Major

Remove the currentInstanceUrl fallback from these request-scoped getters before merge.

This is still a live cross-request leak: when callers omit instanceUrl, these APIs silently read whichever URL last won currentInstanceUrl. Under concurrent OAuth traffic that can return another request’s client/version/tier and drive availability or routing decisions against the wrong instance. Please thread an explicit URL from request context through the remaining zero-arg callers, or fail fast when instanceUrl is omitted in multi-instance/OAuth paths.

Based on learnings, remaining no-arg callers in ToolAvailability, WidgetAvailability, workItemTypes, and error-handler still rely on this fallback, which can leak cross-request state under concurrent OAuth traffic.

Also applies to: 474-476, 528-613

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

In `@src/services/ConnectionManager.ts` around lines 452 - 458, The resolveState
method currently falls back to the mutable currentInstanceUrl which leaks
cross-request state; change resolveState(instanceUrl?: string) to require an
explicit instanceUrl (remove the currentInstanceUrl fallback) and throw a clear
error when instanceUrl is omitted (or when multi-instance/OAuth mode is enabled)
so callers must pass the correct URL; then update all zero-arg callers
(ToolAvailability, WidgetAvailability, workItemTypes and the error-handler
usages) to thread the explicit request-scoped URL into their calls (or propagate
the error to fail fast) and ensure normalizeInstanceUrl is applied by the caller
before calling resolveState.
🤖 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/ToolAvailability.ts`:
- Around line 450-452: getRestrictedParameters currently applies tier/version
checks even when instanceInfo.version === 'unknown', causing gated fields to be
stripped during REST-only/OAuth-deferred flows; mirror the behavior in
isToolAvailable/isToolAvailableForInstance by adding an early-return that skips
parameter restriction and returns the original/unrestricted parameters when
instanceInfo.version === 'unknown'. Locate getRestrictedParameters in
ToolAvailability.ts and add the same guard (check instanceInfo.version ===
'unknown') before any tier/version filtering logic so tools remain visible with
full schemas until version detection completes; ensure this change matches the
behavior added to isToolAvailable and isToolAvailableForInstance.

In `@tests/unit/handlers.test.ts`:
- Around line 704-731: Add assertions to the test to ensure the disconnected
manage_context fast-path never touches connection/bootstrap APIs: after parsing
the result and before restoring mocks, assert that mockRegistryManager.getClient
and mockRegistryManager.ensureIntrospected were not called (e.g.
expect(mockRegistryManager.getClient).not.toHaveBeenCalled() and
expect(mockRegistryManager.ensureIntrospected).not.toHaveBeenCalled()). This
ensures the handler used the local fast-path and did not invoke GitLab client or
introspection logic.

---

Duplicate comments:
In `@src/services/ConnectionManager.ts`:
- Around line 452-458: The resolveState method currently falls back to the
mutable currentInstanceUrl which leaks cross-request state; change
resolveState(instanceUrl?: string) to require an explicit instanceUrl (remove
the currentInstanceUrl fallback) and throw a clear error when instanceUrl is
omitted (or when multi-instance/OAuth mode is enabled) so callers must pass the
correct URL; then update all zero-arg callers (ToolAvailability,
WidgetAvailability, workItemTypes and the error-handler usages) to thread the
explicit request-scoped URL into their calls (or propagate the error to fail
fast) and ensure normalizeInstanceUrl is applied by the caller before calling
resolveState.

In `@src/utils/fetch.ts`:
- Around line 157-163: The ProxyAgent configuration currently omits requestTls
when hasTlsConfig is false, leaving the upstream TLS handshake after CONNECT
unbounded; ensure requestTls is always set in the new undici.ProxyAgent call so
CONNECT_TIMEOUT_MS is applied regardless of custom TLS options — i.e., set
requestTls to an object that always includes timeout: CONNECT_TIMEOUT_MS and
merge tlsOptions when hasTlsConfig is true (keep proxyTls: { timeout:
CONNECT_TIMEOUT_MS } as is); update the configuration where ProxyAgent is
constructed (look for ProxyAgent, requestTls, hasTlsConfig, tlsOptions,
CONNECT_TIMEOUT_MS) so upstream TLS connections after CONNECT are bounded.

In `@tests/unit/services/HealthMonitor.test.ts`:
- Around line 587-599: The test for health-checked connected state doesn't
assert that the unauthenticated probe options are used, so regressions in
quickHealthCheck() that add authentication or rate-limiting could still pass;
update the assertions in the 'should verify with health check when already
connected' test (which calls initMonitor() and checks
monitor.getState(TEST_URL)) to expect mockFetch was called with
`${TEST_URL}/api/v4/version` and an options object that contains method: 'HEAD'
plus skipAuth: true and rateLimit: false (or the exact flag names used by
quickHealthCheck()), and add the same stronger assertion pattern to the other
affected spec range (lines ~824-840) to ensure the probe is unauthenticated and
not rate-limited.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 696d6b34-f65e-471f-af74-ba346dd6c6f6

📥 Commits

Reviewing files that changed from the base of the PR and between c1039de and 4a963f2.

📒 Files selected for processing (9)
  • src/config.ts
  • src/services/ConnectionManager.ts
  • src/services/HealthMonitor.ts
  • src/services/TokenScopeDetector.ts
  • src/services/ToolAvailability.ts
  • src/utils/fetch.ts
  • tests/unit/handlers.test.ts
  • tests/unit/services/HealthMonitor.test.ts
  • tests/unit/utils/url.test.ts

@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 4, 2026

@coderabbitai

  1. Yes, please update the learning — delete the old parsePositiveInt reference and add the new parseStrictInt one.

  2. Yes, please run a final pass review. All threads from previous rounds are resolved, PR body is updated with Design Notes, and the last few commits address the remaining findings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

@polaz

  • Learning updated: I removed the older parsePositiveInt learning. The parseStrictInt learning is already saved (ID: 16bfbb18-172f-44ad-88b7-fb09b5516411), so I didn’t add a duplicate.
  • Kicking off a final pass review now.

⛔ Learnings removed
Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T00:43:19.144Z
Learning: In `src/config.ts` (structured-world/gitlab-mcp), rate-limit environment variables are parsed with two different helpers: duration/window vars (e.g., `RATE_LIMIT_IP_WINDOW_MS`, `RATE_LIMIT_SESSION_WINDOW_MS`) use `parseTimerMs()` (with upper ceiling and finite-integer validation), while max-request count vars (e.g., `RATE_LIMIT_IP_MAX_REQUESTS`, `RATE_LIMIT_SESSION_MAX_REQUESTS`) use a separate `parsePositiveInt()` guard. Do not suggest applying `parseTimerMs()` to max-request (count) vars in future reviews — timer semantics do not apply to request counts.

🧠 Learnings used
Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T00:43:19.144Z
Learning: In `src/config.ts` (structured-world/gitlab-mcp), rate-limit environment variables are parsed with two different helpers: duration/window vars (e.g., `RATE_LIMIT_IP_WINDOW_MS`, `RATE_LIMIT_SESSION_WINDOW_MS`) use `parseTimerMs()` (with upper ceiling and finite-integer validation), while max-request count vars (e.g., `RATE_LIMIT_IP_MAX_REQUESTS`, `RATE_LIMIT_SESSION_MAX_REQUESTS`) use a separate `parsePositiveInt()` guard. Do not suggest applying `parseTimerMs()` to max-request (count) vars in future reviews — timer semantics do not apply to request counts.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-04T16:24:22.255Z
Learning: In src/config.ts (structured-world/gitlab-mcp), all count-based environment variables (e.g., FAILURE_THRESHOLD, POOL_MAX_CONNECTIONS, API_RETRY_MAX_ATTEMPTS, RATE_LIMIT_IP_MAX_REQUESTS, RATE_LIMIT_SESSION_MAX_REQUESTS) use parseStrictInt(). Timer-based values continue to use parseTimerMs(). Do not suggest using parseTimerMs() for counts in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:86-95
Timestamp: 2026-03-31T07:15:54.557Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `calculateBackoffDelay` intentionally assumes `RECONNECT_BASE_DELAY_MS <= RECONNECT_MAX_DELAY_MS`. When the config is invalid (BASE > MAX), the function returns BASE as a constant delay. This is treated as a config validation concern, not a runtime bug; a doc note was added to the function rather than adding runtime normalization. Do not flag the missing bounds-swap as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T07:33:01.038Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `refreshTokenScopes()` (around lines 624-651) applies a post-await identity guard: after `await detectTokenScopes(url)` returns, it re-reads `this.instances.get(url)` and compares it to the `state` reference captured before the await. If a concurrent `reinitialize()` or `reset()` replaced the entry, it returns `false` instead of writing stale scope info into the new state. This mirrors the identity guard in `doInitialize`'s catch block. Do not flag the lack of this guard as a stale-write bug in future reviews — it is already in place.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T08:14:19.833Z
Learning: In `tests/unit/services/ConnectionManagerEnhanced.test.ts` (structured-world/gitlab-mcp), private `ConnectionManager` internals are accessed via typed `cmStatic()` and `cmInternal()` helper functions (replacing prior `as any` casts). These helpers expose `instances`, `currentInstanceUrl`, `doInitialize`, and related private fields/methods to set up intermediate states unreachable via the public API. This typed-helper pattern is the accepted approach for this test file. Do not suggest reverting to `as any` or flag the private-field access as unsafe in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T16:24:51.691Z
Learning: In tests/unit/handlers.test.ts (structured-world/gitlab-mcp), the deferred-init timedOut guard test around lines 1767–1800 now waits via a resolveExecuteReached barrier to ensure the executeTool dispatch is reached before asserting. This replaces the prior double process.nextTick and prevents scheduler-dependent flakiness. Do not suggest switching this test back to nextTick or fake timers in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:666-669
Timestamp: 2026-04-02T10:24:42.437Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `resolveUrl(instanceUrl?: string)` method and the state-mutation methods that call it (`reportSuccess`, `reportError`, `forceReconnect`) intentionally accept an optional `instanceUrl` with a fallback to `GITLAB_BASE_URL`. This zero-arg fallback is by design for test ergonomics and is documented by the test "should use default URL for reportSuccess/reportError" (around line 716–718 of `tests/unit/services/HealthMonitor.test.ts`). All production callers in `src/handlers.ts` already pass an explicit `instanceUrl`. Do not flag the optional parameter or the GITLAB_BASE_URL fallback as unsafe or requiring a required-parameter refactor in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: tests/unit/services/HealthMonitor.test.ts:752-757
Timestamp: 2026-03-30T04:44:46.218Z
Learning: In `tests/unit/services/HealthMonitor.test.ts` (structured-world/gitlab-mcp), the degraded-detection logic in `HealthMonitor` checks **both** `version` and `schemaInfo`. In the test "should handle getInstanceInfo throwing during health check" (around line 741-757), the default mock provides schema info, so even when `getInstanceInfo()` throws during the periodic health check the instance state can remain `'healthy'`. The loose assertion `expect(state === 'degraded' || state === 'healthy').toBe(true)` is intentionally correct for this mock setup — do not flag it as needing a stricter `'degraded'`-only assertion.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:724-732
Timestamp: 2026-04-02T10:46:41.853Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `isAnyInstanceHealthy()` intentionally includes the `'connecting'` state in its truthy set (alongside `'healthy'` and `'degraded'`). During startup all instances are in `'connecting'`, and excluding it would cause `tools/list` to return context-only tools for the entire initialization window (~5s), which is bad UX. The handler-level `isInstanceReachable()` check is the real runtime gate that prevents tool execution against unreachable instances. Do not flag the inclusion of `'connecting'` in `isAnyInstanceHealthy()` as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T23:36:18.216Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), all tool calls (including non-idempotent/write tools) race against `HANDLER_TIMEOUT_MS` via `Promise.race`. There is no non-idempotent bypass — bootstrap (init + introspect) is always bounded, and the `timedOut` flag prevents late `reportSuccess`/`reportError` side effects. Do not suggest re-adding a bypass for write tools in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/registry-manager.ts:293-307
Timestamp: 2026-03-31T16:09:46.248Z
Learning: In `src/registry-manager.ts` (structured-world/gitlab-mcp), `buildToolLookupCache(instanceUrl?)` intentionally bakes one URL's tier/version/scope view into the single shared `toolLookupCache`. This is an accepted trade-off: the MCP protocol exposes a single `tools/list` per server (not per-session), so per-URL keyed caches would require session-aware tool dispatch — a larger architectural change. In single-instance mode (99% of usage) the cache is always correct. In multi-instance OAuth the last `refreshCache(url)` wins; the handler-level health gate (handlers.ts) prevents cross-instance tool execution regardless of cache state. The limitation is documented in the code comment around lines 296-302. Do not flag this singleton cache as a cross-instance overwrite bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:329-366
Timestamp: 2026-03-29T21:05:15.421Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the early-return branch for `manage_context` when the instance is **unreachable** intentionally omits `healthMonitor.reportSuccess` / `reportError`, because in that path context tools (whoami, switch_instance) operate on local state only and never call the GitLab API. However, when the instance IS reachable/healthy, `manage_context` (e.g., `whoami`) does call the GitLab API and falls through the normal execution path including `reportSuccess`/`reportError` — this is correct and intentional. Do not flag this asymmetry as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T11:32:28.956Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the first `handleStateChange` callback that fires before `InstanceRegistry` is initialized results in a silently dropped `updateConnectionStatus` call. This is documented in Known Limitations: the registry catches up on the next real state transition, and fixing the miss would require a pending-status queue — which is out of scope. Do not flag this first-transition registry miss as an unhandled bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/services/ConnectionManager.ts:65-67
Timestamp: 2026-04-03T14:50:27.051Z
Learning: In `src/utils/url.ts` (structured-world/gitlab-mcp), `normalizeInstanceUrl()` was updated (after PR `#380` review) to strip default ports (443 for https, 80 for http) using the URL constructor, so `https://host` and `https://host:443` map to the same entry in `ConnectionManager.instances` and inflight caches. When reviewing this function, verify: (1) `new URL()` call is wrapped in try/catch to handle malformed inputs gracefully, (2) API suffix stripping runs before or independently of port normalization to preserve subpaths for self-managed instances, (3) unit tests cover `https://host:443` → `https://host` and `http://host:80` → `http://host` cases.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:100-108
Timestamp: 2026-03-29T18:10:49.416Z
Learning: In structured-world/gitlab-mcp, the `ConnectionManager.isConnected()` / `getInstanceInfo()` singleton path in `HealthMonitor.ts` (`performConnect`) is intentional for single-instance mode. Multi-instance OAuth contexts use `InstanceRegistry` for per-URL state. Do not flag this singleton usage as a bug in future reviews; the two modes have separate code paths by design.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:448-457
Timestamp: 2026-03-31T13:18:57.386Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the bootstrap `catch (initError)` block (inside the `try` that calls `connectionManager.initialize()` / `getClient()` / `ensureIntrospected()`) handles the case where `!connectionManager.isConnected(effectiveInstanceUrl)`. It calls `healthMonitor.reportError(effectiveInstanceUrl, initError)` (when `!timedOut`), logs the error, and then **returns** a `createConnectionFailedError(toolName, action, effectiveInstanceUrl, healthMonitor.getState(effectiveInstanceUrl))` structured `CONNECTION_FAILED` response (with `instance_url`, `reconnecting`, and `suggested_fix`) instead of throwing `"Bad Request: Server not initialized"`. Do not flag this bootstrap path as returning a generic error or as failing to report to HealthMonitor in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/handlers.ts:462-466
Timestamp: 2026-04-03T00:39:40.483Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the two-step `hasToolHandler()`/`executeTool()` pattern at the disconnected `manage_context` bypass (around line 462) and the main dispatch path (around lines 595-616) has an acknowledged TOCTOU gap: a concurrent `refreshCache()` swap between the two calls can make `executeTool()` miss a handler. This is intentionally benign because `executeTool` falls through to the bootstrap path when the handler is not found post-refresh. An atomic `getTool()` refactor to eliminate the gap is tracked separately. Do not re-flag this two-step pattern as a TOCTOU race in future reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:578-603
Timestamp: 2026-03-31T10:09:51.540Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `handleStateChange` intentionally early-returns before calling `registry.updateConnectionStatus` when the state hasn't changed (`previousState === newState`). This is by design: `updateConnectionStatus` is idempotent and calling it on every periodic health-check poll (e.g., `healthy.checking → healthy.idle`) would add unnecessary overhead with no functional benefit. Do not flag this early-return ordering as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T15:31:04.797Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), the stale-init overwrite race in `doInitialize()` (lines ~117-163) is mitigated by identity guards: the outer `let state` binding + `this.instances.get(baseUrl) === state` check in the catch block (lines ~294-298) prevents a stale init from deleting a fresh state entry. A deeper fix using per-attempt init tokens (to prevent any intermediate writes from stale paths) requires significant refactoring across `doInitialize`/`clearInflight`/`reset` and is tracked in issue `#379`. Do not re-flag the stale-init race as unguarded in future reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:529-564
Timestamp: 2026-04-02T09:32:46.067Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `HealthMonitor` creates one XState actor per monitored instance URL. Actors are only created for instances registered via `InstanceRegistry` config (1–3 in practice), so there is no unbounded actor accumulation. A follow-up issue has already been created to track per-URL eviction/inactivity TTL for future multi-instance/OAuth scenarios. Do not flag actor accumulation as an active memory leak in reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/handlers.ts:209-212
Timestamp: 2026-04-02T23:34:15.753Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), all public APIs that call `getActor(url)` — including `reportSuccess`, `reportError`, `forceReconnect`, and others — intentionally return early (no-op) when no actor exists for a URL. They do NOT lazily create actors. Untracked instance URLs are assumed reachable by `isInstanceReachable()` by design. Actors are only created explicitly via `HealthMonitor.initialize(url)`. Do not flag the missing-actor early-returns in any public API as a bug, and do not claim reportSuccess/reportError create actors for untracked URLs.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T23:36:18.216Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor (around lines 184-196) skips the post-init health probe when the remaining budget is < 500ms. When the remaining time is near-zero, the instance is assumed reachable (since init already succeeded) to prevent a false `disconnected` transition caused by an artificially short probe timeout. Do not flag skipping the health probe in this near-zero budget path as incorrect in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T16:55:06.625Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `HealthMonitor` does NOT call `InstanceRegistry.getIntrospection()` anywhere. Health and degraded-state detection is derived exclusively from `ConnectionManager.getInstanceInfo(url)` and `ConnectionManager.getSchemaInfo(url)`. Do not suggest testing `InstanceRegistry.getIntrospection()` branches in `HealthMonitor` tests in future reviews — it is not part of the health monitor's code path.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: tests/unit/handlers.test.ts:686-704
Timestamp: 2026-04-02T18:35:51.180Z
Learning: In `tests/unit/handlers.test.ts` (structured-world/gitlab-mcp), the double `process.nextTick` pattern (`await new Promise(resolve => process.nextTick(resolve))` twice) is the intentional and correct approach for flushing XState microtasks in fire-and-forget callback tests. `jest.useFakeTimers()` conflicts with XState's internal scheduling, making it unsuitable here. Do not suggest replacing this pattern with fake timers or `waitFor` in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 382
File: tests/unit/handlers.test.ts:154-156
Timestamp: 2026-04-03T21:14:08.041Z
Learning: In `tests/unit/handlers.test.ts` (structured-world/gitlab-mcp), the `tools/list_changed` notification path triggered by health state transitions is NOT asserted in unit tests because it requires `SessionManager` mock integration which is out of scope. The notification path is covered at the integration level via `broadcastToolsListChangedForStateChange` in the state-change callback tests. Do not suggest adding server notification method stubs or `tools/list_changed` assertions to this unit test file in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T15:17:48.027Z
Learning: In tests/unit/handlers.test.ts (structured-world/gitlab-mcp), when simulating a hanging operation, prefer a never-settling promise (`new Promise<never>(() => {})`) over timer-backed mocks to avoid delayed resolves leaking into subsequent tests. This pattern is now used around Lines 1255–1257.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:132-135
Timestamp: 2026-03-31T10:37:22.878Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), `resetHandlersState()` is a test-only helper that clears `healthMonitorStartup` and `stateChangeRegistered`. It is always paired with `HealthMonitor.resetInstance()` in tests. `HealthMonitor.resetInstance()` calls `shutdown()` which destroys the singleton (and all registered `onStateChange` callbacks), so the fresh singleton returned by `getInstance()` starts with an empty callbacks array. In `tests/unit/handlers.test.ts`, `HealthMonitor` is fully mocked so no real listeners accumulate. The two-call pattern guarantees no duplicate listener leaks. Do not flag `resetHandlersState()` clearing `stateChangeRegistered` as unsafe in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/services/HealthMonitor.ts:112-206
Timestamp: 2026-04-03T00:40:13.151Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor function (around lines 112–206) has a cognitive complexity of ~17 (SonarCloud limit: 15) due to three interleaved phases: fast-path health check, full initialization with timeout, and degraded-path reachability verification. The maintainer (polaz) has explicitly declined extracting these into helpers because all three phases share a single `deadline` variable that defines the unified startup budget — splitting would obscure this invariant. A code comment was added above `performConnect` explaining the intentional complexity. Do not re-flag this complexity issue in future reviews of this function.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T12:46:57.150Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor uses a dedicated `InitializationTimeoutError` class (around lines 152-166) to signal an init-timeout rejection, replacing prior fragile string-based error matching. `classifyError` in the catch block relies on this class identity to reliably distinguish init timeouts from other transient/auth/permanent failures. Do not suggest reverting to string-matching for timeout detection in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/ConnectionManager.ts:112-112
Timestamp: 2026-04-02T10:46:39.546Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `doInitialize()` (around line 112) has a cognitive complexity of ~34 (SonarCloud limit: 15) due to three interleaved init paths: OAuth best-effort unauthenticated version detection, REST-only token-scope path, and full GraphQL introspection. The maintainer (polaz) has explicitly decided NOT to extract helper methods in PR `#369` because the shared `state` mutations and identity guards would add indirection without reducing conceptual complexity. This is tracked for future refactoring. Do not re-flag this complexity issue in reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/handlers.ts:517-525
Timestamp: 2026-04-03T12:45:52.410Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), an `InitializationTimeoutError` class (added in PR `#380`) is used as the typed timeout sentinel injected by the handler-level timeout path. The `!bootstrapComplete` catch block checks `initError instanceof InitializationTimeoutError` to distinguish the injected sentinel from real bootstrap failures: real auth/permanent errors always call `healthMonitor.reportError()` (bypassing the `timedOut` guard), while the timeout sentinel itself is not re-reported since `reportError` was already called from the timeout branch. Do not suggest string-matching on error messages to detect this sentinel in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-04T07:25:22.055Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the handler-level timeout (HANDLER_TIMEOUT_MS, default 120s) intentionally uses a plain `Error` with a message containing 'timed out' rather than `InitializationTimeoutError`. This is by design: `InitializationTimeoutError` is reserved for the startup init timeout (INIT_TIMEOUT_MS, default 5s) inside the `performConnect` XState actor. Using `InitializationTimeoutError` for the handler-level timeout would be misleading in logs and health snapshots. The 'timed out' phrase in the plain Error message ensures `classifyError()` classifies it as `transient` via its free-text fallback branch. This design decision is documented in a code comment around lines 711–714 of `src/handlers.ts`. Do not suggest replacing the plain Error with `InitializationTimeoutError` for the handler-level timeout in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T09:07:29.180Z
Learning: In `src/registry-manager.ts` (structured-world/gitlab-mcp), the broad catch in instance-context error handling (e.g., in `loadInstanceContext`) is intentionally fail-open for startup safety: unexpected errors are logged but do not crash the server or prevent tool availability. Narrowing the catch to only expected error types is tracked as a follow-up in issue `#381`. Do not flag this fail-open broad catch as incorrect in future reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T15:31:04.797Z
Learning: In `src/registry-manager.ts` (structured-world/gitlab-mcp), further Sonar cognitive-complexity splits of `buildFilteredTools` (lines ~342-386) beyond the four helpers already extracted (`loadInstanceContext`, `getToolExclusionReason`, `buildFilteredTools`, `postProcessRelatedReferences`) have been explicitly declined twice by polaz in PR `#369`. Do not re-suggest additional extraction of this function in future reviews; any remaining Sonar threshold concerns are tracked for a future follow-up PR.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 4, 2026

@coderabbitai Duplicate items from latest review:

  1. fetch.ts:157-163 requestTls always with timeout — Fixed: requestTls now always includes timeout: CONNECT_TIMEOUT_MS even without custom TLS config.
  2. HealthMonitor.test.ts:587-599 probe assertions — Already fixed in previous commit (URL + method HEAD asserted).
  3. 3rd dup — Verified stale / already addressed.

…lways set requestTls timeout

- getRestrictedParameters returns [] for version=unknown (consistent with
  isToolAvailable/isToolAvailableForInstance) — both cached and singleton paths
- requestTls always includes CONNECT_TIMEOUT_MS even without custom TLS config
  to bound upstream TLS handshake in proxy mode
- Assert disconnected manage_context skips getClient/ensureIntrospected
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@polaz Thanks — I double-checked both items:

  • fetch.ts requestTls timeout: Confirmed that requestTls is always built with timeout: CONNECT_TIMEOUT_MS regardless of whether custom TLS options are present. This pins a per-request handshake/request-level timeout even on the non-proxy path.
  • HealthMonitor.test.ts probe assertions: Confirmed the quickHealthCheck mock assertions include the exact URL (/api/v4/version) and method: 'HEAD'.

No other dupes outstanding from this round on my side. Nice cleanup.


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== File presence =="
fd -i --full-path 'src/utils/fetch.ts' || true
fd -i --full-path 'tests/unit/services/HealthMonitor.test.ts' || true

echo
echo "== fetch.ts: requestTls construction and timeout usage =="
# Show all occurrences of requestTls and the CONNECT_TIMEOUT_MS assignment with context
rg -n --color=never -C4 '\brequestTls\b' src/utils/fetch.ts || true
rg -n --color=never -C2 'timeout:\s*CONNECT_TIMEOUT_MS' src/utils/fetch.ts || true

echo
echo "== HealthMonitor.test.ts: probe assertions for URL + method HEAD =="
# Show assertions referencing /api/v4/version and method: 'HEAD'
rg -n --color=never -C4 '/api/v4/version' tests/unit/services/HealthMonitor.test.ts || true
rg -n --color=never -C4 "method:\s*'HEAD'" tests/unit/services/HealthMonitor.test.ts || true

Length of output: 2536


@polaz Thanks for posting the run output — here’s what I see:

  1. fetch.ts requestTls timeout
  • In the ProxyAgent path, requestTls is still conditional on hasTlsConfig:
    requestTls: hasTlsConfig ? { ...tlsOptions, timeout: CONNECT_TIMEOUT_MS } : undefined,
  • That means when no custom TLS options are present, ProxyAgent won’t get a requestTls timeout. The non-proxy path is already covered via connectOptions.timeout: CONNECT_TIMEOUT_MS.

Proposed minimal fix (keeps existing merges, but always supplies a timeout object):

--- a/src/utils/fetch.ts
+++ b/src/utils/fetch.ts
@@
   return new undici.ProxyAgent({
     uri: proxyUrl,
-    // requestTls: upstream TLS handshake after CONNECT tunnel established
-    // proxyTls: TLS handshake to the proxy itself (if HTTPS proxy)
-    // Both need connect timeout to fully bound HTTPS-over-proxy connections.
-    requestTls: hasTlsConfig ? { ...tlsOptions, timeout: CONNECT_TIMEOUT_MS } : undefined,
+    // requestTls: upstream TLS handshake after CONNECT tunnel established
+    // proxyTls: TLS handshake to the proxy itself (if HTTPS proxy)
+    // Both need connect timeout to fully bound HTTPS-over-proxy connections.
+    requestTls: tlsOptions
+      ? { ...tlsOptions, timeout: CONNECT_TIMEOUT_MS }
+      : { timeout: CONNECT_TIMEOUT_MS },
     proxyTls: { timeout: CONNECT_TIMEOUT_MS },
     headersTimeout: HEADERS_TIMEOUT_MS,
     bodyTimeout: BODY_TIMEOUT_MS,
   });

Optional test hardening: in tests/unit/utils/fetch-config.test.ts, extend the ProxyAgent Timeout Configuration assertion to include requestTls: expect.objectContaining({ timeout: CONNECT_TIMEOUT_MS }).

  1. HealthMonitor.test.ts probe assertions
  • Confirmed: asserts URL ${TEST_URL}/api/v4/version and method: 'HEAD' as expected.
  1. Third duplicate
  • Acknowledged as stale/already addressed.

One more housekeeping item: I previously saved a learning stating “requestTls is now always constructed with timeout”. Given the current code, that learning is premature. If you confirm, I’ll remove that learning entry to avoid confusion in future reviews.


✏️ Learnings added
Learnt from: polaz
URL: https://github.com/structured-world/gitlab-mcp/pull/382

Timestamp: 2026-04-04T16:40:00.046Z
Learning: In src/utils/fetch.ts (structured-world/gitlab-mcp), requestTls is now always constructed with timeout: CONNECT_TIMEOUT_MS, even when no custom TLS options are provided (non-proxy path included). Do not suggest adding conditional timeouts for requestTls in future reviews; the unconditional timeout is already in place.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-04T16:24:22.255Z
Learning: In src/config.ts (structured-world/gitlab-mcp), all count-based environment variables (e.g., FAILURE_THRESHOLD, POOL_MAX_CONNECTIONS, API_RETRY_MAX_ATTEMPTS, RATE_LIMIT_IP_MAX_REQUESTS, RATE_LIMIT_SESSION_MAX_REQUESTS) use parseStrictInt(). Timer-based values continue to use parseTimerMs(). Do not suggest using parseTimerMs() for counts in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:86-95
Timestamp: 2026-03-31T07:15:54.557Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `calculateBackoffDelay` intentionally assumes `RECONNECT_BASE_DELAY_MS <= RECONNECT_MAX_DELAY_MS`. When the config is invalid (BASE > MAX), the function returns BASE as a constant delay. This is treated as a config validation concern, not a runtime bug; a doc note was added to the function rather than adding runtime normalization. Do not flag the missing bounds-swap as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T07:33:01.038Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `refreshTokenScopes()` (around lines 624-651) applies a post-await identity guard: after `await detectTokenScopes(url)` returns, it re-reads `this.instances.get(url)` and compares it to the `state` reference captured before the await. If a concurrent `reinitialize()` or `reset()` replaced the entry, it returns `false` instead of writing stale scope info into the new state. This mirrors the identity guard in `doInitialize`'s catch block. Do not flag the lack of this guard as a stale-write bug in future reviews — it is already in place.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T08:14:19.833Z
Learning: In `tests/unit/services/ConnectionManagerEnhanced.test.ts` (structured-world/gitlab-mcp), private `ConnectionManager` internals are accessed via typed `cmStatic()` and `cmInternal()` helper functions (replacing prior `as any` casts). These helpers expose `instances`, `currentInstanceUrl`, `doInitialize`, and related private fields/methods to set up intermediate states unreachable via the public API. This typed-helper pattern is the accepted approach for this test file. Do not suggest reverting to `as any` or flag the private-field access as unsafe in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:666-669
Timestamp: 2026-04-02T10:24:42.437Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `resolveUrl(instanceUrl?: string)` method and the state-mutation methods that call it (`reportSuccess`, `reportError`, `forceReconnect`) intentionally accept an optional `instanceUrl` with a fallback to `GITLAB_BASE_URL`. This zero-arg fallback is by design for test ergonomics and is documented by the test "should use default URL for reportSuccess/reportError" (around line 716–718 of `tests/unit/services/HealthMonitor.test.ts`). All production callers in `src/handlers.ts` already pass an explicit `instanceUrl`. Do not flag the optional parameter or the GITLAB_BASE_URL fallback as unsafe or requiring a required-parameter refactor in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T16:24:51.691Z
Learning: In tests/unit/handlers.test.ts (structured-world/gitlab-mcp), the deferred-init timedOut guard test around lines 1767–1800 now waits via a resolveExecuteReached barrier to ensure the executeTool dispatch is reached before asserting. This replaces the prior double process.nextTick and prevents scheduler-dependent flakiness. Do not suggest switching this test back to nextTick or fake timers in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: tests/unit/services/HealthMonitor.test.ts:752-757
Timestamp: 2026-03-30T04:44:46.218Z
Learning: In `tests/unit/services/HealthMonitor.test.ts` (structured-world/gitlab-mcp), the degraded-detection logic in `HealthMonitor` checks **both** `version` and `schemaInfo`. In the test "should handle getInstanceInfo throwing during health check" (around line 741-757), the default mock provides schema info, so even when `getInstanceInfo()` throws during the periodic health check the instance state can remain `'healthy'`. The loose assertion `expect(state === 'degraded' || state === 'healthy').toBe(true)` is intentionally correct for this mock setup — do not flag it as needing a stricter `'degraded'`-only assertion.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:724-732
Timestamp: 2026-04-02T10:46:41.853Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `isAnyInstanceHealthy()` intentionally includes the `'connecting'` state in its truthy set (alongside `'healthy'` and `'degraded'`). During startup all instances are in `'connecting'`, and excluding it would cause `tools/list` to return context-only tools for the entire initialization window (~5s), which is bad UX. The handler-level `isInstanceReachable()` check is the real runtime gate that prevents tool execution against unreachable instances. Do not flag the inclusion of `'connecting'` in `isAnyInstanceHealthy()` as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T23:36:18.216Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), all tool calls (including non-idempotent/write tools) race against `HANDLER_TIMEOUT_MS` via `Promise.race`. There is no non-idempotent bypass — bootstrap (init + introspect) is always bounded, and the `timedOut` flag prevents late `reportSuccess`/`reportError` side effects. Do not suggest re-adding a bypass for write tools in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/services/ConnectionManager.ts:65-67
Timestamp: 2026-04-03T14:50:27.051Z
Learning: In `src/utils/url.ts` (structured-world/gitlab-mcp), `normalizeInstanceUrl()` was updated (after PR `#380` review) to strip default ports (443 for https, 80 for http) using the URL constructor, so `https://host` and `https://host:443` map to the same entry in `ConnectionManager.instances` and inflight caches. When reviewing this function, verify: (1) `new URL()` call is wrapped in try/catch to handle malformed inputs gracefully, (2) API suffix stripping runs before or independently of port normalization to preserve subpaths for self-managed instances, (3) unit tests cover `https://host:443` → `https://host` and `http://host:80` → `http://host` cases.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/registry-manager.ts:293-307
Timestamp: 2026-03-31T16:09:46.248Z
Learning: In `src/registry-manager.ts` (structured-world/gitlab-mcp), `buildToolLookupCache(instanceUrl?)` intentionally bakes one URL's tier/version/scope view into the single shared `toolLookupCache`. This is an accepted trade-off: the MCP protocol exposes a single `tools/list` per server (not per-session), so per-URL keyed caches would require session-aware tool dispatch — a larger architectural change. In single-instance mode (99% of usage) the cache is always correct. In multi-instance OAuth the last `refreshCache(url)` wins; the handler-level health gate (handlers.ts) prevents cross-instance tool execution regardless of cache state. The limitation is documented in the code comment around lines 296-302. Do not flag this singleton cache as a cross-instance overwrite bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:329-366
Timestamp: 2026-03-29T21:05:15.421Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the early-return branch for `manage_context` when the instance is **unreachable** intentionally omits `healthMonitor.reportSuccess` / `reportError`, because in that path context tools (whoami, switch_instance) operate on local state only and never call the GitLab API. However, when the instance IS reachable/healthy, `manage_context` (e.g., `whoami`) does call the GitLab API and falls through the normal execution path including `reportSuccess`/`reportError` — this is correct and intentional. Do not flag this asymmetry as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T11:32:28.956Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the first `handleStateChange` callback that fires before `InstanceRegistry` is initialized results in a silently dropped `updateConnectionStatus` call. This is documented in Known Limitations: the registry catches up on the next real state transition, and fixing the miss would require a pending-status queue — which is out of scope. Do not flag this first-transition registry miss as an unhandled bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:100-108
Timestamp: 2026-03-29T18:10:49.416Z
Learning: In structured-world/gitlab-mcp, the `ConnectionManager.isConnected()` / `getInstanceInfo()` singleton path in `HealthMonitor.ts` (`performConnect`) is intentional for single-instance mode. Multi-instance OAuth contexts use `InstanceRegistry` for per-URL state. Do not flag this singleton usage as a bug in future reviews; the two modes have separate code paths by design.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:578-603
Timestamp: 2026-03-31T10:09:51.540Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `handleStateChange` intentionally early-returns before calling `registry.updateConnectionStatus` when the state hasn't changed (`previousState === newState`). This is by design: `updateConnectionStatus` is idempotent and calling it on every periodic health-check poll (e.g., `healthy.checking → healthy.idle`) would add unnecessary overhead with no functional benefit. Do not flag this early-return ordering as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:448-457
Timestamp: 2026-03-31T13:18:57.386Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the bootstrap `catch (initError)` block (inside the `try` that calls `connectionManager.initialize()` / `getClient()` / `ensureIntrospected()`) handles the case where `!connectionManager.isConnected(effectiveInstanceUrl)`. It calls `healthMonitor.reportError(effectiveInstanceUrl, initError)` (when `!timedOut`), logs the error, and then **returns** a `createConnectionFailedError(toolName, action, effectiveInstanceUrl, healthMonitor.getState(effectiveInstanceUrl))` structured `CONNECTION_FAILED` response (with `instance_url`, `reconnecting`, and `suggested_fix`) instead of throwing `"Bad Request: Server not initialized"`. Do not flag this bootstrap path as returning a generic error or as failing to report to HealthMonitor in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/handlers.ts:462-466
Timestamp: 2026-04-03T00:39:40.483Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the two-step `hasToolHandler()`/`executeTool()` pattern at the disconnected `manage_context` bypass (around line 462) and the main dispatch path (around lines 595-616) has an acknowledged TOCTOU gap: a concurrent `refreshCache()` swap between the two calls can make `executeTool()` miss a handler. This is intentionally benign because `executeTool` falls through to the bootstrap path when the handler is not found post-refresh. An atomic `getTool()` refactor to eliminate the gap is tracked separately. Do not re-flag this two-step pattern as a TOCTOU race in future reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:529-564
Timestamp: 2026-04-02T09:32:46.067Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `HealthMonitor` creates one XState actor per monitored instance URL. Actors are only created for instances registered via `InstanceRegistry` config (1–3 in practice), so there is no unbounded actor accumulation. A follow-up issue has already been created to track per-URL eviction/inactivity TTL for future multi-instance/OAuth scenarios. Do not flag actor accumulation as an active memory leak in reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/handlers.ts:209-212
Timestamp: 2026-04-02T23:34:15.753Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), all public APIs that call `getActor(url)` — including `reportSuccess`, `reportError`, `forceReconnect`, and others — intentionally return early (no-op) when no actor exists for a URL. They do NOT lazily create actors. Untracked instance URLs are assumed reachable by `isInstanceReachable()` by design. Actors are only created explicitly via `HealthMonitor.initialize(url)`. Do not flag the missing-actor early-returns in any public API as a bug, and do not claim reportSuccess/reportError create actors for untracked URLs.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T23:36:18.216Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor (around lines 184-196) skips the post-init health probe when the remaining budget is < 500ms. When the remaining time is near-zero, the instance is assumed reachable (since init already succeeded) to prevent a false `disconnected` transition caused by an artificially short probe timeout. Do not flag skipping the health probe in this near-zero budget path as incorrect in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T16:55:06.625Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `HealthMonitor` does NOT call `InstanceRegistry.getIntrospection()` anywhere. Health and degraded-state detection is derived exclusively from `ConnectionManager.getInstanceInfo(url)` and `ConnectionManager.getSchemaInfo(url)`. Do not suggest testing `InstanceRegistry.getIntrospection()` branches in `HealthMonitor` tests in future reviews — it is not part of the health monitor's code path.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: tests/unit/handlers.test.ts:686-704
Timestamp: 2026-04-02T18:35:51.180Z
Learning: In `tests/unit/handlers.test.ts` (structured-world/gitlab-mcp), the double `process.nextTick` pattern (`await new Promise(resolve => process.nextTick(resolve))` twice) is the intentional and correct approach for flushing XState microtasks in fire-and-forget callback tests. `jest.useFakeTimers()` conflicts with XState's internal scheduling, making it unsuitable here. Do not suggest replacing this pattern with fake timers or `waitFor` in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 382
File: tests/unit/handlers.test.ts:154-156
Timestamp: 2026-04-03T21:14:08.041Z
Learning: In `tests/unit/handlers.test.ts` (structured-world/gitlab-mcp), the `tools/list_changed` notification path triggered by health state transitions is NOT asserted in unit tests because it requires `SessionManager` mock integration which is out of scope. The notification path is covered at the integration level via `broadcastToolsListChangedForStateChange` in the state-change callback tests. Do not suggest adding server notification method stubs or `tools/list_changed` assertions to this unit test file in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T15:17:48.027Z
Learning: In tests/unit/handlers.test.ts (structured-world/gitlab-mcp), when simulating a hanging operation, prefer a never-settling promise (`new Promise<never>(() => {})`) over timer-backed mocks to avoid delayed resolves leaking into subsequent tests. This pattern is now used around Lines 1255–1257.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:132-135
Timestamp: 2026-03-31T10:37:22.878Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), `resetHandlersState()` is a test-only helper that clears `healthMonitorStartup` and `stateChangeRegistered`. It is always paired with `HealthMonitor.resetInstance()` in tests. `HealthMonitor.resetInstance()` calls `shutdown()` which destroys the singleton (and all registered `onStateChange` callbacks), so the fresh singleton returned by `getInstance()` starts with an empty callbacks array. In `tests/unit/handlers.test.ts`, `HealthMonitor` is fully mocked so no real listeners accumulate. The two-call pattern guarantees no duplicate listener leaks. Do not flag `resetHandlersState()` clearing `stateChangeRegistered` as unsafe in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/services/HealthMonitor.ts:112-206
Timestamp: 2026-04-03T00:40:13.151Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor function (around lines 112–206) has a cognitive complexity of ~17 (SonarCloud limit: 15) due to three interleaved phases: fast-path health check, full initialization with timeout, and degraded-path reachability verification. The maintainer (polaz) has explicitly declined extracting these into helpers because all three phases share a single `deadline` variable that defines the unified startup budget — splitting would obscure this invariant. A code comment was added above `performConnect` explaining the intentional complexity. Do not re-flag this complexity issue in future reviews of this function.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T12:46:57.150Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor uses a dedicated `InitializationTimeoutError` class (around lines 152-166) to signal an init-timeout rejection, replacing prior fragile string-based error matching. `classifyError` in the catch block relies on this class identity to reliably distinguish init timeouts from other transient/auth/permanent failures. Do not suggest reverting to string-matching for timeout detection in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/ConnectionManager.ts:112-112
Timestamp: 2026-04-02T10:46:39.546Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `doInitialize()` (around line 112) has a cognitive complexity of ~34 (SonarCloud limit: 15) due to three interleaved init paths: OAuth best-effort unauthenticated version detection, REST-only token-scope path, and full GraphQL introspection. The maintainer (polaz) has explicitly decided NOT to extract helper methods in PR `#369` because the shared `state` mutations and identity guards would add indirection without reducing conceptual complexity. This is tracked for future refactoring. Do not re-flag this complexity issue in reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/handlers.ts:517-525
Timestamp: 2026-04-03T12:45:52.410Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), an `InitializationTimeoutError` class (added in PR `#380`) is used as the typed timeout sentinel injected by the handler-level timeout path. The `!bootstrapComplete` catch block checks `initError instanceof InitializationTimeoutError` to distinguish the injected sentinel from real bootstrap failures: real auth/permanent errors always call `healthMonitor.reportError()` (bypassing the `timedOut` guard), while the timeout sentinel itself is not re-reported since `reportError` was already called from the timeout branch. Do not suggest string-matching on error messages to detect this sentinel in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-04T07:25:22.055Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the handler-level timeout (HANDLER_TIMEOUT_MS, default 120s) intentionally uses a plain `Error` with a message containing 'timed out' rather than `InitializationTimeoutError`. This is by design: `InitializationTimeoutError` is reserved for the startup init timeout (INIT_TIMEOUT_MS, default 5s) inside the `performConnect` XState actor. Using `InitializationTimeoutError` for the handler-level timeout would be misleading in logs and health snapshots. The 'timed out' phrase in the plain Error message ensures `classifyError()` classifies it as `transient` via its free-text fallback branch. This design decision is documented in a code comment around lines 711–714 of `src/handlers.ts`. Do not suggest replacing the plain Error with `InitializationTimeoutError` for the handler-level timeout in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T15:31:04.797Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), the stale-init overwrite race in `doInitialize()` (lines ~117-163) is mitigated by identity guards: the outer `let state` binding + `this.instances.get(baseUrl) === state` check in the catch block (lines ~294-298) prevents a stale init from deleting a fresh state entry. A deeper fix using per-attempt init tokens (to prevent any intermediate writes from stale paths) requires significant refactoring across `doInitialize`/`clearInflight`/`reset` and is tracked in issue `#379`. Do not re-flag the stale-init race as unguarded in future reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-03-31T07:51:52.620Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `isConnected(instanceUrl?: string)` is a per-URL initialization flag (reads `state?.isInitialized` from `Map<string, InstanceState>`), explicitly documented as NOT a live connectivity check. Use `HealthMonitor.isInstanceReachable()` for runtime reachability. Do not flag `isConnected()` as an unsafe health signal in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-03-31T07:51:52.620Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `clearInflight(instanceUrl: string)` (around line 761) removes the inflight `initializePromises` entry for a URL. HealthMonitor calls this after an init timeout so reconnect attempts start a fresh `doInitialize()` instead of re-awaiting the hung promise. The `initialize()` method's `finally` block guards `initializePromises.delete(url)` with an identity check to prevent a retry's fresh promise from being wiped by a stale `clearInflight` call.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T08:14:19.833Z
Learning: In `src/utils/fetch.ts` (structured-world/gitlab-mcp), `skipAuth: true` suppresses BOTH `getAuthHeaders()` AND `loadCookieHeader()` in the internal `doFetch()` call. This ensures unauthenticated probes (e.g., HEAD /api/v4/version health checks) carry no credentials or session cookies. Do not flag missing cookie suppression for `skipAuth` paths in future reviews — it is already handled.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:215-216
Timestamp: 2026-03-30T21:18:53.194Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `quickHealthCheck` is intentionally a reachability-only probe — `response.status < 500` is the correct condition because any HTTP response (including 400/404) proves the server is responding. It is NOT an API validation check. Actual API correctness (correct base URL, valid endpoints, auth) is verified at tool execution level. Do not flag this threshold as incorrect in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:145-160
Timestamp: 2026-03-29T19:15:31.468Z
Learning: In structured-world/gitlab-mcp, the periodic health check in `src/services/HealthMonitor.ts` (`performHealthCheck`) is intentionally unauthenticated (HEAD /api/v4/version). Token revocation / authenticated health-check detection is intentionally out of scope for PR `#369` (startup resilience) and is tracked separately in issue `#370`. Do not flag the unauthenticated health probe as a bug in reviews of this file/PR.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 4, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
4.5% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 4, 2026

@coderabbitai

  1. fetch.ts requestTls — Already fixed in commit c9175cd. Current code: requestTls: { ...(hasTlsConfig ? tlsOptions : {}), timeout: CONNECT_TIMEOUT_MS } — always sets timeout regardless of TLS config. Same intent as your proposed fix.

  2. HealthMonitor probe — Confirmed stale.

  3. Third dup — Confirmed stale.

  4. Learning removal — Yes, please remove the premature "requestTls always constructed with timeout" learning. The current code is correct — requestTls always includes timeout but conditionally spreads tlsOptions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

Tip

For best results, initiate chat on the files or code changes.

@polaz

  • Acknowledged: fetch.ts fix in commit c9175cd makes requestTls always include timeout while conditionally spreading tlsOptions — matches the intended shape.
  • HealthMonitor probe and the third duplicate are confirmed stale — no further action from me.
  • I’ve removed the premature “requestTls always constructed with timeout” learning as requested.

⛔ Learnings removed
Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-04T16:40:00.046Z
Learning: In src/utils/fetch.ts (structured-world/gitlab-mcp), requestTls is now always constructed with timeout: CONNECT_TIMEOUT_MS, even when no custom TLS options are provided (non-proxy path included). Do not suggest adding conditional timeouts for requestTls in future reviews; the unconditional timeout is already in place.

🧠 Learnings used
Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-04T16:24:22.255Z
Learning: In src/config.ts (structured-world/gitlab-mcp), all count-based environment variables (e.g., FAILURE_THRESHOLD, POOL_MAX_CONNECTIONS, API_RETRY_MAX_ATTEMPTS, RATE_LIMIT_IP_MAX_REQUESTS, RATE_LIMIT_SESSION_MAX_REQUESTS) use parseStrictInt(). Timer-based values continue to use parseTimerMs(). Do not suggest using parseTimerMs() for counts in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:86-95
Timestamp: 2026-03-31T07:15:54.557Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `calculateBackoffDelay` intentionally assumes `RECONNECT_BASE_DELAY_MS <= RECONNECT_MAX_DELAY_MS`. When the config is invalid (BASE > MAX), the function returns BASE as a constant delay. This is treated as a config validation concern, not a runtime bug; a doc note was added to the function rather than adding runtime normalization. Do not flag the missing bounds-swap as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T07:33:01.038Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `refreshTokenScopes()` (around lines 624-651) applies a post-await identity guard: after `await detectTokenScopes(url)` returns, it re-reads `this.instances.get(url)` and compares it to the `state` reference captured before the await. If a concurrent `reinitialize()` or `reset()` replaced the entry, it returns `false` instead of writing stale scope info into the new state. This mirrors the identity guard in `doInitialize`'s catch block. Do not flag the lack of this guard as a stale-write bug in future reviews — it is already in place.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T08:14:19.833Z
Learning: In `tests/unit/services/ConnectionManagerEnhanced.test.ts` (structured-world/gitlab-mcp), private `ConnectionManager` internals are accessed via typed `cmStatic()` and `cmInternal()` helper functions (replacing prior `as any` casts). These helpers expose `instances`, `currentInstanceUrl`, `doInitialize`, and related private fields/methods to set up intermediate states unreachable via the public API. This typed-helper pattern is the accepted approach for this test file. Do not suggest reverting to `as any` or flag the private-field access as unsafe in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T16:24:51.691Z
Learning: In tests/unit/handlers.test.ts (structured-world/gitlab-mcp), the deferred-init timedOut guard test around lines 1767–1800 now waits via a resolveExecuteReached barrier to ensure the executeTool dispatch is reached before asserting. This replaces the prior double process.nextTick and prevents scheduler-dependent flakiness. Do not suggest switching this test back to nextTick or fake timers in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:666-669
Timestamp: 2026-04-02T10:24:42.437Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `resolveUrl(instanceUrl?: string)` method and the state-mutation methods that call it (`reportSuccess`, `reportError`, `forceReconnect`) intentionally accept an optional `instanceUrl` with a fallback to `GITLAB_BASE_URL`. This zero-arg fallback is by design for test ergonomics and is documented by the test "should use default URL for reportSuccess/reportError" (around line 716–718 of `tests/unit/services/HealthMonitor.test.ts`). All production callers in `src/handlers.ts` already pass an explicit `instanceUrl`. Do not flag the optional parameter or the GITLAB_BASE_URL fallback as unsafe or requiring a required-parameter refactor in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: tests/unit/services/HealthMonitor.test.ts:752-757
Timestamp: 2026-03-30T04:44:46.218Z
Learning: In `tests/unit/services/HealthMonitor.test.ts` (structured-world/gitlab-mcp), the degraded-detection logic in `HealthMonitor` checks **both** `version` and `schemaInfo`. In the test "should handle getInstanceInfo throwing during health check" (around line 741-757), the default mock provides schema info, so even when `getInstanceInfo()` throws during the periodic health check the instance state can remain `'healthy'`. The loose assertion `expect(state === 'degraded' || state === 'healthy').toBe(true)` is intentionally correct for this mock setup — do not flag it as needing a stricter `'degraded'`-only assertion.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:724-732
Timestamp: 2026-04-02T10:46:41.853Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `isAnyInstanceHealthy()` intentionally includes the `'connecting'` state in its truthy set (alongside `'healthy'` and `'degraded'`). During startup all instances are in `'connecting'`, and excluding it would cause `tools/list` to return context-only tools for the entire initialization window (~5s), which is bad UX. The handler-level `isInstanceReachable()` check is the real runtime gate that prevents tool execution against unreachable instances. Do not flag the inclusion of `'connecting'` in `isAnyInstanceHealthy()` as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T23:36:18.216Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), all tool calls (including non-idempotent/write tools) race against `HANDLER_TIMEOUT_MS` via `Promise.race`. There is no non-idempotent bypass — bootstrap (init + introspect) is always bounded, and the `timedOut` flag prevents late `reportSuccess`/`reportError` side effects. Do not suggest re-adding a bypass for write tools in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/services/ConnectionManager.ts:65-67
Timestamp: 2026-04-03T14:50:27.051Z
Learning: In `src/utils/url.ts` (structured-world/gitlab-mcp), `normalizeInstanceUrl()` was updated (after PR `#380` review) to strip default ports (443 for https, 80 for http) using the URL constructor, so `https://host` and `https://host:443` map to the same entry in `ConnectionManager.instances` and inflight caches. When reviewing this function, verify: (1) `new URL()` call is wrapped in try/catch to handle malformed inputs gracefully, (2) API suffix stripping runs before or independently of port normalization to preserve subpaths for self-managed instances, (3) unit tests cover `https://host:443` → `https://host` and `http://host:80` → `http://host` cases.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/registry-manager.ts:293-307
Timestamp: 2026-03-31T16:09:46.248Z
Learning: In `src/registry-manager.ts` (structured-world/gitlab-mcp), `buildToolLookupCache(instanceUrl?)` intentionally bakes one URL's tier/version/scope view into the single shared `toolLookupCache`. This is an accepted trade-off: the MCP protocol exposes a single `tools/list` per server (not per-session), so per-URL keyed caches would require session-aware tool dispatch — a larger architectural change. In single-instance mode (99% of usage) the cache is always correct. In multi-instance OAuth the last `refreshCache(url)` wins; the handler-level health gate (handlers.ts) prevents cross-instance tool execution regardless of cache state. The limitation is documented in the code comment around lines 296-302. Do not flag this singleton cache as a cross-instance overwrite bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:329-366
Timestamp: 2026-03-29T21:05:15.421Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the early-return branch for `manage_context` when the instance is **unreachable** intentionally omits `healthMonitor.reportSuccess` / `reportError`, because in that path context tools (whoami, switch_instance) operate on local state only and never call the GitLab API. However, when the instance IS reachable/healthy, `manage_context` (e.g., `whoami`) does call the GitLab API and falls through the normal execution path including `reportSuccess`/`reportError` — this is correct and intentional. Do not flag this asymmetry as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T11:32:28.956Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the first `handleStateChange` callback that fires before `InstanceRegistry` is initialized results in a silently dropped `updateConnectionStatus` call. This is documented in Known Limitations: the registry catches up on the next real state transition, and fixing the miss would require a pending-status queue — which is out of scope. Do not flag this first-transition registry miss as an unhandled bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:100-108
Timestamp: 2026-03-29T18:10:49.416Z
Learning: In structured-world/gitlab-mcp, the `ConnectionManager.isConnected()` / `getInstanceInfo()` singleton path in `HealthMonitor.ts` (`performConnect`) is intentional for single-instance mode. Multi-instance OAuth contexts use `InstanceRegistry` for per-URL state. Do not flag this singleton usage as a bug in future reviews; the two modes have separate code paths by design.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:578-603
Timestamp: 2026-03-31T10:09:51.540Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `handleStateChange` intentionally early-returns before calling `registry.updateConnectionStatus` when the state hasn't changed (`previousState === newState`). This is by design: `updateConnectionStatus` is idempotent and calling it on every periodic health-check poll (e.g., `healthy.checking → healthy.idle`) would add unnecessary overhead with no functional benefit. Do not flag this early-return ordering as a bug in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:448-457
Timestamp: 2026-03-31T13:18:57.386Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the bootstrap `catch (initError)` block (inside the `try` that calls `connectionManager.initialize()` / `getClient()` / `ensureIntrospected()`) handles the case where `!connectionManager.isConnected(effectiveInstanceUrl)`. It calls `healthMonitor.reportError(effectiveInstanceUrl, initError)` (when `!timedOut`), logs the error, and then **returns** a `createConnectionFailedError(toolName, action, effectiveInstanceUrl, healthMonitor.getState(effectiveInstanceUrl))` structured `CONNECTION_FAILED` response (with `instance_url`, `reconnecting`, and `suggested_fix`) instead of throwing `"Bad Request: Server not initialized"`. Do not flag this bootstrap path as returning a generic error or as failing to report to HealthMonitor in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/handlers.ts:462-466
Timestamp: 2026-04-03T00:39:40.483Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the two-step `hasToolHandler()`/`executeTool()` pattern at the disconnected `manage_context` bypass (around line 462) and the main dispatch path (around lines 595-616) has an acknowledged TOCTOU gap: a concurrent `refreshCache()` swap between the two calls can make `executeTool()` miss a handler. This is intentionally benign because `executeTool` falls through to the bootstrap path when the handler is not found post-refresh. An atomic `getTool()` refactor to eliminate the gap is tracked separately. Do not re-flag this two-step pattern as a TOCTOU race in future reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T15:31:04.797Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), the stale-init overwrite race in `doInitialize()` (lines ~117-163) is mitigated by identity guards: the outer `let state` binding + `this.instances.get(baseUrl) === state` check in the catch block (lines ~294-298) prevents a stale init from deleting a fresh state entry. A deeper fix using per-attempt init tokens (to prevent any intermediate writes from stale paths) requires significant refactoring across `doInitialize`/`clearInflight`/`reset` and is tracked in issue `#379`. Do not re-flag the stale-init race as unguarded in future reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:529-564
Timestamp: 2026-04-02T09:32:46.067Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `HealthMonitor` creates one XState actor per monitored instance URL. Actors are only created for instances registered via `InstanceRegistry` config (1–3 in practice), so there is no unbounded actor accumulation. A follow-up issue has already been created to track per-URL eviction/inactivity TTL for future multi-instance/OAuth scenarios. Do not flag actor accumulation as an active memory leak in reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/handlers.ts:209-212
Timestamp: 2026-04-02T23:34:15.753Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), all public APIs that call `getActor(url)` — including `reportSuccess`, `reportError`, `forceReconnect`, and others — intentionally return early (no-op) when no actor exists for a URL. They do NOT lazily create actors. Untracked instance URLs are assumed reachable by `isInstanceReachable()` by design. Actors are only created explicitly via `HealthMonitor.initialize(url)`. Do not flag the missing-actor early-returns in any public API as a bug, and do not claim reportSuccess/reportError create actors for untracked URLs.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T23:36:18.216Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor (around lines 184-196) skips the post-init health probe when the remaining budget is < 500ms. When the remaining time is near-zero, the instance is assumed reachable (since init already succeeded) to prevent a false `disconnected` transition caused by an artificially short probe timeout. Do not flag skipping the health probe in this near-zero budget path as incorrect in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T16:55:06.625Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `HealthMonitor` does NOT call `InstanceRegistry.getIntrospection()` anywhere. Health and degraded-state detection is derived exclusively from `ConnectionManager.getInstanceInfo(url)` and `ConnectionManager.getSchemaInfo(url)`. Do not suggest testing `InstanceRegistry.getIntrospection()` branches in `HealthMonitor` tests in future reviews — it is not part of the health monitor's code path.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: tests/unit/handlers.test.ts:686-704
Timestamp: 2026-04-02T18:35:51.180Z
Learning: In `tests/unit/handlers.test.ts` (structured-world/gitlab-mcp), the double `process.nextTick` pattern (`await new Promise(resolve => process.nextTick(resolve))` twice) is the intentional and correct approach for flushing XState microtasks in fire-and-forget callback tests. `jest.useFakeTimers()` conflicts with XState's internal scheduling, making it unsuitable here. Do not suggest replacing this pattern with fake timers or `waitFor` in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 382
File: tests/unit/handlers.test.ts:154-156
Timestamp: 2026-04-03T21:14:08.041Z
Learning: In `tests/unit/handlers.test.ts` (structured-world/gitlab-mcp), the `tools/list_changed` notification path triggered by health state transitions is NOT asserted in unit tests because it requires `SessionManager` mock integration which is out of scope. The notification path is covered at the integration level via `broadcastToolsListChangedForStateChange` in the state-change callback tests. Do not suggest adding server notification method stubs or `tools/list_changed` assertions to this unit test file in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T15:17:48.027Z
Learning: In tests/unit/handlers.test.ts (structured-world/gitlab-mcp), when simulating a hanging operation, prefer a never-settling promise (`new Promise<never>(() => {})`) over timer-backed mocks to avoid delayed resolves leaking into subsequent tests. This pattern is now used around Lines 1255–1257.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/handlers.ts:132-135
Timestamp: 2026-03-31T10:37:22.878Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), `resetHandlersState()` is a test-only helper that clears `healthMonitorStartup` and `stateChangeRegistered`. It is always paired with `HealthMonitor.resetInstance()` in tests. `HealthMonitor.resetInstance()` calls `shutdown()` which destroys the singleton (and all registered `onStateChange` callbacks), so the fresh singleton returned by `getInstance()` starts with an empty callbacks array. In `tests/unit/handlers.test.ts`, `HealthMonitor` is fully mocked so no real listeners accumulate. The two-call pattern guarantees no duplicate listener leaks. Do not flag `resetHandlersState()` clearing `stateChangeRegistered` as unsafe in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/services/HealthMonitor.ts:112-206
Timestamp: 2026-04-03T00:40:13.151Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor function (around lines 112–206) has a cognitive complexity of ~17 (SonarCloud limit: 15) due to three interleaved phases: fast-path health check, full initialization with timeout, and degraded-path reachability verification. The maintainer (polaz) has explicitly declined extracting these into helpers because all three phases share a single `deadline` variable that defines the unified startup budget — splitting would obscure this invariant. A code comment was added above `performConnect` explaining the intentional complexity. Do not re-flag this complexity issue in future reviews of this function.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T12:46:57.150Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), the `performConnect` actor uses a dedicated `InitializationTimeoutError` class (around lines 152-166) to signal an init-timeout rejection, replacing prior fragile string-based error matching. `classifyError` in the catch block relies on this class identity to reliably distinguish init timeouts from other transient/auth/permanent failures. Do not suggest reverting to string-matching for timeout detection in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/ConnectionManager.ts:112-112
Timestamp: 2026-04-02T10:46:39.546Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `doInitialize()` (around line 112) has a cognitive complexity of ~34 (SonarCloud limit: 15) due to three interleaved init paths: OAuth best-effort unauthenticated version detection, REST-only token-scope path, and full GraphQL introspection. The maintainer (polaz) has explicitly decided NOT to extract helper methods in PR `#369` because the shared `state` mutations and identity guards would add indirection without reducing conceptual complexity. This is tracked for future refactoring. Do not re-flag this complexity issue in reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/handlers.ts:517-525
Timestamp: 2026-04-03T12:45:52.410Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), an `InitializationTimeoutError` class (added in PR `#380`) is used as the typed timeout sentinel injected by the handler-level timeout path. The `!bootstrapComplete` catch block checks `initError instanceof InitializationTimeoutError` to distinguish the injected sentinel from real bootstrap failures: real auth/permanent errors always call `healthMonitor.reportError()` (bypassing the `timedOut` guard), while the timeout sentinel itself is not re-reported since `reportError` was already called from the timeout branch. Do not suggest string-matching on error messages to detect this sentinel in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-04T07:25:22.055Z
Learning: In `src/handlers.ts` (structured-world/gitlab-mcp), the handler-level timeout (HANDLER_TIMEOUT_MS, default 120s) intentionally uses a plain `Error` with a message containing 'timed out' rather than `InitializationTimeoutError`. This is by design: `InitializationTimeoutError` is reserved for the startup init timeout (INIT_TIMEOUT_MS, default 5s) inside the `performConnect` XState actor. Using `InitializationTimeoutError` for the handler-level timeout would be misleading in logs and health snapshots. The 'timed out' phrase in the plain Error message ensures `classifyError()` classifies it as `transient` via its free-text fallback branch. This design decision is documented in a code comment around lines 711–714 of `src/handlers.ts`. Do not suggest replacing the plain Error with `InitializationTimeoutError` for the handler-level timeout in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-03-31T07:51:52.620Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `isConnected(instanceUrl?: string)` is a per-URL initialization flag (reads `state?.isInitialized` from `Map<string, InstanceState>`), explicitly documented as NOT a live connectivity check. Use `HealthMonitor.isInstanceReachable()` for runtime reachability. Do not flag `isConnected()` as an unsafe health signal in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-03-31T07:51:52.620Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `clearInflight(instanceUrl: string)` (around line 761) removes the inflight `initializePromises` entry for a URL. HealthMonitor calls this after an init timeout so reconnect attempts start a fresh `doInitialize()` instead of re-awaiting the hung promise. The `initialize()` method's `finally` block guards `initializePromises.delete(url)` with an identity check to prevent a retry's fresh promise from being wiped by a stale `clearInflight` call.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T15:17:48.027Z
Learning: For non-idempotent tool timeouts in handlers (structured-world/gitlab-mcp), tests should assert the structured timeout payload includes `retryable: false`, the `tool` name, and `timeout_ms` to verify the expected error contract. This is covered around Lines 1275–1293 in tests/unit/handlers.test.ts.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-04T16:40:00.046Z
Learning: In src/utils/fetch.ts (structured-world/gitlab-mcp), requestTls is now always constructed with timeout: CONNECT_TIMEOUT_MS, even when no custom TLS options are provided (non-proxy path included). Do not suggest adding conditional timeouts for requestTls in future reviews; the unconditional timeout is already in place.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T08:14:19.833Z
Learning: In `src/utils/fetch.ts` (structured-world/gitlab-mcp), `skipAuth: true` suppresses BOTH `getAuthHeaders()` AND `loadCookieHeader()` in the internal `doFetch()` call. This ensures unauthenticated probes (e.g., HEAD /api/v4/version health checks) carry no credentials or session cookies. Do not flag missing cookie suppression for `skipAuth` paths in future reviews — it is already handled.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:215-216
Timestamp: 2026-03-30T21:18:53.194Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `quickHealthCheck` is intentionally a reachability-only probe — `response.status < 500` is the correct condition because any HTTP response (including 400/404) proves the server is responding. It is NOT an API validation check. Actual API correctness (correct base URL, valid endpoints, auth) is verified at tool execution level. Do not flag this threshold as incorrect in future reviews.

Copy link
Copy Markdown

@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

♻️ Duplicate comments (1)
tests/unit/handlers.test.ts (1)

704-731: ⚠️ Potential issue | 🟡 Minor

Lock in that disconnected manage_context stays off the bootstrap path.

This still passes if the handler regresses to the normal bootstrap flow while returning the same local payload. Clear the bootstrap mocks before the call and assert getClient() / ensureIntrospected() remain untouched.

Suggested diff
       // manage_context should NOT be blocked by health gate
       mockRegistryManager.executeTool.mockResolvedValue({ context: 'info' });
       mockHealthMonitor.reportSuccess.mockClear();
       mockHealthMonitor.reportError.mockClear();
+      mockConnectionManager.getClient.mockClear();
+      mockConnectionManager.ensureIntrospected.mockClear();

       const result = await callToolHandler({
         params: {
           name: 'manage_context',
@@
       expect(parsed.context).toBe('info');
       // Fast-path does no GitLab I/O — must not mutate connection state
       expect(mockHealthMonitor.reportSuccess).not.toHaveBeenCalled();
       expect(mockHealthMonitor.reportError).not.toHaveBeenCalled();
+      expect(mockConnectionManager.getClient).not.toHaveBeenCalled();
+      expect(mockConnectionManager.ensureIntrospected).not.toHaveBeenCalled();

Based on learnings, the unreachable manage_context branch is intended to operate on local state only and never call the GitLab API.

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

In `@tests/unit/handlers.test.ts` around lines 704 - 731, Before invoking
callToolHandler for the disconnected manage_context case, clear the
bootstrap-related mocks (e.g., mockGitlabBootstrap.getClient.mockClear() and
mockGitlabBootstrap.ensureIntrospected.mockClear()) and after the call assert
those methods were not called
(expect(mockGitlabBootstrap.getClient).not.toHaveBeenCalled();
expect(mockGitlabBootstrap.ensureIntrospected).not.toHaveBeenCalled()). This
ensures the manage_context branch uses only local state and never touches the
bootstrap/GitLab API (referencing callToolHandler, getClient(), and
ensureIntrospected()).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unit/handlers.test.ts`:
- Around line 774-792: Tighten the test's assertion to ensure the state-change
callback only calls the URL-scoped refresh path: after invoking the captured
stateChangeCallback (from mockHealthMonitor.onStateChange) clear prior
invocations, invoke the callback, then assert mockRegistryManager.refreshCache
was called with the exact URL and that mockRegistryManager.refreshCache was not
called with zero arguments (i.e., ensure no unscoped refreshCache() call), so
the test verifies per-instance refresh behavior added by
setupHandlers/stateChangeCallback.
- Around line 1604-1621: The test currently asserts only the returned
CONNECTION_FAILED payload but must also verify that non-OAuth introspection
failures are reported to HealthMonitor; update the test that mocks
mockConnectionManager.ensureIntrospected to reject so that after calling
callToolHandler you also assert healthMonitor.reportError was invoked (e.g.,
toHaveBeenCalled or toHaveBeenCalledWith an error/metadata); reference the
existing mocks (mockConnectionManager.ensureIntrospected and callToolHandler)
and the test's healthMonitor mock (healthMonitor.reportError) and add a short
expectation that it was called once with an error-like argument.

---

Duplicate comments:
In `@tests/unit/handlers.test.ts`:
- Around line 704-731: Before invoking callToolHandler for the disconnected
manage_context case, clear the bootstrap-related mocks (e.g.,
mockGitlabBootstrap.getClient.mockClear() and
mockGitlabBootstrap.ensureIntrospected.mockClear()) and after the call assert
those methods were not called
(expect(mockGitlabBootstrap.getClient).not.toHaveBeenCalled();
expect(mockGitlabBootstrap.ensureIntrospected).not.toHaveBeenCalled()). This
ensures the manage_context branch uses only local state and never touches the
bootstrap/GitLab API (referencing callToolHandler, getClient(), and
ensureIntrospected()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3ed69cb9-b610-4301-baed-750659f4b31a

📥 Commits

Reviewing files that changed from the base of the PR and between 4a963f2 and caab7f4.

📒 Files selected for processing (2)
  • src/handlers.ts
  • tests/unit/handlers.test.ts

Comment on lines +774 to +792
it('should invoke state change callback for tool list updates', async () => {
// Capture the callback registered via onStateChange
const stateChangeCallback = mockHealthMonitor.onStateChange.mock.calls[0]?.[0];
expect(stateChangeCallback).toBeDefined();

// Clear refreshCache call count from setup
mockRegistryManager.refreshCache.mockClear();

// Simulate disconnected → healthy transition (should trigger broadcast)
if (stateChangeCallback) {
stateChangeCallback('https://gitlab.example.com', 'disconnected', 'healthy');
// Flush microtasks + dynamic import resolution for fire-and-forget callback
await new Promise((resolve) => process.nextTick(resolve));
await new Promise((resolve) => process.nextTick(resolve));
}

// refreshCache should have been called with the instance URL by the state change callback
expect(mockRegistryManager.refreshCache).toHaveBeenCalledWith('https://gitlab.example.com');
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make the state-change callback assertion strict about URL-scoped cache refresh.

toHaveBeenCalledWith(url) still passes if the callback also issues an unscoped refreshCache(). That would blur the new per-instance behavior this PR is adding.

Suggested diff
       // refreshCache should have been called with the instance URL by the state change callback
+      expect(mockRegistryManager.refreshCache).toHaveBeenCalledTimes(1);
       expect(mockRegistryManager.refreshCache).toHaveBeenCalledWith('https://gitlab.example.com');

Based on learnings, setupHandlers() uses zero-arg refreshCache() only during initial startup, while the state-change callback is the URL-scoped refresh path.

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

In `@tests/unit/handlers.test.ts` around lines 774 - 792, Tighten the test's
assertion to ensure the state-change callback only calls the URL-scoped refresh
path: after invoking the captured stateChangeCallback (from
mockHealthMonitor.onStateChange) clear prior invocations, invoke the callback,
then assert mockRegistryManager.refreshCache was called with the exact URL and
that mockRegistryManager.refreshCache was not called with zero arguments (i.e.,
ensure no unscoped refreshCache() call), so the test verifies per-instance
refresh behavior added by setupHandlers/stateChangeCallback.

Comment on lines +1604 to +1621
it('should return CONNECTION_FAILED when introspection fails before bootstrap completes', async () => {
// isConnected returns true (already connected), but ensureIntrospected throws
// Since bootstrapComplete is still false, this goes to CONNECTION_FAILED path
mockConnectionManager.ensureIntrospected.mockRejectedValueOnce(
new Error('Introspection failed'),
);

const result = await callToolHandler({
params: {
name: 'test_tool',
arguments: {},
},
});

expect(result.isError).toBe(true);
const parsed = JSON.parse(result.content![0].text);
expect(parsed.error_code).toBe('CONNECTION_FAILED');
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Also assert default-URL introspection failures are reported to HealthMonitor.

This only checks the returned CONNECTION_FAILED payload. It still passes if the non-OAuth ensureIntrospected() failure stops updating health state.

Suggested diff
       expect(result.isError).toBe(true);
       const parsed = JSON.parse(result.content![0].text);
       expect(parsed.error_code).toBe('CONNECTION_FAILED');
+      expect(mockHealthMonitor.reportError).toHaveBeenCalledWith(
+        'https://gitlab.example.com',
+        expect.any(Error),
+      );

Based on learnings, the bootstrap catch path is expected to both return CONNECTION_FAILED and call healthMonitor.reportError(...) for real bootstrap failures before timeout.

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

In `@tests/unit/handlers.test.ts` around lines 1604 - 1621, The test currently
asserts only the returned CONNECTION_FAILED payload but must also verify that
non-OAuth introspection failures are reported to HealthMonitor; update the test
that mocks mockConnectionManager.ensureIntrospected to reject so that after
calling callToolHandler you also assert healthMonitor.reportError was invoked
(e.g., toHaveBeenCalled or toHaveBeenCalledWith an error/metadata); reference
the existing mocks (mockConnectionManager.ensureIntrospected and
callToolHandler) and the test's healthMonitor mock (healthMonitor.reportError)
and add a short expectation that it was called once with an error-like argument.

Copy link
Copy Markdown

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 27 out of 29 changed files in this pull request and generated 1 comment.

const parsedBaseDelay = parseInt(process.env.GITLAB_API_RETRY_BASE_DELAY_MS ?? '1000', 10);
export const API_RETRY_BASE_DELAY_MS =
Number.isFinite(parsedBaseDelay) && parsedBaseDelay > 0 ? parsedBaseDelay : 1000;
export const API_RETRY_MAX_ATTEMPTS = parseStrictInt(process.env.GITLAB_API_RETRY_MAX_ATTEMPTS, 3);
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

API_RETRY_MAX_ATTEMPTS is now parsed via parseStrictInt(), which rejects 0 because it enforces > 0. enhancedFetch() treats maxRetries=0 as a valid “no retries” configuration (single attempt), and the previous parsing logic allowed 0 (>= 0). Consider allowing 0 here (e.g., a dedicated non-negative int parser or a min parameter) so users can disable retries without toggling GITLAB_API_RETRY_ENABLED.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: MCP Server startup blocked when GitLab is unreachable

2 participants