Skip to content

Remote Muster MCP endpoint for Hermes - #82

Merged
jusso-dev merged 7 commits into
mainfrom
claude/issue-72-hermes-mcp-20260728-130015
Jul 28, 2026
Merged

Remote Muster MCP endpoint for Hermes#82
jusso-dev merged 7 commits into
mainfrom
claude/issue-72-hermes-mcp-20260728-130015

Conversation

@jusso-dev

@jusso-dev jusso-dev commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Closes #72

Summary

Ships the first Hermes-native Muster MCP vertical slice per the product pivot in #78: Slack is the human interface, Hermes is the harness, Muster is the governed MCP/control-plane backend, Kelpie stays authoritative for cases.

  • apps/mcp-server — a minimal node:http app (same shape as apps/agent-gateway/apps/worker, not folded into either) speaking MCP Streamable HTTP. GET /health is unauthenticated/tenant-free and dependency-aware (a real select 1, not a stub); POST/GET /mcp requires a bearer installation token. Shutdown drains in-flight requests before closing the DB pool; request/socket timeouts are explicit.
  • packages/mcp — pure domain logic, no HTTP framework dependency:
    • mcp_installations: a random bearer token, SHA-256 hashed at rest, bound to exactly one organisation and one "policy subject" actor. Installation lifecycle mutations (createInstallation/revokeInstallation) re-derive the acting actor's organisation membership and administration.manage capability from the database inside the mutation's own transaction — never trusted from the caller — and a composite (actor_id, organisation_id) -> actors(id, organisation_id) foreign key enforces the same boundary at the schema level as defence in depth. Revocation is immediate and fails closed. Every credential failure path (unknown/malformed/revoked/cross-org token, deactivated actor) returns the identical generic denial.
    • Four schema-validated, read-only tools: muster_get_status, muster_list_capabilities, muster_search_kelpie_cases, muster_get_kelpie_case. No tool schema accepts an organisation id, actor id, or capability.
    • Kelpie access reuses the existing governed connector path exactly (integration_query_runs → outbox → the unmodified apps/worker processor → executeGovernedQuery's DNS pinning/SSRF denial/schema validation/redaction). The MCP tool call polls the authoritative row for a bounded window (8s) instead of blocking, now with a deterministic, time-windowed idempotency key (a retry within 5s dedupes to the same queued run; a later identical query still gets fresh data) and an integration-scoped advisory lock around the rate-limit check (closing the read-then-insert race).
    • Kelpie results are wrapped classification: "untrusted_evidence", bounded to 25 records with depth/breadth-bounded nested structures, oversized strings truncated, secrets redacted a second time at the tool boundary.
    • One mcp.tool.invoked audit event per call via the existing hash-chained audit_events table; a failure to write it is now logged, not silently swallowed.
    • create-installation/revoke-installation CLI scripts provision credentials (no chat/UI-driven registration flow), now reporting authorization failures with a clean exit rather than an unhandled rejection.
  • skills/muster-soc-operations/SKILL.md, docs/integrations/hermes-mcp.md, ADR 0005 — skill contract, operator configuration (placeholders only), and the architectural decision record, including a new "Deferred" section documenting two intentionally-out-of-scope design tensions (see Residual risks).

Non-goals preserved

No LangGraph/model runtime, no Muster chat UI/rooms/Inbox/composer/threads, no Slack gateway, no arbitrary MCP registration, no write/destructive/external-communication tools, no admin UI. apps/agent-gateway is untouched.

Corrective update (this PR was already reviewed once)

CodeRabbit raised 9 actionable findings and 6 nitpicks against the first commit. All 9 actionable findings are fixed and their review threads replied-to (with the exact fix) and resolved:

  1. Graceful shutdownserver.close() is now awaited via its completion callback before closeDatabase() runs (new apps/mcp-server/src/shutdown.ts, unit-tested, live-verified with SIGTERM).
  2. Actor-to-organisation integrity in the schema — composite FK (actor_id, organisation_id) -> actors(id, organisation_id) on all three mcp_installations actor columns, backed by a new actors_id_organisation_unique index; Postgres itself now rejects a cross-org actor binding.
    3 & 4. Authoritative server-side capability/approval enforcement for installation lifecyclecreateInstallation/revokeInstallation re-check administration.manage and organisation membership from the database inside the mutation's transaction. A full multi-party approval record workflow (as opposed to capability enforcement) is a documented, justified deferral — see ADR 0005 — since it's materially larger than this read-only-tools vertical slice and belongs with tracker item 2's governed write/approval operations.
  3. SSRF test didn't exercise the real path — rewritten to drive muster_search_kelpie_cases through a real installation/MCP client against the SSRF-shaped integration, asserting the tool call, the run's errorCode, and the audit outcome all reflect the denial.
  4. Audit-write failures on the error path were swallowed — now logged with tool/installation/trace context.
  5. newId() idempotency key never deduped — replaced with a deterministic, 5-second-windowed key; verified with a test asserting exactly one connector run across two immediately-repeated identical tool calls.
  6. Client-side query filter used full-record JSON.stringify — now matches specific fields only, with a doc comment stating plainly this is a best-effort filter over the already-fetched page (Kelpie's cases.list has no query parameter to push down to).
  7. malformed_response was reported as not_found — now stays a distinct upstream_error; only a genuinely absent/unreachable case maps to not_found.

Of the 6 nitpicks: the depth/breadth evidence-bounding, dependency-aware /health, and typed ScopeError (replacing message-string matching for audit "denied" classification) are fixed. The rate-limit race is fixed via an advisory lock (matching appendAuditEvent's existing pattern) rather than left as noted. The 8-second bounded-poll-inside-the-request-handler design tension is not restructured — see ADR 0005's "Deferred" section for why (it's a genuine MCP protocol-shape decision, not a bug) — but request/socket timeouts were added as the concrete, in-scope mitigation the reviewer asked to confirm.

A defect not flagged by review, found while fixing the health check: @muster/database's shared pg.Pool had no error listener. An idle-client error (e.g. Postgres restarting) is an unhandled Node 'error' event, which crashes the entire process — used by every app in this repo (web, worker, agent-gateway, and now mcp-server), not just this PR's code. This is exactly what a real dependency-aware /health check provokes during any outage. Reproduced live (stopped Postgres under a running mcp-server, watched it crash) and fixed with a pool-level error listener; re-verified the same scenario now returns 503 and the process survives, then recovers to 200 when Postgres comes back.

Validation evidence (distinguished by tier)

Local, real Postgres (docker --context m3-max, started and torn down for this task):

  • Squashed the two-step migration into one clean 0021 (it was only ever unreleased in this PR); fixed a statement-ordering bug (the new actors unique index must be created before the composite FKs that reference it).
  • pnpm db:migrate && pnpm db:bootstrap && pnpm db:verify-clean on a pristine database — clean, 42 operational tables empty, no schema drift (pnpm db:generate reports "No schema changes").
  • packages/mcp full suite: 18/18 passing (10 pure unit + 8 integration, up from 6 — added: capability/cross-org-actor authorization test, cross-org DB-level FK-rejection test, idempotency-dedup test; rewrote the SSRF test to go through the real tool path).
  • apps/mcp-server unit suite: 4/4 passing (new health.test.ts, shutdown.test.ts).
  • Live-verified: built apps/mcp-server, ran it against real Postgres — /health reports 200 ready; stopped Postgres — /health reports 503 not_ready and the process stays alive (previously crashed, see above); restarted Postgres — recovers to 200; sent SIGTERM — exits cleanly. All via the actual built binary, not a mock.

Repo-wide:

  • pnpm lint / pnpm typecheck — 25/25 packages pass.
  • pnpm test:unit — 186 passed, 84 skipped (0 regressions; skips are the repo's existing integration tests, gated the same way as CI).
  • pnpm test (turbo, per-package) — 39/39 tasks pass.
  • pnpm build — 25/25 packages, including apps/web.

GitHub Actions run 30326839826 (this PR's first commit) — two failed checks, both confirmed pre-existing on main, not introduced by this PR:

  • quality (Playwright e2e, tests/muster.spec.ts/tests/jessie-hunt.spec.ts): fails with the identical error signature (Second seeded room required, element(s) not found, etc.) on base commit 496cc09d09f2d81090be96b7e42ec375a9891db0's own CI run (30247756940, job quality, conclusion failure). This PR touches zero UI/room code; not weakened or worked around.
  • release-security / image (Trivy image scan, exit-code: 1 on HIGH/CRITICAL): also fails identically on the same base-SHA run (job 89918532363). This PR doesn't touch Dockerfile or pin any base image. I rebuilt the exact Dockerfile fresh (local docker --context m3-max build) and ran Trivy (both 0.67.2 and 0.70.0, matching the CI action's bundled version) against it just now: zero HIGH/CRITICAL fixable vulnerabilities. node:24-bookworm-slim and gcr.io/distroless/nodejs24-debian13:nonroot are floating tags — the specific CVE that failed in the original CI run has very likely already rolled off with a newer base-layer pull since then; the original run's trivy.sarif was never uploaded (the job died before that step), so the exact CVE ID from that specific run can't be retrieved retroactively. No vulnerability was suppressed, ignored, or worked around — I could not reproduce a finding to act on, and the mechanism (a pre-existing, floating-tag, time-dependent base image state) is confirmed identical to base main.
  • Both are reported here for transparency; neither was touched, weakened, or suppressed to force green, per instruction.

Migration / rollback

packages/database/migrations/0021_dazzling_living_mummy.sql (squashed, additive-only): new table mcp_installations; new unique index actors_id_organisation_unique on the existing actors table; three composite FKs from mcp_installations to actors(id, organisation_id). No column drops, type changes, or backfills on any existing table besides the new index. Rollback: DROP TABLE mcp_installations; DROP INDEX actors_id_organisation_unique;.

Security review notes

  • Tenant isolation: enforced at the application layer (fresh capability/org lookups on every request and every lifecycle mutation) and the schema layer (composite FK) — two independent, redundant checks.
  • Bearer-token handling: unchanged from the original review — SHA-256 hashed at rest, only a non-secret prefix stored in cleartext, never logged or returned after creation.
  • SSRF/redirect safety: no new HTTP-egress code in packages/mcp; delegated entirely to the existing, already-tested executeGovernedQuery. Now proven through the real tool/gateway path, not just a direct unit-level call.
  • Redaction: unchanged inherited limitation noted previously (secrets embedded in prose without a key=/suspicious field name can evade content-based redaction) — out of scope for this PR, shared with every other connector. Nested evidence is now depth/breadth-bounded in addition to size-bounded.
  • Idempotency: now genuinely deduping within a 5s window rather than structurally unreachable.
  • Audit integrity: a failed audit write is now observable (logged), not a silent gap. Denied-outcome classification uses a typed error, not string matching.
  • Process resilience: the pool-crash defect above means a Postgres blip previously took down the whole process for every app using @muster/database — now contained to a logged error and a failed health check, as intended.
  • No secrets found in the diff (scanned for private-key/API-key/token patterns); no synthetic canary values appear in this PR description or its comments.

Residual risks / follow-ups

  • No real Hermes discovery test — still only local, SDK-client-verified (the actual @modelcontextprotocol/sdk client library over real HTTP, but not the Hermes binary itself, which isn't available in this sandboxed environment).
  • apps/mcp-server isn't wired into Dockerfile/docker-compose.yml — unchanged from the original PR; still out of the issue's required-implementation list.
  • Two documented, justified deferrals (ADR 0005 "Deferred" section): the 8-second bounded poll living inside the HTTP request handler (a genuine MCP synchronous-tool-call protocol constraint, not a bug — mitigated with explicit request/socket timeouts), and the rate-limit check being a soft per-integration limit rather than a hard token-bucket limiter (the read-then-insert race is now closed via an advisory lock, but a determined caller could still contend on that lock across many concurrent connections).
  • Full multi-party approval records for installation lifecycle mutations (as opposed to capability enforcement, which is now authoritative and server-side) remain deferred to tracker item 2, per [TRACKER] Hermes + Slack + Muster MCP product pivot #78's ordering.
  • Tracker item 2 (write/proposal tools) and real (non-mock) Kelpie certification remain explicitly out of scope here.

Not merged or deployed.

Summary by CodeRabbit

  • New Features
    • Launched a remote, authenticated MCP HTTP service with GET /health plus four read-only tools (status, capabilities, bounded Kelpie search, and single case details).
    • Added installation-scoped bearer tokens with revocation, tool scope enforcement, per-invocation auditing, bounded evidence redaction/truncation, and Kelpie-query queuing with idempotency and polling.
    • Added operator CLIs to create and revoke MCP installations.
  • Documentation
    • Added/updated Hermes connector docs, the remote MCP architecture write-up, and the “Muster SOC operations” skill guide.
  • Reliability
    • Improved database readiness checking and shutdown sequencing, with expanded test coverage.
  • Chores
    • Updated database schema/migration to support MCP installations.

Ship the first Hermes-native Muster MCP vertical slice: a Streamable HTTP
MCP server (apps/mcp-server) backed by revocable, organisation- and
actor-scoped installation credentials (packages/mcp), exposing four
read-only tools (muster_get_status, muster_list_capabilities,
muster_search_kelpie_cases, muster_get_kelpie_case) that route Kelpie
access through the existing governed connector path (integration_query_runs
-> outbox -> the unmodified worker's processConnectorQuery), never letting
connector credentials or model-supplied tenant/capability fields reach
Hermes. Every call is bounded, redacted, classified as untrusted evidence,
and appended to the existing hash-chained audit log with tool/version,
installation/actor, outcome, result hash, and evidence references.

Adds the mcp_installations migration, a starter Hermes skill
(skills/muster-soc-operations/SKILL.md), operator docs for connecting
Hermes to the endpoint, and ADR 0005. CLI scripts
(create-installation/revoke-installation) provision credentials without a
chat- or UI-driven registration flow, consistent with the "no arbitrary MCP
registration" boundary for this slice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces a remote, installation-authenticated MCP server with four read-only Kelpie tools, governed query execution, evidence redaction, audit logging, database support, health checks, graceful shutdown, integration tests, and Hermes integration documentation.

Changes

Remote Muster MCP server

Layer / File(s) Summary
Installation schema and migration foundation
packages/database/src/schema.ts, packages/database/migrations/*, packages/database/src/verify-clean-install.ts
Adds organisation-scoped MCP installations, composite actor constraints, indexes, migration metadata, and clean-install verification.
Installation lifecycle and audit contracts
packages/mcp/src/installation.ts, packages/mcp/src/audit.ts, packages/mcp/src/cli-*.ts, packages/mcp/src/constants.ts, packages/mcp/src/errors.ts
Adds hashed bearer-token creation, revocation, resolution, scope enforcement, invocation auditing, public exports, operator CLIs, and focused tests.
Governed Kelpie tools and evidence handling
packages/mcp/src/kelpie-gateway.ts, packages/mcp/src/tools.ts, packages/mcp/src/redact.ts, packages/mcp/src/*test.ts
Adds capability-checked, idempotent Kelpie queries with polling, bounded and redacted untrusted evidence, standardized errors, and four read-only tools.
MCP server and HTTP transport
packages/mcp/src/server.ts, apps/mcp-server/src/*, packages/database/src/index.ts
Registers and audits MCP tools, resolves bearer installations, forwards Streamable HTTP requests, exposes database readiness, configures timeouts, handles pool errors, and drains requests during shutdown.
Vertical validation and integration documentation
packages/mcp/src/mcp.integration.test.ts, docs/architecture/*, docs/integrations/*, skills/muster-soc-operations/*, tests/muster.spec.ts
Validates isolation, governance, redaction, idempotency, audit behavior, and SSRF rejection while documenting the Hermes connector and skill contract and updating seeded test identifiers.
Workspace and demo support updates
apps/mcp-server/package.json, packages/mcp/package.json, packages/agent-harness/package.json, pnpm-workspace.yaml, .github/workflows/security.yml, packages/database/src/seed.ts, apps/web/components/room-view.tsx
Adds package manifests and dependency wiring, adjusts security artifact handling, and updates demo seed and fixture identifiers.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

  • Issue 72 — Directly covers the Hermes-native Muster MCP vertical slice implemented by this change.

Suggested labels: dependencies, javascript

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a remote Muster MCP endpoint for Hermes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/issue-72-hermes-mcp-20260728-130015

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (6)
packages/mcp/src/kelpie-gateway.ts (2)

79-96: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Rate-limit check is racy (read outside the insert transaction).

The count is read before db.transaction, so concurrent MCP calls can each observe a sub-limit count and all insert, overshooting limits.requestsPerMinute. Acceptable as a soft limit, but if the cap is meant to protect the Kelpie upstream, move the count inside the transaction (with an appropriate isolation level) or use a dedicated counter/limiter keyed by organisationId:integrationId.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp/src/kelpie-gateway.ts` around lines 79 - 96, The rate-limit
check around the recent query count is racy because it runs outside the insert
transaction. Move the count and limit decision into the transaction that records
the integration query run, using an appropriate isolation level or an atomic
counter/limiter keyed by organisationId and integrationId so concurrent calls
cannot exceed limits.requestsPerMinute.

198-240: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Bounded DB polling holds the HTTP request for up to timeoutMs.

Each iteration issues a fresh query and sleeps, so a slow worker keeps an MCP request occupied for the full window (8s per KELPIE_POLL_OPTIONS in tools.ts) and consumes a pool connection per concurrent call. Acceptable for the first slice, but consider a listen/notify or a "run id + fetch later" contract so the tool can return immediately, and cap concurrent pollers.

As per coding guidelines: "Keep long-running integration and agent work outside HTTP request handlers."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp/src/kelpie-gateway.ts` around lines 198 - 240, The
pollKelpieQuery implementation performs bounded polling inside the HTTP request
handler, holding requests and database capacity for up to timeoutMs. Replace
this synchronous polling contract with an immediate run-id response and a
separate fetch-later path, or use a database listen/notify mechanism so
completion does not require repeated queries and sleeps within the handler;
preserve the existing organisation and run-id authorization checks and terminal
result mapping.

Source: Coding guidelines

packages/mcp/src/redact.ts (1)

6-45: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Bounding is per-string only; nested breadth/depth is unbounded.

truncateStrings (and redactUntrusted before it) recurse over arbitrary external evidence: a deeply nested or very wide Kelpie response still passes through in full, so a single record with thousands of keys/elements can blow the payload budget or overflow the stack. Since the stated goal is that oversized content never survives this step, consider a depth cap and per-record array/key cap alongside MAX_STRING_LENGTH.

♻️ Sketch: depth and breadth caps
 const MAX_STRING_LENGTH = 4_000;
+const MAX_DEPTH = 8;
+const MAX_CHILDREN = 200;
 
-function truncateStrings(value: unknown): unknown {
+function truncateStrings(value: unknown, depth = 0): unknown {
   if (typeof value === "string")
     return value.length > MAX_STRING_LENGTH
       ? `${value.slice(0, MAX_STRING_LENGTH)}…[truncated]`
       : value;
-  if (Array.isArray(value)) return value.map(truncateStrings);
+  if (depth >= MAX_DEPTH) return "[truncated]";
+  if (Array.isArray(value))
+    return value.slice(0, MAX_CHILDREN).map((item) => truncateStrings(item, depth + 1));
   if (value && typeof value === "object")
     return Object.fromEntries(
-      Object.entries(value as Record<string, unknown>).map(([key, entry]) => [
-        key,
-        truncateStrings(entry),
-      ]),
+      Object.entries(value as Record<string, unknown>)
+        .slice(0, MAX_CHILDREN)
+        .map(([key, entry]) => [key, truncateStrings(entry, depth + 1)]),
     );
   return value;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp/src/redact.ts` around lines 6 - 45, Update truncateStrings and
the redactUntrusted traversal used by classifyKelpieRecords to enforce
per-record depth and breadth limits in addition to MAX_STRING_LENGTH. Cap nested
object keys and array elements, stop or safely truncate traversal once the depth
limit is reached, and preserve the existing ClassifiedRecords shape while
ensuring oversized nested structures cannot pass through in full or cause stack
exhaustion.
docs/architecture/0005-remote-mcp-server.md (1)

43-55: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

8-second bounded poll runs inside the HTTP request handler.

Because muster_search_kelpie_cases/muster_get_kelpie_case poll the integration_query_runs row for up to 8s as part of the tool call, and the tool call executes inside transport.handleRequest within the raw node:http handler in apps/mcp-server/src/index.ts, each Kelpie tool invocation ties up its HTTP request for up to 8s. This is in tension with the guideline to keep long-running integration work out of request handlers, though it may be an unavoidable consequence of MCP's synchronous tool-call/response model. Worth confirming the node:http server has appropriate request/socket timeouts configured so a burst of concurrent 8s-bound calls can't exhaust connection resources.

As per coding guidelines, "Keep long-running integration and agent work outside HTTP request handlers."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/architecture/0005-remote-mcp-server.md` around lines 43 - 55, The Kelpie
tool polling in muster_search_kelpie_cases and muster_get_kelpie_case can occupy
the raw node:http request handler for up to 8 seconds. Review
apps/mcp-server/src/index.ts around transport.handleRequest and configure
appropriate request and socket timeouts for the node:http server, ensuring
concurrent bounded polls cannot leave connections indefinitely open while
preserving the existing synchronous MCP response behavior.

Source: Coding guidelines

apps/mcp-server/src/index.ts (1)

35-38: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

/health doesn't actually verify readiness, despite the code and docs implying it does. The handler returns a static {status: "ready", authority: "postgresql"} regardless of whether db is reachable, while the docs describe it as reporting readiness.

  • apps/mcp-server/src/index.ts#L35-L38: perform a lightweight liveness check (e.g., a trivial SELECT 1 via db) before returning status: "ready", so orchestrators don't get false-positive readiness during a Postgres outage.
  • docs/integrations/hermes-mcp.md#L74-L75: once the check reflects real state, keep this description; otherwise adjust the wording to clarify it's a process-liveness check rather than a dependency-readiness check.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mcp-server/src/index.ts` around lines 35 - 38, Update the GET /health
handler in apps/mcp-server/src/index.ts (lines 35-38) to perform a lightweight
db liveness query, such as SELECT 1, before returning status "ready"; report an
appropriate non-ready response when the check fails. Keep
docs/integrations/hermes-mcp.md (lines 74-75) unchanged if it already describes
dependency readiness, or revise it to describe process liveness if the endpoint
does not verify db availability.
packages/mcp/src/server.ts (1)

34-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Denied/error classification relies on brittle message-string matching, duplicated in two places.

Both toolFailure (Lines 40-42) and isDenied (Lines 53-54) independently parse error.message.startsWith("Installation is not scoped"). requireScope in packages/mcp/src/installation.ts throws a plain Error, so any future wording change to that message silently breaks the "denied" audit classification (it would fall through to "error"), which matters for governance monitoring given this repo's requirement to preserve accurate audit/evidence metadata.

As per coding guidelines, "Preserve append-only messages, timelines, evidence metadata, and audit events." — the outcome field depends on this fragile check.

♻️ Proposed fix: introduce a typed scope error
// packages/mcp/src/installation.ts
export class ScopeError extends Error {
  constructor(tool: McpToolName) {
    super(`Installation is not scoped for ${tool}`);
    this.name = "ScopeError";
  }
}

export function requireScope(context: InstallationContext, tool: McpToolName): void {
  if (!context.scopes.includes(tool)) throw new ScopeError(tool);
}
-import { requireScope } from "./installation.ts";
+import { requireScope, ScopeError } from "./installation.ts";
...
-        : error instanceof Error &&
-            error.message.startsWith("Installation is not scoped")
-          ? error.message
+        : error instanceof ScopeError
+          ? error.message
...
-    error instanceof ForbiddenError ||
-    (error instanceof Error &&
-      error.message.startsWith("Installation is not scoped"))
+    error instanceof ForbiddenError || error instanceof ScopeError
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp/src/server.ts` around lines 34 - 56, Introduce and export a
typed ScopeError in requireScope, preserving the existing message format, and
throw it when the requested tool lacks scope. Update toolFailure and isDenied to
classify ScopeError with instanceof checks instead of matching error.message,
reusing the shared type so denied audit outcomes remain stable if wording
changes.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/mcp-server/src/index.ts`:
- Around line 83-89: Update shutdown so closeDatabase() runs only after
server.close() has completed draining existing connections and emitted its
completion callback. Preserve the SIGINT and SIGTERM handlers’ use of shutdown,
and ensure database cleanup still occurs after the server-close operation
finishes.

In `@packages/database/migrations/0021_colorful_saracen.sql`:
- Around line 19-22: Update the mcp_installations actor relationships
represented by mcp_installations_bound_actor_id_actors_id_fk,
mcp_installations_installed_by_actor_id_actors_id_fk, and
mcp_installations_revoked_by_actor_id_actors_id_fk to enforce matching
organisation_id values, preferably by adding the required unique actors key and
composite foreign keys on (actor_id, organisation_id); otherwise validate all
three actor IDs against the installation organisation within the same
create/update transaction.

In `@packages/mcp/src/cli-revoke-installation.ts`:
- Around line 12-30: Before calling revokeInstallation in main, validate
server-side that revokedByActorId has the required capability for the specified
organisation and installationId has an approved revocation record. Perform both
checks through the existing authorization/approval mechanisms, reject the
request when either is missing, and only then invoke revokeInstallation; do not
treat traceId or audit metadata as authorization.

In `@packages/mcp/src/installation.ts`:
- Around line 39-115: Update createInstallation and revokeInstallation to
validate every referenced actor belongs to input.organisationId, enforce the
server-side capability authorization for the installing or revoking actor, and
require the corresponding approval record before changing mcpInstallations.
Perform these checks within the same transaction before the mutation, and only
append the existing audit event after authorization and approval succeed.

In `@packages/mcp/src/mcp.integration.test.ts`:
- Around line 509-587: Update the SSRF test to exercise the MCP gateway path
through the relevant tool, such as muster_search_kelpie_cases or
muster_get_kelpie_case, using an installation scoped to ssrfIntegrationId and
asserting the tool or audit result reflects denial; retain the seeded
integration records and remove the stale gateway-selection comment. If this test
is intentionally limited to executeGovernedQuery, remove the unused database
setup and comment instead.

In `@packages/mcp/src/server.ts`:
- Around line 64-88: Update the error-path audit handling in invoke so failures
from recordInvocation are still caught but logged with sufficient context for
operators to detect the audit gap, rather than silently ignored. Preserve the
existing toolFailure(error) response and the denied/error outcome
classification, and use the server’s existing logging mechanism.

In `@packages/mcp/src/tools.ts`:
- Around line 133-141: Update the settled-error handling around the
failed-result branch to stop mapping malformed_response to not_found. Preserve
source_unavailable and malformed_response as distinct upstream failures, map
upstream_error consistently for both, and reserve not_found for successful
results that are empty or null.
- Line 84: Replace the per-call newId() component in the idempotency keys used
by the list and getKelpieCase request paths with a deterministic key derived
from the MCP request identity or stable request inputs, including installationId
and the operation/template arguments as needed. Update both the visible list key
and the corresponding key near getKelpieCase so retries and transport replays
produce the same key and allow queueKelpieQuery deduplication.
- Around line 99-106: Update the tool flow around the Kelpie cases-list
invocation to pass args.query through the template input when supported, rather
than always using an empty input and filtering JSON locally. Remove the
client-side JSON.stringify matching once the query is delegated, or explicitly
document it as a best-effort filter over the returned page if the template
cannot accept query parameters.

---

Nitpick comments:
In `@apps/mcp-server/src/index.ts`:
- Around line 35-38: Update the GET /health handler in
apps/mcp-server/src/index.ts (lines 35-38) to perform a lightweight db liveness
query, such as SELECT 1, before returning status "ready"; report an appropriate
non-ready response when the check fails. Keep docs/integrations/hermes-mcp.md
(lines 74-75) unchanged if it already describes dependency readiness, or revise
it to describe process liveness if the endpoint does not verify db availability.

In `@docs/architecture/0005-remote-mcp-server.md`:
- Around line 43-55: The Kelpie tool polling in muster_search_kelpie_cases and
muster_get_kelpie_case can occupy the raw node:http request handler for up to 8
seconds. Review apps/mcp-server/src/index.ts around transport.handleRequest and
configure appropriate request and socket timeouts for the node:http server,
ensuring concurrent bounded polls cannot leave connections indefinitely open
while preserving the existing synchronous MCP response behavior.

In `@packages/mcp/src/kelpie-gateway.ts`:
- Around line 79-96: The rate-limit check around the recent query count is racy
because it runs outside the insert transaction. Move the count and limit
decision into the transaction that records the integration query run, using an
appropriate isolation level or an atomic counter/limiter keyed by organisationId
and integrationId so concurrent calls cannot exceed limits.requestsPerMinute.
- Around line 198-240: The pollKelpieQuery implementation performs bounded
polling inside the HTTP request handler, holding requests and database capacity
for up to timeoutMs. Replace this synchronous polling contract with an immediate
run-id response and a separate fetch-later path, or use a database listen/notify
mechanism so completion does not require repeated queries and sleeps within the
handler; preserve the existing organisation and run-id authorization checks and
terminal result mapping.

In `@packages/mcp/src/redact.ts`:
- Around line 6-45: Update truncateStrings and the redactUntrusted traversal
used by classifyKelpieRecords to enforce per-record depth and breadth limits in
addition to MAX_STRING_LENGTH. Cap nested object keys and array elements, stop
or safely truncate traversal once the depth limit is reached, and preserve the
existing ClassifiedRecords shape while ensuring oversized nested structures
cannot pass through in full or cause stack exhaustion.

In `@packages/mcp/src/server.ts`:
- Around line 34-56: Introduce and export a typed ScopeError in requireScope,
preserving the existing message format, and throw it when the requested tool
lacks scope. Update toolFailure and isDenied to classify ScopeError with
instanceof checks instead of matching error.message, reusing the shared type so
denied audit outcomes remain stable if wording changes.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ce6ba0f5-ca91-4323-b769-927a04ded504

📥 Commits

Reviewing files that changed from the base of the PR and between 496cc09 and 3ac2f2c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (28)
  • apps/mcp-server/package.json
  • apps/mcp-server/src/index.ts
  • apps/mcp-server/tsconfig.json
  • docs/architecture/0005-remote-mcp-server.md
  • docs/integrations/README.md
  • docs/integrations/hermes-mcp.md
  • packages/database/migrations/0021_colorful_saracen.sql
  • packages/database/migrations/meta/0021_snapshot.json
  • packages/database/migrations/meta/_journal.json
  • packages/database/src/schema.ts
  • packages/database/src/verify-clean-install.ts
  • packages/mcp/package.json
  • packages/mcp/src/audit.ts
  • packages/mcp/src/cli-create-installation.ts
  • packages/mcp/src/cli-revoke-installation.ts
  • packages/mcp/src/constants.ts
  • packages/mcp/src/errors.ts
  • packages/mcp/src/index.ts
  • packages/mcp/src/installation.test.ts
  • packages/mcp/src/installation.ts
  • packages/mcp/src/kelpie-gateway.ts
  • packages/mcp/src/mcp.integration.test.ts
  • packages/mcp/src/redact.test.ts
  • packages/mcp/src/redact.ts
  • packages/mcp/src/server.ts
  • packages/mcp/src/tools.ts
  • packages/mcp/tsconfig.json
  • skills/muster-soc-operations/SKILL.md

Comment thread apps/mcp-server/src/index.ts
Comment thread packages/database/migrations/0021_colorful_saracen.sql Outdated
Comment thread packages/mcp/src/cli-revoke-installation.ts
Comment thread packages/mcp/src/installation.ts
Comment thread packages/mcp/src/mcp.integration.test.ts Outdated
Comment thread packages/mcp/src/server.ts
Comment thread packages/mcp/src/tools.ts Outdated
Comment thread packages/mcp/src/tools.ts
Comment thread packages/mcp/src/tools.ts
Corrective pass on PR #82 covering all 9 CodeRabbit findings plus the
bounded-polling/rate-limit nitpicks:

- Authoritative capability + actor-organisation checks for installation
  create/revoke, re-derived from the database inside the mutation's own
  transaction; a composite (actor_id, organisation_id) -> actors(id,
  organisation_id) FK enforces the same tenant boundary at the schema level.
- Graceful HTTP shutdown (drain before closing the DB pool), a real
  dependency-aware /health check, and explicit request/socket timeouts.
- Fixed a latent crash: @muster/database's pg Pool had no 'error' listener,
  so any idle-client error (e.g. a DB restart) was an unhandled Node event
  that killed the whole process -- exactly what the new health check would
  trigger during a real outage. Reproduced live and confirmed fixed.
- Typed ScopeError instead of string-matching for denied-outcome audit
  classification; audit-write failures on the error path are now logged,
  not swallowed.
- Deterministic, time-windowed idempotency keys for Kelpie queries (retries
  dedupe; a genuinely later query still gets fresh data).
- Kept malformed_response distinct from not_found; local query filtering now
  matches specific fields (with a documented best-effort-page caveat) rather
  than a stringified-record substring match.
- Atomic rate-limit check via an integration-scoped advisory lock, mirroring
  appendAuditEvent's existing pattern.
- Rewired the SSRF integration test through the real muster_search_kelpie_cases
  tool/gateway path instead of calling executeGovernedQuery directly.
- Bounded nested evidence depth/breadth in redact.ts.
- Squashed the mcp_installations migration into one clean 0021 (it was only
  ever unreleased in this PR) instead of leaving a same-PR fix-up migration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
packages/database/migrations/0021_dazzling_living_mummy.sql (1)

1-1: 🩺 Stability & Availability | 🔵 Trivial

Plan a non-blocking rollout for this index.

A regular CREATE UNIQUE INDEX blocks writes to actors while it builds. For a populated production table, create it concurrently outside the transactional migration runner, or schedule a maintenance window.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/database/migrations/0021_dazzling_living_mummy.sql` at line 1,
Update the rollout for the actors_id_organisation_unique index so it is created
concurrently outside the transactional migration runner, or explicitly schedule
its creation during a maintenance window. Preserve the unique constraint while
avoiding write blocking on populated production tables.

Source: Linters/SAST tools

packages/mcp/src/mcp.integration.test.ts (1)

375-377: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hoist the inline dynamic imports to the top of the file.

await import("node:crypto") and await import("./installation.ts") mid-test are unnecessary; a static top-level import is simpler and idiomatic here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp/src/mcp.integration.test.ts` around lines 375 - 377, Move the
dynamic imports for node:crypto and hashInstallationToken from the test body to
static top-level imports in mcp.integration.test.ts, then reuse those imported
symbols where mismatchedToken is created and the token is hashed. Remove the
inline await import statements.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/mcp/src/mcp.integration.test.ts`:
- Around line 650-695: Update the integrationQueryRuns lookup in the real MCP
gateway test to also filter by organisationId alongside integrationId, matching
the domain-query scoping used elsewhere in the file; keep the existing ordering
and errorCode assertion unchanged.
- Around line 378-389: Strengthen the rejection assertion for the mismatched
installation insert to match the specific composite actor/organisation
foreign-key violation, rather than accepting any thrown error. Preserve the
existing insert setup and use an error-message matcher consistent with the
database error surfaced by the other authorization checks.

In `@packages/mcp/src/tools.ts`:
- Around line 124-129: Move the synchronous pollKelpieQuery calls out of the
HTTP request handlers in both search and case-lookup flows:
packages/mcp/src/tools.ts lines 124-129 and 164-169. Return an accepted/pending
response immediately after queuing each request, and arrange asynchronous result
retrieval for completion, preserving the existing queued request context and
applying the same pattern to both tools.
- Around line 24-30: Update deterministicIdempotencyKey to remove the
Date.now-based bucket from the key and derive it solely from a stable request ID
combined with the installation and tool context. Ensure repeated logical MCP
calls, including retries crossing the 5-second boundary, produce the same key
for queueKelpieQuery.

---

Nitpick comments:
In `@packages/database/migrations/0021_dazzling_living_mummy.sql`:
- Line 1: Update the rollout for the actors_id_organisation_unique index so it
is created concurrently outside the transactional migration runner, or
explicitly schedule its creation during a maintenance window. Preserve the
unique constraint while avoiding write blocking on populated production tables.

In `@packages/mcp/src/mcp.integration.test.ts`:
- Around line 375-377: Move the dynamic imports for node:crypto and
hashInstallationToken from the test body to static top-level imports in
mcp.integration.test.ts, then reuse those imported symbols where mismatchedToken
is created and the token is hashed. Remove the inline await import statements.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 60e50d03-ef68-49b4-9681-5d1e77f1e7d6

📥 Commits

Reviewing files that changed from the base of the PR and between 3ac2f2c and 4140106.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • apps/mcp-server/package.json
  • apps/mcp-server/src/health.test.ts
  • apps/mcp-server/src/health.ts
  • apps/mcp-server/src/index.ts
  • apps/mcp-server/src/shutdown.test.ts
  • apps/mcp-server/src/shutdown.ts
  • docs/architecture/0005-remote-mcp-server.md
  • docs/integrations/hermes-mcp.md
  • packages/database/migrations/0021_dazzling_living_mummy.sql
  • packages/database/migrations/meta/0021_snapshot.json
  • packages/database/migrations/meta/_journal.json
  • packages/database/src/index.ts
  • packages/database/src/schema.ts
  • packages/mcp/src/cli-create-installation.ts
  • packages/mcp/src/cli-revoke-installation.ts
  • packages/mcp/src/installation.ts
  • packages/mcp/src/kelpie-gateway.ts
  • packages/mcp/src/mcp.integration.test.ts
  • packages/mcp/src/redact.ts
  • packages/mcp/src/server.ts
  • packages/mcp/src/tools.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • apps/mcp-server/package.json
  • packages/mcp/src/cli-revoke-installation.ts
  • packages/mcp/src/cli-create-installation.ts
  • packages/mcp/src/redact.ts
  • docs/integrations/hermes-mcp.md
  • apps/mcp-server/src/index.ts
  • packages/mcp/src/kelpie-gateway.ts
  • packages/mcp/src/server.ts
  • packages/database/migrations/meta/0021_snapshot.json

Comment thread packages/mcp/src/mcp.integration.test.ts Outdated
Comment thread packages/mcp/src/mcp.integration.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 4

🧹 Nitpick comments (2)
packages/database/migrations/0021_dazzling_living_mummy.sql (1)

1-1: 🩺 Stability & Availability | 🔵 Trivial

Plan a non-blocking rollout for this index.

A regular CREATE UNIQUE INDEX blocks writes to actors while it builds. For a populated production table, create it concurrently outside the transactional migration runner, or schedule a maintenance window.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/database/migrations/0021_dazzling_living_mummy.sql` at line 1,
Update the rollout for the actors_id_organisation_unique index so it is created
concurrently outside the transactional migration runner, or explicitly schedule
its creation during a maintenance window. Preserve the unique constraint while
avoiding write blocking on populated production tables.

Source: Linters/SAST tools

packages/mcp/src/mcp.integration.test.ts (1)

375-377: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hoist the inline dynamic imports to the top of the file.

await import("node:crypto") and await import("./installation.ts") mid-test are unnecessary; a static top-level import is simpler and idiomatic here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp/src/mcp.integration.test.ts` around lines 375 - 377, Move the
dynamic imports for node:crypto and hashInstallationToken from the test body to
static top-level imports in mcp.integration.test.ts, then reuse those imported
symbols where mismatchedToken is created and the token is hashed. Remove the
inline await import statements.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/mcp/src/mcp.integration.test.ts`:
- Around line 650-695: Update the integrationQueryRuns lookup in the real MCP
gateway test to also filter by organisationId alongside integrationId, matching
the domain-query scoping used elsewhere in the file; keep the existing ordering
and errorCode assertion unchanged.
- Around line 378-389: Strengthen the rejection assertion for the mismatched
installation insert to match the specific composite actor/organisation
foreign-key violation, rather than accepting any thrown error. Preserve the
existing insert setup and use an error-message matcher consistent with the
database error surfaced by the other authorization checks.

In `@packages/mcp/src/tools.ts`:
- Around line 124-129: Move the synchronous pollKelpieQuery calls out of the
HTTP request handlers in both search and case-lookup flows:
packages/mcp/src/tools.ts lines 124-129 and 164-169. Return an accepted/pending
response immediately after queuing each request, and arrange asynchronous result
retrieval for completion, preserving the existing queued request context and
applying the same pattern to both tools.
- Around line 24-30: Update deterministicIdempotencyKey to remove the
Date.now-based bucket from the key and derive it solely from a stable request ID
combined with the installation and tool context. Ensure repeated logical MCP
calls, including retries crossing the 5-second boundary, produce the same key
for queueKelpieQuery.

---

Nitpick comments:
In `@packages/database/migrations/0021_dazzling_living_mummy.sql`:
- Line 1: Update the rollout for the actors_id_organisation_unique index so it
is created concurrently outside the transactional migration runner, or
explicitly schedule its creation during a maintenance window. Preserve the
unique constraint while avoiding write blocking on populated production tables.

In `@packages/mcp/src/mcp.integration.test.ts`:
- Around line 375-377: Move the dynamic imports for node:crypto and
hashInstallationToken from the test body to static top-level imports in
mcp.integration.test.ts, then reuse those imported symbols where mismatchedToken
is created and the token is hashed. Remove the inline await import statements.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 60e50d03-ef68-49b4-9681-5d1e77f1e7d6

📥 Commits

Reviewing files that changed from the base of the PR and between 3ac2f2c and 4140106.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • apps/mcp-server/package.json
  • apps/mcp-server/src/health.test.ts
  • apps/mcp-server/src/health.ts
  • apps/mcp-server/src/index.ts
  • apps/mcp-server/src/shutdown.test.ts
  • apps/mcp-server/src/shutdown.ts
  • docs/architecture/0005-remote-mcp-server.md
  • docs/integrations/hermes-mcp.md
  • packages/database/migrations/0021_dazzling_living_mummy.sql
  • packages/database/migrations/meta/0021_snapshot.json
  • packages/database/migrations/meta/_journal.json
  • packages/database/src/index.ts
  • packages/database/src/schema.ts
  • packages/mcp/src/cli-create-installation.ts
  • packages/mcp/src/cli-revoke-installation.ts
  • packages/mcp/src/installation.ts
  • packages/mcp/src/kelpie-gateway.ts
  • packages/mcp/src/mcp.integration.test.ts
  • packages/mcp/src/redact.ts
  • packages/mcp/src/server.ts
  • packages/mcp/src/tools.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • apps/mcp-server/package.json
  • packages/mcp/src/cli-revoke-installation.ts
  • packages/mcp/src/cli-create-installation.ts
  • packages/mcp/src/redact.ts
  • docs/integrations/hermes-mcp.md
  • apps/mcp-server/src/index.ts
  • packages/mcp/src/kelpie-gateway.ts
  • packages/mcp/src/server.ts
  • packages/database/migrations/meta/0021_snapshot.json
🛑 Comments failed to post (2)
packages/mcp/src/tools.ts (2)

24-30: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file and inspect the relevant sections.
wc -l packages/mcp/src/tools.ts
sed -n '1,240p' packages/mcp/src/tools.ts

printf '\n--- usages ---\n'
rg -n "deterministicIdempotencyKey|IDEMPOTENCY_WINDOW_MS|idempotency" packages/mcp/src -S

Repository: jusso-dev/Muster

Length of output: 6961


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the surrounding implementation in manageable slices.
sed -n '1,220p' packages/mcp/src/tools.ts
printf '\n---\n'
sed -n '220,420p' packages/mcp/src/tools.ts
printf '\n--- call sites ---\n'
rg -n "deterministicIdempotencyKey\\(" packages -S

Repository: jusso-dev/Muster

Length of output: 6374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect queue dedupe behavior and the corresponding integration test.
sed -n '1,260p' packages/mcp/src/kelpie-gateway.ts

printf '\n--- test ---\n'
sed -n '520,640p' packages/mcp/src/mcp.integration.test.ts

Repository: jusso-dev/Muster

Length of output: 12769


Use a stable request identity for idempotency packages/mcp/src/tools.ts:24-30

A retry that crosses the 5s bucket boundary gets a different key even when it’s the same logical MCP call, so queueKelpieQuery treats it as fresh work and can enqueue a second Kelpie run. Derive the key from a stable request ID plus installation/tool context instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp/src/tools.ts` around lines 24 - 30, Update
deterministicIdempotencyKey to remove the Date.now-based bucket from the key and
derive it solely from a stable request ID combined with the installation and
tool context. Ensure repeated logical MCP calls, including retries crossing the
5-second boundary, produce the same key for queueKelpieQuery.

Source: Coding guidelines


124-129: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Move connector-result waiting off the HTTP request path.

Both tools wait up to eight seconds for worker-backed Kelpie processing, tying up MCP HTTP capacity during connector slowness. Return an accepted/pending result and retrieve completion asynchronously instead.

  • packages/mcp/src/tools.ts#L124-L129: replace synchronous search polling with asynchronous result handling.
  • packages/mcp/src/tools.ts#L164-L169: apply the same asynchronous pattern to case lookup.

As per coding guidelines, “Keep long-running integration and agent work outside HTTP request handlers.”

📍 Affects 1 file
  • packages/mcp/src/tools.ts#L124-L129 (this comment)
  • packages/mcp/src/tools.ts#L164-L169
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp/src/tools.ts` around lines 124 - 129, Move the synchronous
pollKelpieQuery calls out of the HTTP request handlers in both search and
case-lookup flows: packages/mcp/src/tools.ts lines 124-129 and 164-169. Return
an accepted/pending response immediately after queuing each request, and arrange
asynchronous result retrieval for completion, preserving the existing queued
request context and applying the same pattern to both tools.

Source: Coding guidelines

jusso-dev and others added 2 commits July 28, 2026 15:43
MUSTER_DEMO_MODE seeding reused the bootstrap workspace's admin@muster.local
identity and soc-operations room slug, so an ambiguous lookup by either
identifier could resolve to the wrong organisation's row and crash the web
dev server mid-request (surfacing as "Second seeded room required" and
cascading into unrelated e2e failures). Rename the two starter fixtures
before seeding demo data, and switch the e2e suite and room-view.tsx off
hardcoded starter-namespace UUIDs onto demoIds so they stay in sync.

Confirmed pre-existing on main (identical failure signature); not
introduced by this PR. Verified locally: 26/26 passed, 0 failed,
1 skipped (previously 9 failed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Tighten the FK-violation assertion to check the driver's underlying
  cause message (foreign key/violates), not just "any thrown error" -
  drizzle-orm wraps the real Postgres error under `.cause`.
- Scope the SSRF-denial run lookup by organisationId in addition to
  integrationId, matching every other domain query in the file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/database/src/seed.ts`:
- Around line 18-29: Update the two seed updates for actors and rooms to include
starterIds.organisation in their WHERE predicates alongside the existing record
ID conditions. Keep both updates tenant-scoped while preserving their current
field values and target IDs.
- Around line 18-29: Wrap the actor and room updates in the seed bootstrap flow
within a single db.transaction callback, using the transaction handle for both
update operations. Preserve the existing update values and conditions so both
changes commit together or roll back together.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7728ba36-048b-453d-8bf0-c39b44ceeb86

📥 Commits

Reviewing files that changed from the base of the PR and between 4140106 and 09ab17c.

📒 Files selected for processing (4)
  • apps/web/components/room-view.tsx
  • packages/database/src/seed.ts
  • packages/mcp/src/mcp.integration.test.ts
  • tests/muster.spec.ts

Comment on lines +18 to +29
await db
.update(schema.actors)
.set({ identityReference: "starter-admin@muster.local" })
.where(sql`${schema.actors.id} = ${starterIds.actors.jordan}`);
await db
.update(schema.rooms)
.set({
name: "starter-soc-operations",
slug: "starter-soc-operations",
displayName: "Starter SOC operations",
})
.where(sql`${schema.rooms.id} = ${starterIds.rooms.soc}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Scope both seed updates by organisation.

The WHERE clauses only match record IDs. Add starterIds.organisation to both predicates so this cleanup remains tenant-scoped if fixtures are reused or data is inconsistent. As per coding guidelines, scope every domain query by organisation.

Proposed fix
-  .where(sql`${schema.actors.id} = ${starterIds.actors.jordan}`);
+  .where(sql`
+    ${schema.actors.id} = ${starterIds.actors.jordan}
+    AND ${schema.actors.organisationId} = ${starterIds.organisation}
+  `);

-  .where(sql`${schema.rooms.id} = ${starterIds.rooms.soc}`);
+  .where(sql`
+    ${schema.rooms.id} = ${starterIds.rooms.soc}
+    AND ${schema.rooms.organisationId} = ${starterIds.organisation}
+  `);
📝 Committable suggestion

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

Suggested change
await db
.update(schema.actors)
.set({ identityReference: "starter-admin@muster.local" })
.where(sql`${schema.actors.id} = ${starterIds.actors.jordan}`);
await db
.update(schema.rooms)
.set({
name: "starter-soc-operations",
slug: "starter-soc-operations",
displayName: "Starter SOC operations",
})
.where(sql`${schema.rooms.id} = ${starterIds.rooms.soc}`);
await db
.update(schema.actors)
.set({ identityReference: "starter-admin@muster.local" })
.where(sql`
${schema.actors.id} = ${starterIds.actors.jordan}
AND ${schema.actors.organisationId} = ${starterIds.organisation}
`);
await db
.update(schema.rooms)
.set({
name: "starter-soc-operations",
slug: "starter-soc-operations",
displayName: "Starter SOC operations",
})
.where(sql`
${schema.rooms.id} = ${starterIds.rooms.soc}
AND ${schema.rooms.organisationId} = ${starterIds.organisation}
`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/database/src/seed.ts` around lines 18 - 29, Update the two seed
updates for actors and rooms to include starterIds.organisation in their WHERE
predicates alongside the existing record ID conditions. Keep both updates
tenant-scoped while preserving their current field values and target IDs.

Source: Coding guidelines


🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

rg -n 'db\.transaction|\.transaction\(async' packages/database/src packages/mcp/src apps

Repository: jusso-dev/Muster

Length of output: 6577


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## packages/database/src/seed.ts\n'
ast-grep outline packages/database/src/seed.ts --view expanded || true
printf '\n## relevant slice\n'
cat -n packages/database/src/seed.ts | sed -n '1,120p'

printf '\n## packages/database/src/bootstrap-connectors.ts\n'
ast-grep outline packages/database/src/bootstrap-connectors.ts --view expanded || true
printf '\n## relevant slice\n'
cat -n packages/database/src/bootstrap-connectors.ts | sed -n '90,150p'

Repository: jusso-dev/Muster

Length of output: 7800


Wrap the bootstrap cleanup in a transaction. If the actor update succeeds and the room update fails, demo seeding leaves a partially rewritten bootstrap state. Put both updates in db.transaction(...) so they commit or roll back together.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/database/src/seed.ts` around lines 18 - 29, Wrap the actor and room
updates in the seed bootstrap flow within a single db.transaction callback,
using the transaction handle for both update operations. Preserve the existing
update values and conditions so both changes commit together or roll back
together.

Source: Coding guidelines

The image job's sbom/provenance/upload-artifact steps had no if: always(),
so a HIGH/CRITICAL trivy finding (exit-code 1) skipped every step after it
and the sarif was never uploaded - the job fails with zero visibility into
which CVE tripped it. Add if: always() to those three steps so a failing
scan still produces an inspectable artifact.

release-security/image is failing on this PR against a locally-reproduced
build of the same unmodified Dockerfile with zero HIGH/CRITICAL findings
in repeated local trivy scans - this change is to see what CI is actually
finding, not to weaken the gate (exit-code: 1 is untouched).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/security.yml:
- Around line 77-91: Separate the required evidence files muster-sbom.cdx.json
and SHA256SUMS from optional diagnostics in the security-artifacts upload step.
Make the required upload fail when either file is missing, while keeping
provenance.json and trivy.sarif in a separate upload that may continue with
missing files; update the workflow around the upload-artifact step without
changing the artifact contents or generation commands.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4e08d68e-92b0-4828-a513-03fb2364c7dd

📥 Commits

Reviewing files that changed from the base of the PR and between 09ab17c and a36896f.

📒 Files selected for processing (1)
  • .github/workflows/security.yml

Comment on lines +77 to +91
if: always()
run: |
printf '{"commit":"%s","workflow":"%s","runId":"%s","image":"muster:security"}\n' \
"$GITHUB_SHA" "$GITHUB_WORKFLOW" "$GITHUB_RUN_ID" > provenance.json
sha256sum muster-sbom.cdx.json provenance.json > SHA256SUMS
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: security-artifacts
path: |
muster-sbom.cdx.json
provenance.json
SHA256SUMS
trivy.sarif
if-no-files-found: ignore

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant workflow region with line numbers.
sed -n '1,140p' .github/workflows/security.yml | cat -n

# Also locate all uses of upload-artifact in this workflow for context.
rg -n "upload-artifact|if-no-files-found|muster-sbom|trivy\.sarif|provenance\.json|SHA256SUMS" .github/workflows/security.yml

Repository: jusso-dev/Muster

Length of output: 4566


🌐 Web query:

GitHub Actions upload-artifact if-no-files-found partial path list missing files semantics

💡 Result:

In GitHub Actions, the upload-artifact action provides the if-no-files-found input to control behavior when the specified path does not match any files [1][2][3]. The semantics of if-no-files-found apply to the collection of files resulting from the path input (which supports file, directory, or wildcard patterns) [2][4][3]. When the action evaluates the provided paths and finds no files, it follows the behavior specified by this setting: - warn (default): The action completes successfully but outputs a warning in the workflow logs [1][2][5]. - error: The action fails, causing the workflow step to exit with an error message [1][2][3]. - ignore: The action completes successfully without outputting any warnings or errors [2][4][3]. Regarding "partial path list missing files": If you provide a list of paths (via a YAML multiline string) and some paths match files while others do not, the upload-artifact action generally evaluates the entire set. If at least one file is found across the provided paths, the action typically considers the task successful (regardless of whether specific paths in that list were empty). The if-no-files-found setting is triggered only if the final result of the evaluation is an empty set of files [1][2][3]. If you are experiencing issues where you believe files should be found but are not, ensure that: 1. You are using absolute paths from the root of the repository (or correctly relative to the project root), as the action does not necessarily respect working-directory settings from other steps [6]. 2. Hidden files are not being inadvertently excluded; you must set include-hidden-files: true to include them [6][7]. 3. You are using the latest version of the action (e.g., v4 or higher) to ensure you have access to the full if-no-files-found functionality [1][4][8].

Citations:


🌐 Web query:

"upload-artifact" "if-no-files-found" "all files are missing" documentation

💡 Result:

The if-no-files-found input parameter for the GitHub Actions upload-artifact action allows you to customize the behavior when the specified path results in no files being found [1][2]. The available options are [1][3]: - warn: Prints a warning in the logs but the action succeeds. This is the default behavior [1][2]. - error: Marks the workflow run as failed and displays an error message [2][3]. - ignore: Suppresses all warnings and errors; the action does not fail [2][3]. Usage Example: - uses: actions/upload-artifact@v7 with: name: my-artifact path: path/to/artifact/ if-no-files-found: error Note that this parameter triggers when no files are found across the entire provided path configuration [1]. Users have reported that when multiple paths are specified (e.g., using a list), the action may still succeed if some files are found even if others are missing, as it does not explicitly guarantee that every single specified path or file exists [4].

Citations:


Keep required evidence separate from optional diagnostics
.github/workflows/security.yml:77-91if-no-files-found only fails when nothing matches, so this upload can still succeed with provenance.json or trivy.sarif alone if muster-sbom.cdx.json or SHA256SUMS is missing. Split the required evidence into a strict upload or add explicit existence checks before uploading.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/security.yml around lines 77 - 91, Separate the required
evidence files muster-sbom.cdx.json and SHA256SUMS from optional diagnostics in
the security-artifacts upload step. Make the required upload fail when either
file is missing, while keeping provenance.json and trivy.sarif in a separate
upload that may continue with missing files; update the workflow around the
upload-artifact step without changing the artifact contents or generation
commands.

jusso-dev and others added 2 commits July 28, 2026 16:17
@modelcontextprotocol/sdk@1.29.0 pins a transitive dependency on
@hono/node-server@1.19.15, which carries GHSA-frvp-7c67-39w9 (Windows
path traversal via encoded backslash in serve-static, fixed in 2.0.5).
This dependency chain already existed on main via packages/agent-harness
and is not introduced by this PR, but it fails release-security/image's
Trivy gate on every run.

Bump @modelcontextprotocol/sdk to 1.30.0 (the first version whose
package.json accepts @hono/node-server 2.x) across all three consumers
(packages/agent-harness, packages/mcp, apps/mcp-server), and add a pnpm
workspace override pinning @hono/node-server to ^2.0.5 as a floor for any
future dependency path. Verified: pnpm-lock.yaml now resolves
@hono/node-server@2.0.12; pnpm build (25/25), lint (25/25), and the
mcp/mcp-server/agent-harness test suites (18+4+15 passing) are
unaffected; a fresh local build + trivy scan against the exact CI
invocation (severity HIGH,CRITICAL, ignore-unfixed, sarif format)
now exits 0 with zero findings, where it previously failed on this
specific CVE.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docker exporter cannot load multi-artifact manifest lists. Keep
provenance+SBOM on push (registry export); drop them on PR load
so Trivy and local inspect still work.
@jusso-dev
jusso-dev merged commit 820eef1 into main Jul 28, 2026
8 checks passed
@jusso-dev
jusso-dev deleted the claude/issue-72-hermes-mcp-20260728-130015 branch July 28, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P0: Ship the first Hermes-native Muster MCP vertical slice

1 participant