MCP: add upsert_entity and upsert_relationship tools (#4864)#5301
Conversation
…Insert and allowUpdate
… from identifier rejection message
|
Tick the box to add this pull request to the merge queue (same as
|
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 8 |
🟢 Coverage 97.14% diff coverage · -6.94% coverage variation
Metric Results Coverage variation ✅ -6.94% coverage variation Diff coverage ✅ 97.14% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (2f3ff7f) 140111 104363 74.49% Head commit (89da34e) 172152 (+32041) 116289 (+11926) 67.55% (-6.94%) Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#5301) 140 136 97.14% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Code Review
This pull request implements two new MCP tools, upsert_entity and upsert_relationship, to perform injection-safe match-or-create operations using parameterized Cypher MERGE statements. It introduces a shared utility helper for parameterized writes and identifier quoting, registers the tools in the dispatcher, and adds comprehensive integration and unit tests. The review feedback correctly points out that the database parameter in both tools is retrieved without defensive validation, which could cause a JSONException instead of a clean IllegalArgumentException if omitted by the client.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
|
||
| public static JSONObject execute(final ArcadeDBServer server, final ServerSecurityUser user, final JSONObject args, | ||
| final MCPConfiguration config) { | ||
| final String databaseName = args.getString("database"); |
There was a problem hiding this comment.
The database parameter is retrieved using args.getString("database") without a default value or validation. If the client omits this parameter, it will throw a JSONException instead of a clean IllegalArgumentException. It is safer to retrieve it with a default value of null and validate it defensively, consistent with how typeName is handled.
| final String databaseName = args.getString("database"); | |
| final String databaseName = args.getString("database", null); | |
| if (databaseName == null || databaseName.isBlank()) | |
| throw new IllegalArgumentException("'database' is required"); |
There was a problem hiding this comment.
Applied. These tools already validate every other required arg (typeName/matchKeys) defensively, and resolveDatabase returns agent-friendly errors, so a raw getString throw on missing database was the odd one out. Now: getString("database", null) + null/blank guard -> clean IllegalArgumentException. (725ff4a)
|
|
||
| public static JSONObject execute(final ArcadeDBServer server, final ServerSecurityUser user, final JSONObject args, | ||
| final MCPConfiguration config) { | ||
| final String databaseName = args.getString("database"); |
There was a problem hiding this comment.
The database parameter is retrieved using args.getString("database") without validation. Since a require helper method is already defined in this class, we should reuse it to ensure consistent validation and throw a clean IllegalArgumentException if the parameter is missing or blank.
| final String databaseName = args.getString("database"); | |
| final String databaseName = require(args, "database"); |
There was a problem hiding this comment.
Applied - reused the existing require() helper: final String databaseName = require(args, "database");. (725ff4a)
Code Review: MCP
|
Review: MCP
|
…s; cover empty rel match-keys and backtick-identifier rejection; note scalar values
|
Addressed in 6cc243d:
|
…dex on match keys
|
Verified all four against the engine and addressed them (641f2f6, plus 6cc243d earlier): 1. Idempotency requires a UNIQUE index - confirmed and now documented. Checked 2. Type auto-creation bypassing 3. Non-scalar values. Addressed in 6cc243d: confirmed 4. Duplicated merge loop. Addressed in 6cc243d: Minors noted: the double parse mirrors |
Review:
|
…and setProperties injection
|
Addressed the top items in 89da34e:
On the others: |
Review: MCP
|
Review: MCP
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #5301 +/- ##
============================================
+ Coverage 65.55% 65.60% +0.04%
- Complexity 968 971 +3
============================================
Files 1712 1714 +2
Lines 140111 140251 +140
Branches 30013 30023 +10
============================================
+ Hits 91854 92012 +158
+ Misses 35789 35777 -12
+ Partials 12468 12462 -6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
What does this PR do?
Adds two MCP tools,
upsert_entityandupsert_relationship, that perform injection-safe match-or-create-by-key via parameterized CypherMERGE:upsert_entity- match (or create) a vertex bymatchKeys, then applysetProperties.upsert_relationship- match (or create) a directed edge between twoMERGEd endpoints, keyed by endpoints +relType, then applyrelPropertiesin a trailingSET(so repeated calls update the edge, never duplicate it).Every caller value is a bound Cypher parameter; identifiers (type name, relationship type, property keys) are backtick-quoted via a new
MCPToolUtils.quoteIdentifierthat rejects embedded backticks so the quoting can't be escaped. A sharedMCPToolUtils.executeParameterizedWriteruns the generated Cypher through the sameengine.analyze()->ExecuteCommandTool.checkPermission()gatingexecute_commanduses, executes in a transaction, and serializes identically. No engine changes; registration is centralized inMCPDispatcher.Motivation
Part of Wave 1 of the MCP GraphRAG & Agent-Memory epic (#4859). Models reliably build match-key logic wrong when hand-writing
execute_command(string-concatenatedWHEREclauses, missing normalization) and duplicate nodes/edges as a result - the most common agent-memory failure mode. Wrapping match-or-create behind a parameterized tool is a concrete correctness win and removes the injection vector that interpolated match keys carry.Design and plan are committed on the branch:
docs/superpowers/specs/2026-07-15-mcp-upsert-tools-4864-design.mdanddocs/superpowers/plans/2026-07-15-mcp-upsert-tools-4864.md.Related issues
Additional Notes
MERGEanalyzes to{CREATE, UPDATE}in ArcadeDB's OpenCypher engine (OpenCypherQueryEngine.getOperationTypes()adds both wheneverhasMerge()is true, regardless ofSET), so every upsert requires bothallowInsertandallowUpdate. Matches the issue's "require both as a single unit" intent and the pre-existingMCPPermissionsTest.upsertRequiresBothCreateAndUpdatestub. Safe-by-default is unchanged (no new flag; write flags defaultfalse).allowInsertis intentional (ArcadeDBMERGEauto-creates the vertex/edge type), for incremental graph building - documented in the spec.typeName, nottype, to avoid the JSON-Schematypekeyword collision (same precedent asfull_text_searchMCP: add full_text_search tool #4862). Issue MCP: add upsert_entity and upsert_relationship tools #4864's schema text should be amended to match.arcadedb-docs; a separate follow-up, as with MCP: add full_text_search tool #4862.mvn -pl server test -Dtest='MCP*'-> 114/114 green (a fullmvn clean packagewas not run locally; CI will cover it). Tests exercise the running server end-to-end and cover idempotency, no-duplicate edges (incl. the edge property actually updating), endpoint auto-creation, injection inertness (x'}) DETACH DELETE n //stored as a literal, deletes nothing), permission denial naming the missing flag, empty-matchKeysrejection, and identifier validation. Tool count 11 -> 13 in both transports.Checklist
mvn clean packagecommand (ran the targetedmvn -pl server test -Dtest='MCP*'suite, 114/114 green; full package build left to CI)🤖 Generated with Claude Code