Skip to content

Promote to main: MCP + UI defect fixes from skeptical audit#45

Merged
mvalancy merged 7 commits into
mainfrom
develop
Jun 13, 2026
Merged

Promote to main: MCP + UI defect fixes from skeptical audit#45
mvalancy merged 7 commits into
mainfrom
develop

Conversation

@mvalancy

Copy link
Copy Markdown
Member

Promotes three verified, independently-merged bug fixes from develop to main. All found during a skeptical live-app audit and each guarded by a test the mock-only suites missed.

Each PR is green on the full dev-stack gate (unit, real-Neo4j MCP contract, smoke gate, perf, showcase).

🤖 Generated with Claude Code

mvalancy and others added 3 commits June 13, 2026 10:31
Skeptical sweep of all routes (desktop + phone) found every page logged
a console error: '503 /mcp/status'. The MCP server is an OPTIONAL
subsystem (we already gray out AI & Agents when it's offline), but the
server's /mcp/status proxy returned 503 when MCP was unreachable — and
browsers log failed resource loads (5xx) to the console regardless of
try/catch, so it was unavoidable noise site-wide (2 console errors per
page load).

Fix: return 200 with { connected: false, status: 'offline' } for the
offline case. The client already treats connected:false as offline, so
behavior is unchanged — just no more console error. Verified: /mcp/status
now 200, and /, /settings, /backend load with 0 console errors (was 2).

Regression guard: user-smoke now fails on ANY 5xx from our own origin
during the session and sweeps the main routes — this class of bug was
invisible because the gate never checked non-GraphQL responses.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Fix get_graph_context reporting an empty graph as "not found"

The type/status tally used `CALL { WITH items UNWIND items as i RETURN
i.type as type, count(i) as cnt }`. UNWIND of an empty list yields ZERO
rows, and a correlated CALL subquery returning zero rows eliminates the
outer row — so for a brand-new empty graph the whole query returned no
records and getGraphContext threw "Graph with ID X not found", even
though the Graph node plainly existed.

Return the raw type/status lists from Cypher and tally them in JS
instead. The blockers/recent subqueries were already safe because they
end in `collect(...)` (aggregation always yields one row).

Adds two real-Neo4j contract cases: an empty graph returns zero counts
(not an error), and a populated graph tallies byType/byStatus correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add smoke guard: a brand-new empty graph shows its empty-state, not an error

UI counterpart to the get_graph_context empty-graph fix. Verifies that
creating a graph with zero work items and opening it renders the
"Create Your First Work Item" invitation with no error chrome and no
uncaught JS errors. Passes against the live dev stack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Update mock-neo4j for the new get_graph_context query shape

getGraphContext now returns raw types/statuses lists (tallied in JS)
instead of pre-aggregated typeCounts/statusCounts, so the mock driver's
canned record must match. Match on the new query (size(items) as
nodeCount / as statuses) and return raw label arrays.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two real, AI-reachable bugs in the clone_graph MCP tool:

1. ParameterMissing(teamId): Neo4j does not persist null-valued
   properties, so a graph created without a teamId has NO teamId property.
   cloneGraph read it back as `undefined` and passed it straight to the
   driver, which rejects undefined param values — so clone threw for
   essentially every graph created via the API. Coalesce teamId (and
   type/isShared/settings defensively) to non-undefined values.

2. Edge types silently collapsed to DEPENDS_ON: the edge-clone query
   matched every relationship type but hard-coded `CREATE (a)-[:DEPENDS_ON
   ...]->(b)`, rewriting BLOCKS / RELATES_TO / CONTAINS / PART_OF into
   DEPENDS_ON on clone. Use apoc.create.relationship(newW, type(r), ...)
   to preserve the real type (APOC 5.x ships with the project's Neo4j).

Adds a real-Neo4j contract case: clone a graph with BLOCKS + RELATES_TO
edges and assert all nodes copy and both edge types survive. The existing
mock unit tests passed through both bugs — they can't reproduce real-DB
param/relationship-type behavior, which is exactly why the contract test
exists.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🧪 Comprehensive Test Suite

  • Unit suites (Node 18.x & 20.x) — core, web, server, mcp-server: ✅ passed
  • Installer & deploy config: ✅ passed

Full-stack smoke gate runs in the CI workflow.

docker-publish.yml used `type=sha,prefix={{branch}}-`, but on a
pull_request event the metadata-action `{{branch}}` placeholder is empty,
so the computed tag became `ghcr.io/...:-<sha>` — an invalid reference
that failed every build-and-push job on PRs targeting main (blocking the
develop→main promotion / prod image pipeline). Use a static `sha-` prefix
so the tag is valid on branch pushes, tags, and PRs alike.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🧪 Comprehensive Test Suite

  • Unit suites (Node 18.x & 20.x) — core, web, server, mcp-server: ✅ passed
  • Installer & deploy config: ✅ passed

Full-stack smoke gate runs in the CI workflow.

…ans) (#47)

The core "humans and AI as peers on one graph" promise was broken: MCP
create_edge wrote a DIRECT Neo4j relationship (a)-[:TYPE]->(b), but the
GraphQL server and web UI model edges as Edge NODES joined via
EDGE_SOURCE / EDGE_TARGET. Proven live: an MCP-created edge produced 0
rows in the GraphQL `edges` query, so edges an AI created were invisible
in the web UI (and human/web-created Edge nodes were invisible to every
MCP read, which only traversed direct rels). The two layers were fully
bifurcated.

This migrates the ENTIRE MCP edge surface onto the canonical Edge-node
model the server already uses:
- Writes: createEdge, executeBulkCreateEdge (MERGE an Edge node +
  EDGE_SOURCE/EDGE_TARGET, idempotent); deleteEdge, executeBulkDeleteEdge
  (DETACH DELETE the Edge node).
- Reads: getNodeDetails relationships, getGraphContext (edge count +
  blockers), getGraphDetails, browseGraph 'dependencies', cloneGraph edge
  clone — all traverse Edge nodes now.
- Analytics: analyzeGraphHealth, getPriorityInsights, getBottlenecks,
  getContributorPriorities swap direct-rel patterns for Edge-node ones.
- Variable-length: findPath and detectCycles use Neo4j 5 quantified path
  patterns / EDGE_SOURCE|EDGE_TARGET traversal and project back to
  WorkItem nodes + edge types (verified live: path A->C type DEPENDS_ON,
  cycle A->B->C->A len 3).

Tests: new real-Neo4j contract case asserts an MCP-created edge is a
web-visible Edge node (and NOT a stray direct rel), and that deleteEdge
removes it. cloneGraph contract case now verifies via Edge nodes. Mock
driver updated for the new relationships query + count(newE). Full
CI-mode unit suite (305) green; contract suite (7) green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🧪 Comprehensive Test Suite

  • Unit suites (Node 18.x & 20.x) — core, web, server, mcp-server: ✅ passed
  • Installer & deploy config: ✅ passed

Full-stack smoke gate runs in the CI workflow.

The build-stack job assembled the multi-image stack manifest from a
`${GITHUB_SHA::8}` tag that the build-and-push job never pushes, so it
failed on every PR (e.g. the develop→main promotion). Use a tag that is
actually published: `pr-<number>` on pull_request events (matches
metadata-action's type=ref,event=pr), `latest` on main, the branch name
on other branch pushes, and a `sha-<short>` fallback that matches the
type=sha tag.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🧪 Comprehensive Test Suite

  • Unit suites (Node 18.x & 20.x) — core, web, server, mcp-server: ✅ passed
  • Installer & deploy config: ✅ passed

Full-stack smoke gate runs in the CI workflow.

…ity with web) (#49)

* create_node: attach to a graph via graph_id (orphan nodes were invisible)

Node-level twin of the edge parity fix (#47). The web lists nodes
per-graph (workItems where graph.id = currentGraph), but MCP create_node
had no graph_id parameter and never created a BELONGS_TO relationship, so
every node an AI created was orphaned and shown in NO graph view. This
also undermined the edge fix: an AI's nodes never appeared, so neither
did the edges between them.

Add an optional graph_id to the create_node tool + CreateNodeArgs; when
present, MATCH the graph and MERGE (n)-[:BELONGS_TO]->(g). A graph_id
that matches no graph returns a clean error instead of silently creating
an unattached node. Omitting graph_id stays backward-compatible.

Real-Neo4j contract test covers all three: attached (BELONGS_TO + shows
in get_graph_context), and unknown graph_id errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* create_node: write canonical WorkItem fields the web reads

MCP node creation also used non-canonical property names the GraphQL
server/web schema don't read: priorityComputed (vs priorityComp),
sphericalRadius/Theta/Phi (vs radius/theta/phi), and it never set
positionX/Y/Z or priority at all. So AI-created nodes showed priority 0
at the origin in the web, and MCP was internally inconsistent
(createNode + browseGraph used priorityComputed while updateNode and the
priority/bottleneck analytics used priorityComp — updates were invisible
to browse-by-priority and vice versa).

Standardize createNode and executeBulkCreateNode on the canonical schema
fields (positionX/Y/Z, radius, theta, phi, priority, priorityComp) while
keeping the MCP-internal priorityExec/Indiv/Comm decomposition, and fix
browseGraph by_priority to read priorityComp. Contract test now also
asserts the canonical fields are set and no legacy priorityComputed
remains.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🧪 Comprehensive Test Suite

  • Unit suites (Node 18.x & 20.x) — core, web, server, mcp-server: ✅ passed
  • Installer & deploy config: ✅ passed

Full-stack smoke gate runs in the CI workflow.

@mvalancy mvalancy merged commit f1ecce8 into main Jun 13, 2026
36 checks passed
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.

1 participant