You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hugegraph.apache.org by ji...@apache.org on 2026/09/26 12:27:34 UTC
(hugegraph-ai) 07/11: chore(mcp): align write docs and quality checks
This is an automated email from the ASF dual-hosted git repository.
imbajin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hugegraph-ai.git
commit ddb83fd412be970b1e90bea7fa27d61a9dc83f56
Author: duyifeng01 <du...@baidu.com>
AuthorDate: Fri Sep 4 23:45:31 2026 +0800
chore(mcp): align write docs and quality checks
- Document canonical write confirmation workflows.
- Describe supported operations and preview-only boundaries.
- Normalize license headers and formatting checks.
---
.workflow/pr73-write-safety/design.md | 522 +++++++++++++
.../logs/task-1.1-current-state-inventory.md | 58 ++
.../logs/task-1.2-backend-capabilities.md | 60 ++
.../logs/task-1.3-confirmation-contract.md | 63 ++
.../logs/task-10-final-verification.md | 46 ++
.../logs/task-2.1-write-plan-tdd.md | 50 ++
.../logs/task-2.2-canonical-write-model.md | 47 ++
.../logs/tasks-2.3-2.5-plan-store.md | 30 +
.../logs/tasks-3.1-3.6-confirm-status-reconcile.md | 33 +
.../logs/tasks-4.1-6.4-atomic-boundaries.md | 28 +
.../logs/tasks-4.2-4.7-7.1-7.4-graph-workflow.md | 29 +
.../logs/tasks-8.1-8.5-runtime-safety.md | 30 +
.../logs/tasks-9.1-9.3-public-contract.md | 19 +
.workflow/pr73-write-safety/requirements.md | 16 +
.workflow/pr73-write-safety/tasks.md | 76 ++
hugegraph-mcp/README.md | 281 +++----
hugegraph-mcp/README.zh-CN.md | 288 +++----
hugegraph-mcp/docs/p0a-integration-checklist.md | 825 +++++----------------
.../hugegraph_mcp/backend_capabilities.py | 10 +-
hugegraph-mcp/hugegraph_mcp/gremlin_policy.py | 47 +-
hugegraph-mcp/hugegraph_mcp/gremlin_tools.py | 4 +-
hugegraph-mcp/hugegraph_mcp/plan_store.py | 10 +-
hugegraph-mcp/hugegraph_mcp/reconciler.py | 16 +-
hugegraph-mcp/hugegraph_mcp/schema_tools.py | 12 +-
hugegraph-mcp/hugegraph_mcp/server.py | 2 +-
.../hugegraph_mcp/tools/extract_graph_data.py | 3 +
.../hugegraph_mcp/tools/generate_gremlin.py | 13 +-
.../hugegraph_mcp/tools/graph_data_mapping.py | 16 +-
.../hugegraph_mcp/tools/graph_write_adapter.py | 10 +-
.../hugegraph_mcp/tools/graph_write_plan.py | 10 +-
.../hugegraph_mcp/tools/ingest_graph_data.py | 5 +-
hugegraph-mcp/hugegraph_mcp/tools/inspect_graph.py | 12 +-
.../hugegraph_mcp/tools/inspect_schema.py | 38 +-
.../hugegraph_mcp/tools/manage_graph_data.py | 2 +-
hugegraph-mcp/hugegraph_mcp/tools/manage_schema.py | 31 +-
.../hugegraph_mcp/tools/mutate_graph_properties.py | 4 +-
.../hugegraph_mcp/tools/query_graph_data.py | 12 +-
.../hugegraph_mcp/tools/schema_contract.py | 86 +--
hugegraph-mcp/hugegraph_mcp/tools/schema_utils.py | 17 +-
.../hugegraph_mcp/tools/schema_write_adapter.py | 10 +-
hugegraph-mcp/hugegraph_mcp/write_contract.py | 10 +-
hugegraph-mcp/hugegraph_mcp/write_executor.py | 20 +-
hugegraph-mcp/hugegraph_mcp/write_limits.py | 18 +-
hugegraph-mcp/hugegraph_mcp/write_plan.py | 10 +-
.../tests/integration/test_real_write_path.py | 9 +-
hugegraph-mcp/tests/test_backend_capabilities.py | 11 +-
hugegraph-mcp/tests/test_config.py | 3 +-
hugegraph-mcp/tests/test_confirm_write.py | 11 +-
hugegraph-mcp/tests/test_error_handling.py | 19 +-
hugegraph-mcp/tests/test_generate_gremlin.py | 31 +-
hugegraph-mcp/tests/test_get_live_schema.py | 8 +-
hugegraph-mcp/tests/test_graph_write_workflow.py | 10 +-
hugegraph-mcp/tests/test_gremlin_policy.py | 6 +-
hugegraph-mcp/tests/test_gremlin_safety.py | 8 +-
hugegraph-mcp/tests/test_hugegraph_ai_client.py | 1 +
hugegraph-mcp/tests/test_import_graph_data_tool.py | 9 +-
hugegraph-mcp/tests/test_ingest_graph_data.py | 1 +
hugegraph-mcp/tests/test_inspect_graph.py | 58 +-
hugegraph-mcp/tests/test_manage_schema.py | 23 +-
hugegraph-mcp/tests/test_plan_hash.py | 6 +-
hugegraph-mcp/tests/test_plan_store_interface.py | 11 +-
hugegraph-mcp/tests/test_plan_store_migration.py | 11 +-
hugegraph-mcp/tests/test_property_validation.py | 1 +
hugegraph-mcp/tests/test_public_docs_contract.py | 128 ++++
hugegraph-mcp/tests/test_query_graph_data_tool.py | 36 +-
hugegraph-mcp/tests/test_reconciler.py | 11 +-
hugegraph-mcp/tests/test_refresh_vid_embeddings.py | 6 +-
hugegraph-mcp/tests/test_schema_utils.py | 8 +-
hugegraph-mcp/tests/test_schema_write_workflow.py | 11 +-
hugegraph-mcp/tests/test_v1_stable_tools.py | 1 +
.../tests/test_write_confirmation_contract.py | 11 +-
hugegraph-mcp/tests/test_write_executor_faults.py | 11 +-
hugegraph-mcp/tests/test_write_plan.py | 13 +-
.../src/pyhugegraph/api/property_cas.py | 10 +-
.../src/tests/api/test_property_cas_contract.py | 10 +-
.../src/tests/api/test_raw_gremlin_contract.py | 10 +-
pyproject.toml | 1 +
77 files changed, 1987 insertions(+), 1446 deletions(-)
diff --git a/.workflow/pr73-write-safety/design.md b/.workflow/pr73-write-safety/design.md
new file mode 100644
index 00000000..463e4461
--- /dev/null
+++ b/.workflow/pr73-write-safety/design.md
@@ -0,0 +1,522 @@
+# Design: HugeGraph MCP Confirmed Write Safety
+
+## 1. Overview
+
+### 1.1 Goal
+
+Build one coherent write model for HugeGraph MCP that can answer three independent questions:
+
+1. What immutable intent did the user approve?
+2. Did the database apply that intent atomically against the approved precondition?
+3. What is the durable outcome after timeout, process crash, restart, or partial workflow progress?
+
+The design covers the confirmed defects and the subsequent independent review. It replaces tool-specific confirmation and recovery behavior with a shared plan, execution, and reconciliation path.
+
+### 1.2 Requirements source
+
+The approved requirements are the defect and remediation package in `hugegraph-pr73-review.zip`, plus the findings verified against the current working tree:
+
+- delete targets must be bound to stable backend IDs;
+- edge creation must bind both endpoint IDs before the write;
+- property mutation requires backend-enforced compare-and-set;
+- ambiguous writes must become durable `UNKNOWN` outcomes;
+- partial workflows must never be reported as rejected/no-op;
+- schema plans contain exactly one create operation;
+- raw Gremlin execution requires enforceable resource boundaries;
+- configuration, public contracts, and documentation must match runtime behavior.
+
+### 1.3 In scope
+
+- A server-side immutable `WritePlan` and per-operation plan model.
+- A durable plan/operation ledger with explicit state transitions.
+- Stable endpoint binding for edge creation.
+- Atomic execution adapters for supported operation kinds.
+- Reconciliation for ambiguous outcomes.
+- Explicit partial workflow semantics.
+- Capability-gated property CAS and isolated vertex deletion.
+- Typed timeout and query-budget configuration.
+- Public confirm, status, and reconcile tools.
+- Compatibility migration from the current nonce/hash API.
+- Unit, fault-injection, concurrency, migration, and Docker integration tests.
+
+### 1.4 Out of scope
+
+- Pretending that a client mutex provides database atomicity.
+- Implementing property CAS only in the MCP process.
+- Claiming that a post-response byte check limits network or process memory.
+- Automatic compensation for arbitrary graph workflows.
+- Enabling writes across multiple MCP replicas while using independent SQLite files.
+
+## 2. First-principles model
+
+### 2.1 Authorization is an immutable server-side fact
+
+The user approves a server-persisted plan identified by `plan_id`. The confirm request contains only `plan_id`. Target IDs, endpoint IDs, expected state, desired state, operation order, graph target, principal, and expiry are loaded from the plan store.
+
+Hashes remain internal integrity fields. Client-supplied payloads never become the execution source after confirmation.
+
+### 2.2 Atomicity belongs at the database boundary
+
+Every operation has the form:
+
+```text
+for stable target T
+if current state matches expected state E
+atomically produce desired state D
+```
+
+An operation is executable only when HugeGraph can enforce its precondition and mutation in one transaction or one documented atomic server primitive. Unsupported operations remain preview-only and return `FEATURE_DISABLED` before consuming confirmation.
+
+### 2.3 Outcome is based on evidence
+
+Transport failure does not prove database failure. The system records what it knows:
+
+- `APPLIED`: commit and desired state are proven.
+- `ALREADY_APPLIED`: desired state was already present for the same operation identity.
+- `REJECTED`: the backend proves no mutation occurred.
+- `CONFLICT`: the approved precondition no longer holds.
+- `PARTIAL`: at least one workflow operation is applied and at least one is not final.
+- `UNKNOWN`: the request may have committed or execution was interrupted at an ambiguous point.
+
+`UNKNOWN` and `PARTIAL` are never automatically retried.
+
+### 2.4 Stable identity is operation-specific
+
+- Vertex: graph target + vertex backend ID + label.
+- Edge: graph target + edge backend ID, when available.
+- New edge: source backend ID + edge label + target backend ID + sort-key values. A schema that permits indistinguishable duplicate edges additionally requires a caller-supplied idempotency key.
+- Schema object: schema kind + name.
+- Property replacement: target type + backend ID + operation ID.
+
+Backend ID represents logical identity. Reuse of the same custom ID is treated as the same logical entity. Deployments requiring instance-level ABA protection must add a version property and include it in `expected_state`.
+
+## 3. System architecture
+
+```mermaid
+flowchart LR
+ T[Typed MCP tool] --> P[Planner]
+ P --> R[Graph and schema reader]
+ R --> P
+ P --> S[(Plan Store)]
+ S --> C[confirm_write plan_id]
+ C --> X[Write Executor]
+ X --> A[HugeGraph atomic adapter]
+ A --> L[(Operation Ledger)]
+ L --> Q[get_write_status]
+ L --> K[Reconciler]
+ K --> R
+ K --> L
+```
+
+### 3.1 Planner
+
+The planner validates input and schema, resolves stable identities, calculates expected and desired states, resolves dependencies, and persists the final canonical plan. It performs no writes.
+
+### 3.2 Plan store
+
+The store owns immutable plans and mutable execution state. SQLite is the supported single-instance implementation. Multi-replica write deployments require a shared transactional implementation before writes are enabled.
+
+### 3.3 Executor
+
+The executor is the only MCP component permitted to call write adapters. It loads the exact persisted plan, atomically transitions the operation state, calls one adapter operation, and persists a receipt.
+
+### 3.4 Reconciler
+
+The reconciler reads the current database state for `UNKNOWN` or `PARTIAL` operations and derives a new evidence-backed state. It never infers success from an exception class.
+
+## 4. Data model
+
+### 4.1 Write plan
+
+```python
+@dataclass(frozen=True)
+class WritePlan:
+ plan_id: str
+ tool_name: str
+ graph_target: GraphTarget
+ principal: str
+ operations: tuple[OperationPlan, ...]
+ payload_digest: str
+ schema_fingerprint: str | None
+ status: PlanStatus
+ created_at: int
+ expires_at: int
+```
+
+```python
+@dataclass(frozen=True)
+class OperationPlan:
+ operation_id: str
+ kind: OperationKind
+ target: dict[str, Any]
+ expected_state: dict[str, Any]
+ desired_state: dict[str, Any]
+ depends_on: tuple[str, ...]
+ idempotency_key: str | None
+```
+
+### 4.2 Receipt
+
+```python
+@dataclass(frozen=True)
+class ApplyReceipt:
+ plan_id: str
+ operation_id: str
+ status: ApplyStatus
+ observed_state: dict[str, Any] | None
+ reason_code: str | None
+ attempt: int
+ reconciliation_required: bool
+ committed_at: int | None
+```
+
+### 4.3 SQLite schema
+
+```sql
+CREATE TABLE write_plans (
+ plan_id TEXT PRIMARY KEY,
+ tool_name TEXT NOT NULL,
+ graph_target_json TEXT NOT NULL,
+ principal TEXT NOT NULL,
+ payload_json TEXT NOT NULL,
+ payload_digest TEXT NOT NULL,
+ schema_fingerprint TEXT,
+ status TEXT NOT NULL,
+ created_at INTEGER NOT NULL,
+ expires_at INTEGER NOT NULL,
+ confirmed_at INTEGER
+);
+
+CREATE TABLE write_operations (
+ operation_id TEXT PRIMARY KEY,
+ plan_id TEXT NOT NULL,
+ ordinal INTEGER NOT NULL,
+ kind TEXT NOT NULL,
+ payload_json TEXT NOT NULL,
+ status TEXT NOT NULL,
+ attempt INTEGER NOT NULL DEFAULT 0,
+ receipt_json TEXT,
+ updated_at INTEGER NOT NULL,
+ UNIQUE(plan_id, ordinal),
+ FOREIGN KEY(plan_id) REFERENCES write_plans(plan_id)
+);
+```
+
+The store has an explicit schema version and additive migrations. Legacy consumed confirmations without an operation receipt migrate to `LEGACY_UNKNOWN`, never to `REJECTED` or `APPLIED`.
+
+## 5. State machines
+
+### 5.1 Operation state
+
+```mermaid
+stateDiagram-v2
+ [*] --> ISSUED
+ ISSUED --> EXECUTING: confirm
+ EXECUTING --> APPLIED
+ EXECUTING --> ALREADY_APPLIED
+ EXECUTING --> REJECTED
+ EXECUTING --> CONFLICT
+ EXECUTING --> UNKNOWN
+ UNKNOWN --> APPLIED: reconcile desired
+ UNKNOWN --> RETRYABLE_NOT_APPLIED: expected and request proven undispatched
+ UNKNOWN --> CONFLICT: reconcile third state
+ RETRYABLE_NOT_APPLIED --> EXECUTING: explicit resume
+```
+
+The transition from `ISSUED` to `EXECUTING` and attempt increment occur in one plan-store transaction. A process crash after this transition is exposed as `UNKNOWN` on status read.
+
+### 5.2 Workflow state
+
+```text
+all operations APPLIED/ALREADY_APPLIED -> APPLIED
+some final success + some non-final/failure -> PARTIAL
+no side effect and deterministic refusal -> REJECTED or CONFLICT
+any ambiguous operation and no known partial success -> UNKNOWN
+```
+
+`PARTIAL` includes `written`, `not_applied`, and `unknown` operation sets. It is never collapsed into `REJECTED`.
+
+## 6. Operation design
+
+### 6.1 Create vertex
+
+Planner:
+
+- validate the label and property map;
+- require a stable explicit/custom ID or deterministic primary-key identity;
+- persist `expected_state={exists: false}` and the complete desired vertex.
+
+Executor:
+
+- create one vertex by stable identity;
+- treat an identical existing vertex as `ALREADY_APPLIED`;
+- treat an existing different vertex as `CONFLICT`.
+
+If the backend cannot atomically create-if-absent for the identity, concurrent correctness remains capability-gated.
+
+### 6.2 Create edge
+
+Planner resolves each endpoint to one of:
+
+- an existing backend vertex ID; or
+- a dependency reference to an earlier `CREATE_VERTEX` operation.
+
+The executor replaces dependency references with the persisted vertex receipt ID. The final write request uses only:
+
+```groovy
+g.V(sourceId).as('s').V(targetId).addE(label).from('s')
+```
+
+No property predicate is evaluated during the write. Before execution, both IDs must exist with the planned labels. Edge identity includes sort keys or an explicit idempotency key.
+
+### 6.3 Delete edge
+
+Planner resolves and persists the edge ID. Executor deletes by edge ID. Missing ID reconciles to `ALREADY_APPLIED`; an incompatible target resolves to `CONFLICT`.
+
+### 6.4 Delete isolated vertex
+
+The required atomic primitive is:
+
+```text
+delete_vertex_if_no_incident_edges(vertex_id, label)
+```
+
+The existing single Gremlin traversal may be used only after concurrency tests prove the required behavior for every supported HugeGraph backend. Otherwise the operation remains disabled until HugeGraph exposes a narrow server API.
+
+### 6.5 Replace properties
+
+Append/eliminate commands are compiled into complete state replacement:
+
+```text
+expected properties -> desired properties
+```
+
+The adapter contract is:
+
+```python
+replace_properties_if_match(
+ target_type,
+ target_id,
+ expected_properties,
+ desired_properties,
+ operation_id,
+) -> ApplyReceipt
+```
+
+This requires a backend-enforced CAS or equivalent transaction with an element/version lock. The public mutation tool stays preview-only until the capability is available.
+
+### 6.6 Schema create
+
+One plan contains one schema create. The executor uses the configured write timeout and treats outcomes as follows:
+
+- identical object exists: `ALREADY_APPLIED`;
+- different object with the same name: `CONFLICT`;
+- create succeeds and post-read matches: `APPLIED`;
+- manager construction, transport, response parsing, or post-read is ambiguous: `UNKNOWN` with a persisted receipt.
+
+The schema-manager construction is inside the same exception boundary as the request.
+
+### 6.7 Graph import workflow
+
+Graph import is a durable dependency graph:
+
+```text
+CREATE_VERTEX operations -> CREATE_EDGE operations
+```
+
+Each operation has its own receipt. Vertices persist their resulting backend IDs. Edges consume those IDs. A failure produces `PARTIAL` when any earlier operation is applied. Resume first reconciles every non-final operation and then executes only proven-not-applied operations.
+
+If a future HugeGraph API provides a true batch transaction, the adapter may execute the same immutable operation set transactionally without changing the public plan contract.
+
+## 7. API design
+
+### 7.1 Dry run
+
+```json
+{
+ "ok": true,
+ "data": {
+ "plan_id": "wp_...",
+ "status": "ISSUED",
+ "expires_at": 1788500000,
+ "preview": {},
+ "confirmable": true
+ }
+}
+```
+
+Preview-only operations return `confirmable=false` and no confirm token.
+
+### 7.2 Confirm
+
+```text
+confirm_write_tool(plan_id)
+```
+
+The tool atomically claims the plan and returns its current receipt. Repeated confirm calls return the persisted current state; they do not create a second attempt.
+
+### 7.3 Status
+
+```text
+get_write_status_tool(plan_id)
+```
+
+Returns the plan status and ordered operation receipts without exposing the stored mutation payload.
+
+### 7.4 Reconcile
+
+```text
+reconcile_write_tool(plan_id)
+```
+
+Only `UNKNOWN`, `PARTIAL`, and legacy unknown plans are eligible. Reconciliation is read-only and idempotent. Observing the expected state does not authorize retry after an operation was dispatched: the old request may still commit later. `RETRYABLE_NOT_APPLIED` is available only when the durable operation record proves that no adapter attempt was claimed, or when a backend-enforced idempotency/fencing primitive proves replay safety.
+
+### 7.5 Compatibility
+
+For one release, existing tool parameters `plan_hash`, `nonce`, and `expires_at` are accepted only as locators and integrity checks. Execution always loads the server plan. Responses include a deprecation warning and `plan_id`.
+
+## 8. Error and status mapping
+
+| Condition | Error type | Durable status | Retryable |
+|---|---|---|---|
+| Validation before confirmation | `VALIDATION_ERROR` | none | false |
+| Expected state changed | `WRITE_CONFLICT` | `CONFLICT` | false |
+| Backend proves no write | operation-specific error | `REJECTED` | false |
+| Request may have committed | `WRITE_OUTCOME_UNKNOWN` | `UNKNOWN` | false |
+| Some operations committed | `PARTIAL_APPLY` | `PARTIAL` | false |
+| Feature lacks atomic primitive | `FEATURE_DISABLED` | none | false |
+
+No error emitted after a possible write recommends replaying the public tool.
+
+## 9. Configuration and resource safety
+
+### 9.1 Typed configuration
+
+Configuration is parsed through field specifications with type, default, minimum, and maximum. Integer fields use integer comparisons and never pass arbitrary-precision values through `math.isfinite`. Float fields reject non-finite values and catch overflow.
+
+Read and write adapters explicitly choose `read_timeout_seconds` or `write_timeout_seconds`. Schema create uses the write timeout.
+
+### 9.2 Raw Gremlin
+
+Default Agent access uses typed query tools. Raw execution remains disabled by default.
+
+Enabling raw execution requires all of:
+
+- explicit admin/debug feature flag;
+- a distinct read-only HugeGraph principal;
+- a server-side evaluation timeout;
+- a server-side result-count cap or enforced traversal cap;
+- an HTTP streaming response reader that aborts after the configured byte limit.
+
+The current post-materialization count/byte check remains a final output guard. It is not advertised as a hard execution or memory budget.
+
+### 9.3 Upstream envelope normalization
+
+Nested Thin API envelopes are unwrapped iteratively with a small fixed depth limit. Excess depth or malformed shapes return a structured upstream-response error.
+
+## 10. Failure recovery
+
+### 10.1 Reconciliation rules
+
+- Create vertex: desired exact state -> `APPLIED`; absent and never dispatched -> `RETRYABLE_NOT_APPLIED`; absent after dispatch -> `UNKNOWN`; other state -> `CONFLICT`.
+- Create edge: stable edge identity present -> `APPLIED`; absent and never dispatched -> `RETRYABLE_NOT_APPLIED`; absent after dispatch -> `UNKNOWN`; ambiguous duplicate identity -> `CONFLICT`.
+- Delete: stable ID absent -> `APPLIED`; present and never dispatched -> `RETRYABLE_NOT_APPLIED`; present after dispatch -> `UNKNOWN` unless backend replay safety is proven; incompatible state -> `CONFLICT`.
+- Replace properties: current equals desired -> `APPLIED`; current equals expected and never dispatched -> `RETRYABLE_NOT_APPLIED`; current equals expected after dispatch -> `UNKNOWN`; otherwise -> `CONFLICT`.
+- Schema create: identical object -> `APPLIED`; missing and never dispatched -> `RETRYABLE_NOT_APPLIED`; missing after dispatch -> `UNKNOWN`; same name with different fields -> `CONFLICT`.
+
+### 10.2 Crash points
+
+The design explicitly tests crashes:
+
+1. before plan claim;
+2. after `EXECUTING` persistence and before adapter call;
+3. after request send and before response;
+4. after response and before receipt persistence;
+5. after receipt persistence and before response to the caller.
+
+Each restart produces a queryable durable state. Only reconciliation can authorize another attempt.
+
+## 11. Testing strategy
+
+### 11.1 Unit and contract tests
+
+- Plan canonicalization and immutable payload loading.
+- Legal and illegal state transitions.
+- SQLite schema migration and corrupted-row fail-closed behavior.
+- Partial workflow aggregation.
+- Huge integer and non-finite configuration values.
+- FLOAT/DOUBLE overflow input.
+- Bounded nested-envelope normalization.
+- Correct read/write timeout selection.
+- Public response and deprecation contracts.
+
+### 11.2 Concurrency tests
+
+- Predicate expands after edge planning; exactly one edge is created between bound endpoint IDs.
+- Two property replacements share one expected version; at most one applies.
+- An edge is added while isolated-vertex deletion executes; either the edge and vertex survive with conflict, or deletion wins before edge creation. No dangling or silently cascaded state is allowed.
+- Two confirmers claim the same operation; one adapter attempt occurs.
+
+Tests use barriers or backend fault hooks, not sleeps.
+
+### 11.3 Fault injection
+
+Inject failure at every crash point and assert the persisted operation state, public error, and reconciliation result.
+
+### 11.4 Docker integration
+
+Run against HugeGraph 1.7.0 for every supported backend configuration. The suite verifies database state and ledger state together, including `get_write_status_tool` after partial and unknown outcomes.
+
+## 12. Key decisions and trade-offs
+
+### Decision A: Keep a multi-operation plan, execute it as a durable workflow
+
+Reason: imports naturally contain dependent vertices and edges. A per-operation ledger makes partial progress explicit and recoverable.
+
+Trade-off: more persistence and reconciliation logic than rejecting every batch. The public semantics become honest and deterministic.
+
+### Decision B: Bind edge endpoints to IDs before the edge write
+
+Reason: a uniqueness check followed by predicate-based creation cannot prevent concurrent fan-out.
+
+Trade-off: newly created vertices must return and persist their IDs before dependent edges execute.
+
+### Decision C: Capability-gate unsupported atomic writes
+
+Reason: user confirmation and process-local locking cannot substitute for backend CAS or transactional conditional deletion.
+
+Trade-off: property mutation remains unavailable until HugeGraph supplies the required primitive.
+
+### Decision D: Separate `PARTIAL` from `UNKNOWN` and `REJECTED`
+
+Reason: recovery behavior depends on whether any side effect is already proven.
+
+Trade-off: callers must handle one additional durable state.
+
+### Decision E: Make SQLite a declared single-instance implementation
+
+Reason: independent SQLite files cannot enforce one-shot confirmation across replicas.
+
+Trade-off: multi-replica write deployments need a shared PlanStore implementation.
+
+## 13. Risks and mitigations
+
+- Backend transaction semantics differ by storage backend. Mitigation: capability matrix and backend-specific Docker concurrency tests before enabling each atomic adapter.
+- Legacy callers may assume retryable transport errors. Mitigation: compatibility response warnings and explicit status/reconcile guidance.
+- Ledger growth may become unbounded. Mitigation: retain final receipts for a configured audit period; never delete active `UNKNOWN` or `PARTIAL` records automatically.
+- Raw Gremlin can exhaust server or client resources. Mitigation: remain disabled until all hard-budget capabilities are present.
+- Documentation can diverge from behavior. Mitigation: contract tests assert tool count, arguments, status enums, defaults, and generated README tables where practical.
+
+## 14. Acceptance criteria
+
+The design is complete when all of the following are demonstrated:
+
+1. Edge creation never re-evaluates endpoint predicates during a write.
+2. Every confirmed write has a durable, queryable operation record before the adapter call.
+3. Partial workflows are returned and stored as `PARTIAL_APPLY`/`PARTIAL`.
+4. No ambiguous result is directly retryable.
+5. Property mutation stays disabled until backend CAS passes concurrency tests.
+6. Schema manager creation, request, response, and verification share one UNKNOWN boundary and use write timeout.
+7. Raw Gremlin is disabled unless hard server and client budgets are available.
+8. Documentation and public contract tests match the runtime behavior.
+9. Unit, migration, fault-injection, and Docker concurrency suites pass.
diff --git a/.workflow/pr73-write-safety/logs/task-1.1-current-state-inventory.md b/.workflow/pr73-write-safety/logs/task-1.1-current-state-inventory.md
new file mode 100644
index 00000000..8c8cd5b6
--- /dev/null
+++ b/.workflow/pr73-write-safety/logs/task-1.1-current-state-inventory.md
@@ -0,0 +1,58 @@
+# Task 1.1 Log: Current-State Inventory
+
+## Scope
+
+Mapped the current uncommitted HugeGraph MCP, HugeGraph Python client, and Thin API changes to requirements R1–R12. This was a read-only implementation inventory; no production or test behavior changed in this task.
+
+The DeepWiki cache establishes the HugeGraph AI monorepo and client/operator boundaries but does not cover the PR #73 MCP implementation. Local source, the approved requirements/design, existing tests, and Docker verification are authoritative for this inventory.
+
+## Requirement mapping
+
+| Requirement | Current state | Reusable implementation | Conflict or missing work |
+|---|---|---|---|
+| R1 Immutable authorization | Partial | `ConfirmationStore.issue()` persists an optional server-side payload; graph confirm loads the persisted compiled plan and atomic consume returns the stored payload. | Public identity is still `plan_hash`/nonce. There is no canonical `WritePlan`, random `plan_id`, `write_plans` table, or confirm-by-plan-ID entry point. Schema and legacy import issue plans without canonical operation payloads. |
+| R2 Stable graph identity | Partial | Delete dry-run resolves one backend ID with a bounded query; delete execution and verification use `target_id`. Future create-edge endpoints are no longer counted during dry-run. | `_create_edge_query()` still re-evaluates source/target property predicates after separate uniqueness checks. Existing endpoint IDs and dependency operation IDs are not persisted. Create receipts do not persist returned vertex IDs. |
+| R3 Atomic conditional delete | Provisional | `cascade=false` uses one `g.V(id).not(bothE()).drop()` request, and Docker HugeGraph 1.7 tests show the vertex and newly added edge survive when the precondition fails. | The test does not prove simultaneous transaction isolation with a barrier and does not cover every supported backend. Capability gating is absent. |
+| R4 Backend property CAS | Safe but incomplete | Property mutation retains schema-aware preview and shared validation, reports `confirmable=false`, and returns `FEATURE_DISABLED` without consuming a confirmation or writing. | No server/Core CAS primitive, pyhugegraph contract, executor adapter, or two-client concurrency proof exists. |
+| R5 Durable outcome model | Partial | `write_operations` persists `EXECUTING`, final status, receipt JSON, and the consumed payload. Crash-left `EXECUTING` is exposed as `UNKNOWN`. `ApplyStatus` and `ApplyReceipt` provide a small shared base. | The table is plan-hash keyed and has no operation ID/ordinal. `ApplyStatus` lacks `PARTIAL`. Tools still aggregate ad-hoc strings. A workflow with prior writes and a later deterministic failure is incorrectly persisted as `REJECTED`. |
+| R6 Reconciliation | Missing | `get_write_status` reads the durable record and redacts the stored plan. | There is no `reconcile_write_tool`, expected/desired comparison engine, resume authorization, or per-kind reconciliation adapter. |
+| R7 Schema safety | Partial | Dry-run requires exactly one schema create. Ambiguous request/post-read failures become `UNKNOWN` receipts. | `_schema_manager()` is created outside the protected execution boundary and uses the read timeout instead of `write_timeout_seconds`. Identical-existing and conflicting-existing reconciliation are not modeled canonically. |
+| R8 Durable import workflow | Missing | The import planner validates ordering and the ledger can retain one top-level outcome. Legacy ambiguous HTTP results now return `WRITE_OUTCOME_UNKNOWN`. | Import remains a sequential batch without per-operation dependencies, IDs, receipts, resume, or reconciliation. Partial progress is summarized rather than durably represented. The legacy AI write path remains separate. |
+| R9 Query resource boundary | Safe default, incomplete capability | Public raw execution and generated-query execution require admin mode. Read/write transport timeouts are split. Post-materialization item and byte checks prevent oversized data from entering the MCP envelope. | There is no separate read-only principal, server evaluation timeout, server-side cap, or streaming byte abort. Current checks cannot bound database work, network transfer, parse memory, or serialization CPU. |
+| R10 Typed configuration | Partial | Config is immutable and contains separate connect/read/write/AI timeouts and output-guard limits. Floats reject non-finite values. | `_parse_int()` calls `math.isfinite()` on arbitrary-precision integers and can raise `OverflowError`; FLOAT/DOUBLE property validation has the same issue for very large JSON integers. Field-specific maximum bounds are absent. Schema writes do not consume the write timeout. |
+| R11 Stable public contracts | Partial | Invalid toolsets fail closed to `v1`; `get_write_status_tool` exists; nested AI errors and Thin API error logging are normalized. | Confirm/status still use plan hash rather than plan ID, reconcile is absent, mutation docstrings describe obsolete confirmation behavior, and README/checklist content conflicts with the runtime tool count, admin gate, schema single-operation rule, and preview-only mutation. |
+| R12 Verifiable correctness | Partial | Current evidence: MCP unit suite passed with external tests skipped; Python client and Thin API focused suites passed; Docker HugeGraph 1.7 real-write suite passed 15 tests. | Missing barrier-controlled edge-create race, true simultaneous isolated-delete race, multi-confirmer adapter-count test, migration matrix, crash/fault injection, reconciliation tests, per-operation ledger assertions, and supported-backend matrix. |
+
+## Reusable code by module
+
+- `plan_hash.py`: retain as an internal canonical integrity digest during compatibility migration.
+- `confirmation_store.py`: reuse permission handling, SQLite transaction setup, nonce digesting, and additive migration approach; evolve into a versioned PlanStore.
+- `confirmable_workflow.py`: reuse error-envelope normalization and atomic-consume boundary; replace tool-specific confirm helpers with canonical plan claim/status/reconcile services.
+- `write_plan.py`: reuse the enum/receipt seed, then extend it with `WritePlan`, `OperationPlan`, IDs, `PARTIAL`, transition validation, and canonical serialization.
+- `graph_data_execute.py` and `graph_data_gremlin.py`: retain delete target compilation and by-ID deletion; replace predicate-based edge writes and top-level sequential status aggregation.
+- `property_validation.py`: retain the shared schema/type/cardinality implementation after making numeric checks total and exception-free.
+- `mutate_graph_properties.py`: retain the preview-only fail-closed contract until a backend CAS capability is proven.
+- `gremlin_tools.py` and `hugegraph_client.py`: retain split transport timeout construction and post-materialization output guards; do not describe them as hard server/client resource limits.
+- `hugegraph_ai_client.py` and Thin API logging changes: retain normalized upstream errors and secret-safe error logging after bounding nested-envelope unwrapping.
+- Docker integration tests: retain unique-schema isolation, stable-ID delete coverage, schema single-operation coverage, and no-write mutation assertions.
+
+## Code that conflicts with the approved design
+
+1. `plan_hash` is simultaneously public identity, integrity proof, and operation-ledger key.
+2. Confirmation tables and `write_operations` represent one top-level attempt rather than one immutable plan with ordered operations.
+3. Edge creation separates endpoint uniqueness checks from a predicate-based write.
+4. Import and schema paths own status mapping and recovery logic instead of delegating to one executor/reconciler.
+5. `REJECTED` is used for known partial workflows.
+6. Raw-query byte checks occur after full response materialization.
+7. Public documentation and docstrings describe contracts that the current implementation deliberately disabled or changed.
+
+## Verification
+
+- Confirmed `requirements.md`, approved `design.md`, and `tasks.md` exist.
+- Inspected current working-tree status, changed-file inventory, and relevant production/test symbols.
+- Confirmed the current branch contains no `confirm_write` or `reconcile_write` implementation.
+- Confirmed `git diff --check -- .workflow/pr73-write-safety` passes after updating this task log and checklist state.
+
+## Result
+
+Task 1.1 is complete. The next task is 1.2, which must verify actual HugeGraph 1.7 client/server capabilities before any adapter or capability-gating implementation is selected.
diff --git a/.workflow/pr73-write-safety/logs/task-1.2-backend-capabilities.md b/.workflow/pr73-write-safety/logs/task-1.2-backend-capabilities.md
new file mode 100644
index 00000000..dac0dbc4
--- /dev/null
+++ b/.workflow/pr73-write-safety/logs/task-1.2-backend-capabilities.md
@@ -0,0 +1,60 @@
+# Task 1.2 Log: HugeGraph 1.7 Backend Capabilities
+
+## Scope
+
+Inspected HugeGraph 1.7 server configuration from `hugegraph/hugegraph:1.7.0`, the local pyhugegraph request and graph APIs, the existing Docker integration tests, and official HugeGraph 1.7 authentication/configuration documentation. Encoded only evidence-backed conclusions; unknown version/backend combinations fail closed.
+
+## Evidence
+
+- Docker `conf/gremlin-server.yaml` configures `evaluationTimeout: 30000`.
+- HugeGraph 1.7 documents the REST-to-Gremlin wait timeout as `gremlinserver.timeout`.
+- HugeGraph 1.7 documents `task.result_size_limit`; this applies to task/job results, not general `/gremlin` responses.
+- Docker graph config contains `memory.one_query_max_capacity`, but it is commented out by default and its Gremlin/backend behavior was not proven.
+- `GraphManager` exposes unconditional vertex/edge create and ID-based update/delete APIs. It has no expected-state CAS or graph-data create-if-absent parameter.
+- Schema `ifNotExist()` does not establish a graph-data conditional-create capability.
+- Existing HugeGraph 1.7 Docker tests prove edge IDs can be returned and addressed by ID. They do not prove duplicate-edge uniqueness or idempotency.
+- The current pyhugegraph request layer uses `requests` without `stream=True` and materializes JSON/text before returning it.
+- HugeGraph 1.7 StandardAuthenticator supports users, groups, operation permissions, and resource scopes. The default Docker image does not enable authentication.
+
+## Encoded profile
+
+Added `hugegraph_mcp.backend_capabilities` with:
+
+- `BackendFeature`
+- `SupportStatus`
+- immutable `CapabilityEvidence`
+- immutable, complete `BackendProfile`
+- exact-match `profile_for(server_version, backend)` lookup
+
+Only `VERIFIED_SUPPORTED` enables `supports()`. Both `VERIFIED_UNSUPPORTED` and `UNKNOWN` fail closed.
+
+### HugeGraph 1.7.0 + RocksDB
+
+| Feature | Status | Reason |
+|---|---|---|
+| Vertex create-if-absent | `VERIFIED_UNSUPPORTED` | Local graph client exposes unconditional POST only. |
+| Edge create-if-absent | `VERIFIED_UNSUPPORTED` | Local graph client exposes unconditional POST only. |
+| Edge ID stable addressing | `VERIFIED_SUPPORTED` | Docker integration proves returned edge ID round-trip and ID-based access. |
+| Edge idempotent identity | `UNKNOWN` | Duplicate-edge uniqueness is not proven. |
+| Isolated vertex delete | `UNKNOWN` | Ordered scenario passes; simultaneous transaction isolation is not proven. |
+| Property compare-and-set | `VERIFIED_UNSUPPORTED` | Append/eliminate APIs have no expected-state field. |
+| Gremlin evaluation timeout | `VERIFIED_SUPPORTED` | Docker server config sets 30 seconds. |
+| REST Gremlin wait timeout | `VERIFIED_SUPPORTED` | HugeGraph 1.7 server configuration exposes it. |
+| Task result-size limit | `VERIFIED_SUPPORTED` | Applies to task/job results only. |
+| Query memory limit | `UNKNOWN` | Config exists but is disabled by default and unverified. |
+| General Gremlin result-item limit | `UNKNOWN` | No general `/gremlin` cap was verified. |
+| HTTP streaming response limit | `VERIFIED_UNSUPPORTED` | Current client fully materializes responses. |
+| Read-only principal | `VERIFIED_SUPPORTED` | Server auth model supports resource/operation permissions; configuration is required. |
+
+No capability is inferred for HStore or future server versions. Runtime code must not assume the configured backend from the server version or Docker default.
+
+## Verification
+
+- `test_backend_capabilities.py`: 17 passed.
+- Ruff check passed for the new module and tests.
+- Ruff format check passed after applying repository formatting.
+- The test suite covers profile completeness, expected evidence states, immutable records, unknown-profile behavior, and fail-closed `supports()` semantics.
+
+## Result
+
+Task 1.2 is complete. Task 1.3 can now define the legacy hash/nonce compatibility contract and the new plan-ID contract without assuming unavailable HugeGraph primitives.
diff --git a/.workflow/pr73-write-safety/logs/task-1.3-confirmation-contract.md b/.workflow/pr73-write-safety/logs/task-1.3-confirmation-contract.md
new file mode 100644
index 00000000..40f3831a
--- /dev/null
+++ b/.workflow/pr73-write-safety/logs/task-1.3-confirmation-contract.md
@@ -0,0 +1,63 @@
+# Task 1.3 Log: Confirmation Compatibility Contract
+
+## Scope
+
+Defined a machine-readable contract and executable contract tests before changing public write tools. The contract separates the canonical plan-ID protocol from the one-release legacy hash/nonce protocol.
+
+## Decisions
+
+### Canonical protocol
+
+- `confirm_write_tool` accepts only `plan_id`.
+- The authoritative execution payload is the immutable server-persisted plan.
+- Client-resubmitted targets, operations, properties, hashes, or nonces are not authoritative.
+- Repeated confirmation returns persisted status and does not create a second attempt.
+- `get_write_status_tool` and `reconcile_write_tool` use `plan_id`.
+
+### Legacy compatibility protocol
+
+- `plan_hash`, `nonce`, and `expires_at` remain accepted for one release.
+- The three fields form one all-or-nothing locator group.
+- They locate and integrity-check a server-persisted plan; they never authorize a client payload.
+- Legacy responses carry warning code `LEGACY_CONFIRMATION_DEPRECATED`.
+- Canonical and legacy fields are mutually exclusive. Mixed requests are rejected rather than assigned implicit precedence.
+
+### Outcome contract
+
+The public vocabulary is:
+
+```text
+APPLIED
+ALREADY_APPLIED
+REJECTED
+CONFLICT
+PARTIAL
+UNKNOWN
+```
+
+No public outcome is directly retryable. A later reconcile result may prove that an operation was not applied and permit an explicit resume transition.
+
+Error mapping is fixed as follows:
+
+- ambiguous outcome -> `WRITE_OUTCOME_UNKNOWN`
+- changed precondition -> `WRITE_CONFLICT`
+- known partial workflow -> `PARTIAL_APPLY`
+- missing atomic primitive -> `FEATURE_DISABLED`
+
+## Artifacts
+
+- `hugegraph-mcp/tests/contracts/write_confirmation_v2.json`
+- `hugegraph-mcp/tests/test_write_confirmation_contract.py`
+
+The contract tests also inspect the four existing public write-tool signatures and require them to retain the complete legacy locator group during the compatibility window.
+
+## Verification
+
+- Contract tests: 5 passed.
+- Ruff check passed for the new test.
+- Ruff format check passed.
+- JSON contract loads through the test suite and contains disjoint canonical/legacy field sets.
+
+## Result
+
+Task 1.3 and the complete research group 1 are finished. The next approved task is 2.1: write failing tests for the canonical immutable plan/operation/receipt model and complete state-transition matrix.
diff --git a/.workflow/pr73-write-safety/logs/task-10-final-verification.md b/.workflow/pr73-write-safety/logs/task-10-final-verification.md
new file mode 100644
index 00000000..b574b5e8
--- /dev/null
+++ b/.workflow/pr73-write-safety/logs/task-10-final-verification.md
@@ -0,0 +1,46 @@
+# Task 10 Log: Final Verification
+
+## Automated suites
+
+- HugeGraph MCP: 784 passed, 17 skipped.
+- HugeGraph Python client: 71 passed, 70 skipped.
+- Thin API: 15 passed with 5 dependency deprecation warnings.
+- SQLite schema/migration suite in a fresh process: 13 passed.
+- Executor fault/fencing suite in a separate fresh process: 8 passed.
+
+Skipped MCP/client cases are explicitly capability- or external-service-gated. Property CAS concurrency remains skipped because HugeGraph 1.7 does not expose the required backend primitive.
+
+## Docker HugeGraph 1.7
+
+The final real-server suite validates:
+
+- canonical single-operation schema dry-run, `plan_id` confirmation, durable receipt, and status;
+- edge endpoint ID binding and exact DELETE_EDGE canonical/legacy flows;
+- import and mutation preview-only behavior with zero writes;
+- isolated vertex deletion fail-closed behavior;
+- graph and ledger state assertions.
+
+Result: 16 passed and one nondeterministic isolation probe. The probe has previously captured a real non-isolated result with `addE().count() == 1` followed by the source vertex and edge both disappearing. A later run can XPASS because the race is schedule-dependent; XPASS does not promote the capability. `ISOLATED_VERTEX_DELETE` remains UNKNOWN and disabled.
+
+All temporary verification containers were removed.
+
+## Static checks
+
+- Ruff check passed for every changed Python path using the applicable module configuration.
+- Ruff format check passed for every changed Python path.
+- `git diff --check` passed.
+- Public documentation contract tests passed and the documented checkout launch command was executed successfully.
+
+## Independent review
+
+Three final review lanes covered core state/SQLite, graph/schema adapters and Docker evidence, and public/config/documentation contracts. Findings were corrected and re-reviewed.
+
+The final core reproduction confirms:
+
+- a previously dispatched request observed at expected state remains UNKNOWN;
+- resume is rejected;
+- the old request is the only possible database effect;
+- stale receipts cannot overwrite a newer attempt;
+- unclaimed operations remain safely resumable after reconciliation.
+
+No reviewer reported a remaining blocker or important issue after the final fixes.
diff --git a/.workflow/pr73-write-safety/logs/task-2.1-write-plan-tdd.md b/.workflow/pr73-write-safety/logs/task-2.1-write-plan-tdd.md
new file mode 100644
index 00000000..4c35b3a2
--- /dev/null
+++ b/.workflow/pr73-write-safety/logs/task-2.1-write-plan-tdd.md
@@ -0,0 +1,50 @@
+# Task 2.1 Log: Canonical Write Model TDD
+
+## Scope
+
+Replaced the initial receipt-only test with the red-phase contract for the approved canonical write model. Production code was intentionally not changed in this task.
+
+## Test contract
+
+The tests require:
+
+- frozen `GraphTarget`, `OperationPlan`, `WritePlan`, and `ApplyReceipt` models;
+- deep immutability for nested target, expected-state, and desired-state mappings;
+- ordered operation storage;
+- unique operation IDs within a plan;
+- dependencies that reference only earlier operations;
+- deterministic canonical JSON and digest independent of mapping insertion order;
+- digest changes whenever authorized desired state changes;
+- receipts with stable plan ID, operation ID, attempt, reason code, commit time, observed state, and reconciliation flag;
+- separate `ApplyStatus` and `PlanStatus` vocabularies;
+- an explicit, complete, fail-closed plan transition matrix;
+- plan status aggregation that preserves `PARTIAL` whenever known successful operations coexist with rejected, conflicting, or unknown operations.
+
+## State decisions locked by tests
+
+```text
+ISSUED -> EXECUTING | EXPIRED
+EXECUTING -> APPLIED | ALREADY_APPLIED | REJECTED | CONFLICT | PARTIAL | UNKNOWN
+UNKNOWN -> APPLIED | CONFLICT | RETRYABLE_NOT_APPLIED
+RETRYABLE_NOT_APPLIED -> EXECUTING
+PARTIAL -> PARTIAL | APPLIED | UNKNOWN
+terminal states -> no transition
+```
+
+Aggregation distinguishes:
+
+- all already applied -> `ALREADY_APPLIED`;
+- applied plus already applied -> `APPLIED`;
+- any known success plus any non-success -> `PARTIAL`;
+- no known success plus unknown -> `UNKNOWN`;
+- proven conflict/rejection/not-applied -> their corresponding status.
+
+## Verification
+
+- `test_write_plan.py` passes Ruff check.
+- `test_write_plan.py` passes Ruff format check.
+- Pytest red phase is confirmed: collection fails because the current production module does not yet export `ALLOWED_PLAN_TRANSITIONS` and the rest of the canonical model API.
+
+## Result
+
+Task 2.1 is complete as a TDD red-phase task. Task 2.2 must implement the specified model and make this suite pass without weakening its immutability or state-transition assertions.
diff --git a/.workflow/pr73-write-safety/logs/task-2.2-canonical-write-model.md b/.workflow/pr73-write-safety/logs/task-2.2-canonical-write-model.md
new file mode 100644
index 00000000..5b43e3bb
--- /dev/null
+++ b/.workflow/pr73-write-safety/logs/task-2.2-canonical-write-model.md
@@ -0,0 +1,47 @@
+# Task 2.2 Log: Canonical Write Model
+
+## Implementation
+
+Implemented the canonical immutable model in `hugegraph_mcp.write_plan`:
+
+- `GraphTarget`
+- `OperationPlan`
+- `WritePlan`
+- `ApplyReceipt`
+- `ApplyStatus`
+- `PlanStatus`
+- `ALLOWED_PLAN_TRANSITIONS`
+- `can_transition()`
+- `aggregate_plan_status()`
+- deterministic canonical JSON and SHA-256 digest
+
+Nested plan mappings are recursively frozen, operations retain their declared order, operation IDs must be unique, and dependencies may reference only earlier operations.
+
+`ApplyReceipt` supports the canonical plan/operation identity fields while temporarily retaining the existing schema-tool construction shape for compatibility. Canonical callers receive plan ID, operation ID, attempt, reason code, reconciliation flag, observed state, and commit time.
+
+## Status integration
+
+Replaced confirmed-write status literals across graph execution, graph orchestration, schema/confirmation persistence, legacy AI import, and mutation preview paths with the shared enums and aggregation function.
+
+Key behavior changes:
+
+- known prior writes plus a later failure aggregate to `PARTIAL`;
+- partial graph and import workflows persist `PARTIAL` and return `PARTIAL_APPLY`;
+- proven zero-write failures use `REJECTED`;
+- ambiguous outcomes remain `UNKNOWN`;
+- crash-left `EXECUTING` is exposed as `UNKNOWN`;
+- mutation preview uses `ISSUED`, while disabled confirmation reports no write and a rejected operation outcome;
+- the SQLite boundary persists enum `.value` strings and validates compatible legacy string inputs before storage.
+
+## Verification
+
+- Canonical model tests: 20 passed.
+- Complete HugeGraph MCP test suite: 642 passed, 15 skipped.
+- Schema/ledger focused tests: 125 passed in the worker lane.
+- Legacy import/mutation focused tests: 89 passed in the worker lane.
+- Graph status focused tests: 85 passed in the worker lane.
+- `git diff --check` passed.
+
+## Result
+
+Task 2.2 is complete. Task 2.3 is the next dependency: add red-phase SQLite migration tests for versioned plan/operation storage, existing databases, corrupt rows, legacy unknown state, and restart persistence.
diff --git a/.workflow/pr73-write-safety/logs/tasks-2.3-2.5-plan-store.md b/.workflow/pr73-write-safety/logs/tasks-2.3-2.5-plan-store.md
new file mode 100644
index 00000000..8a670a89
--- /dev/null
+++ b/.workflow/pr73-write-safety/logs/tasks-2.3-2.5-plan-store.md
@@ -0,0 +1,30 @@
+# Tasks 2.3–2.5 Log: Versioned PlanStore
+
+## Migration tests
+
+Added coverage for a fresh versioned database, legacy issued plans, consumed-only plans, existing receipts, corrupt JSON, and restart idempotency. Legacy consumed state without a trustworthy receipt becomes `LEGACY_UNKNOWN`.
+
+## Store implementation
+
+Added `SQLitePlanStore` backed by `write_plans.sqlite3` with:
+
+- schema version 1;
+- immutable plan payload persistence;
+- ordered per-operation records;
+- canonical plan round-trip;
+- legal compare-and-set plan transitions;
+- operation receipt persistence and aggregate plan status updates;
+- idempotent import from the existing `confirmations.sqlite3` compatibility ledger.
+
+The existing confirmation database remains the one-release legacy protocol source. Canonical plan-ID tools will use the new store; legacy rows are imported into deterministic `wp_legacy_*` and `op_legacy_*` identities.
+
+## Interface and deployment safety
+
+Added the runtime-checkable `PlanStore` protocol and `plan_store_from_config()` factory. SQLite is accepted only when `write_instance_count == 1`. Write capabilities fail closed with `FEATURE_DISABLED` when multiple write-enabled MCP instances are configured without a shared transactional store; reads remain available.
+
+## Verification
+
+- Plan model, migration, interface, config, and guard tests: 89 passed.
+- Complete MCP suite after integration: 689 passed, 15 skipped.
+- Ruff check passed for PlanStore and its focused tests.
+- `git diff --check` passed.
diff --git a/.workflow/pr73-write-safety/logs/tasks-3.1-3.6-confirm-status-reconcile.md b/.workflow/pr73-write-safety/logs/tasks-3.1-3.6-confirm-status-reconcile.md
new file mode 100644
index 00000000..87f416fb
--- /dev/null
+++ b/.workflow/pr73-write-safety/logs/tasks-3.1-3.6-confirm-status-reconcile.md
@@ -0,0 +1,33 @@
+# Tasks 3.1–3.6 Log: Confirm, Status, and Reconcile
+
+## Confirm
+
+Added a canonical `WriteExecutor`, adapter registry, and `confirm_write_tool(plan_id)`. The executor validates that every operation kind has an adapter before claiming the plan, atomically elects one executor, increments operation attempts in durable storage, and records every adapter receipt.
+
+Repeated confirmation returns persisted status. An eight-thread concurrency test proves one adapter attempt for one plan.
+
+## Status
+
+Added canonical `get_write_status_tool(plan_id)`. Stored plan and operation payloads are removed from public responses. Crash-left `EXECUTING` and migrated `LEGACY_UNKNOWN` are exposed as `UNKNOWN` with reconciliation required.
+
+## Reconcile and resume
+
+Added a read-only `WriteReconciler`, reader registry, pure expected/desired/observed decision function, and `reconcile_write_tool(plan_id)`.
+
+Reconciliation maps:
+
+- observed desired state -> `APPLIED`;
+- observed expected state -> `RETRYABLE_NOT_APPLIED`;
+- observed third state -> `CONFLICT`;
+- missing delete target -> `APPLIED`.
+
+Reconciliation is idempotent and skips final operations. A later concurrency review proved that observing expected state cannot exclude a previously dispatched request from committing. The final rule therefore allows `RETRYABLE_NOT_APPLIED` only for an unclaimed attempt-0 operation. A claimed operation remains `UNKNOWN` with `IN_FLIGHT_COMMIT_NOT_EXCLUDED`, even when its current state still equals expected.
+
+Execution uses durable plan leases, owner tokens, and per-attempt fencing tokens. Each operation claim renews the lease for at least the configured write timeout plus safety margin. Receipt writes compare status, attempt, attempt token, owner, and lease. Reconcile can take ownership only after lease expiry, and an active PARTIAL lease cannot be replaced by a second resume owner.
+
+## Verification
+
+- Confirm, status, reconciliation, fencing, and public tool tests pass in the final full suite.
+- PlanStore/model focused tests: 29 passed.
+- Public toolset contract includes confirm, status, and reconcile.
+- `git diff --check` passed.
diff --git a/.workflow/pr73-write-safety/logs/tasks-4.1-6.4-atomic-boundaries.md b/.workflow/pr73-write-safety/logs/tasks-4.1-6.4-atomic-boundaries.md
new file mode 100644
index 00000000..dd329a15
--- /dev/null
+++ b/.workflow/pr73-write-safety/logs/tasks-4.1-6.4-atomic-boundaries.md
@@ -0,0 +1,28 @@
+# Tasks 4.1–6.4 Log: Atomic Graph, Schema, and Property Boundaries
+
+## Edge endpoint identity
+
+Create-edge dry-run resolves live endpoint IDs with bounded ID queries and stores `source_id`/`target_id` in the server compiled plan. Earlier planned vertices are accepted only when they expose an explicit stable ID; otherwise planning fails with `ENDPOINT_ID_NOT_STABLE`. Edge writes use only `g.V(source_id)` and `g.V(target_id)` and never re-evaluate endpoint predicates. Dependency operation IDs and create-vertex result receipts remain for task 4.2/7.2.
+
+Unit and Docker tests cover predicate expansion after dry-run and verify one edge between the approved IDs. Full create identity reconciliation remains pending task 4.4.
+
+## Schema boundary
+
+Schema create is one operation per plan. Manager construction, request, and post-read now share one exception boundary. Schema write clients use `write_timeout_seconds`. Identical existing schema returns `ALREADY_APPLIED`; same-name different schema returns `CONFLICT`; ambiguous failures persist `UNKNOWN`.
+
+Removed unreachable batch-partial helpers. Canonical `ApplyReceipt` supplies the status and observed state while legacy public success fields remain available during compatibility.
+
+## Property validation and CAS
+
+Shared validation is exception-free for arbitrary-precision integers and enforces IEEE-754 FLOAT/DOUBLE bounds, non-finite rejection, collection elements, UUID, DATE, BLOB, and OBJECT rules across import, create, and mutation planners.
+
+Added a narrow `replace_properties_if_match` Protocol and contract without adding a nonexistent GraphManager endpoint. HugeGraph 1.7 RocksDB remains `VERIFIED_UNSUPPORTED`; mutation dry-run exposes expected/desired CAS intent and operation ID, while confirmation remains fail-closed and does not consume or write. SINGLE/LIST/SET two-client tests remain capability-gated and skipped until support is proven.
+
+## Verification
+
+- Complete MCP suite: 739 passed, 16 skipped.
+- Edge tests: 68 unit tests and 6 focused Docker tests in the worker lane.
+- Schema tests: 96 passed.
+- Property validation focused tests: 47 passed.
+- CAS contract: 39 passed, 3 capability-gated skipped.
+- Focused Ruff, formatting, and diff checks passed.
diff --git a/.workflow/pr73-write-safety/logs/tasks-4.2-4.7-7.1-7.4-graph-workflow.md b/.workflow/pr73-write-safety/logs/tasks-4.2-4.7-7.1-7.4-graph-workflow.md
new file mode 100644
index 00000000..906dd75f
--- /dev/null
+++ b/.workflow/pr73-write-safety/logs/tasks-4.2-4.7-7.1-7.4-graph-workflow.md
@@ -0,0 +1,29 @@
+# Tasks 4.2–4.7 and 7.1–7.4 Log: Canonical Graph Workflow
+
+## Canonical graph plans
+
+Added graph plan compilation with ordered operation IDs, stable vertex/edge/delete identities, and explicit edge dependencies on earlier vertex operations. Live endpoints and deletion targets are bound to backend IDs. Earlier planned vertices use dependency operation IDs; create receipts are the source for resulting backend IDs.
+
+`DELETE_EDGE` is the only graph mutation registered in the default canonical executor because HugeGraph 1.7 returns a stable edge ID and supports ID-based deletion. It produces durable receipts and has create/delete reconciliation readers.
+
+## Fail-closed graph creation and import
+
+HugeGraph 1.7 does not expose verified atomic vertex/edge create-if-absent primitives. CREATE_VERTEX, CREATE_EDGE, and graph import therefore compile a complete workflow preview but remain `confirmable=false` and do not issue an executable plan. The legacy sequential and HugeGraph-AI `/graph-import` confirmation paths are disabled rather than presented as durable workflows.
+
+## Isolated vertex deletion evidence
+
+A barrier-controlled Docker race ran three 100-round series against HugeGraph 1.7 RocksDB. The first series produced 54 cases where both add-edge and delete requests reported success while the source vertex and new edge were both absent. This proves implicit cascade can occur and the conditional traversal does not provide the required isolation.
+
+`ISOLATED_VERTEX_DELETE` remains `UNKNOWN`; its adapter is not registered. Dry-run remains available as preview, while confirmation returns `FEATURE_DISABLED` before plan consumption.
+
+## Fault recovery
+
+Added deterministic tests for claim-before-call, crash after plan claim, response loss, receipt persistence failure, and response loss after receipt persistence. Every case reopens a fresh SQLitePlanStore and verifies durable status. Reconcile marks never-claimed operations as `RETRYABLE_NOT_APPLIED`; resume skips applied operations and replays only those proven not applied.
+
+## Verification
+
+- Canonical graph workflow/reconcile tests: 8 focused tests.
+- Executor fault suite and related model/store tests: 59 passed.
+- Barrier Docker suite: 16 passed, 1 expected failure documenting unsupported isolation.
+- Complete MCP suite before final verification: 769 passed, 17 skipped.
+- Focused Ruff, formatting, and diff checks passed.
diff --git a/.workflow/pr73-write-safety/logs/tasks-8.1-8.5-runtime-safety.md b/.workflow/pr73-write-safety/logs/tasks-8.1-8.5-runtime-safety.md
new file mode 100644
index 00000000..fc1e531b
--- /dev/null
+++ b/.workflow/pr73-write-safety/logs/tasks-8.1-8.5-runtime-safety.md
@@ -0,0 +1,30 @@
+# Tasks 8.1–8.5 Log: Configuration and Raw Query Safety
+
+## Typed configuration
+
+Central numeric field specifications now define type, default, minimum, and maximum. Timeout values are limited to 0.001–86400 seconds, AI timeout to 1–86400 seconds, result items to 1–1,000,000, result bytes to 1 byte–1 GiB, and write instance count to 1–1024. Empty, invalid, negative, non-finite, arbitrary-precision, and over-limit inputs fall back safely without raising during server import.
+
+## Upstream envelope boundary
+
+Thin envelope normalization is iterative and depth-limited. One compatibility wrapper is accepted; excessive depth, cycles, and malformed nested envelopes return a structured, non-retryable upstream-response error.
+
+## Raw Gremlin
+
+All public raw execution paths remain disabled even in admin mode:
+
+- direct raw read;
+- direct raw write;
+- generated query with `execute=true`.
+
+Generate-only behavior remains available. Re-enabling execution requires verified read-only principal enforcement, server evaluation timeout, server result cap, and streaming HTTP byte cap. Current post-materialization item/byte checks are explicitly labeled output guards with `hard_budget=false`.
+
+A pyhugegraph contract fixture records the missing request/response capabilities and confirms that `GremlinManager.exec()` does not claim per-request timeout or streaming-budget parameters.
+
+## Verification
+
+- Configuration tests: 50 passed.
+- HugeGraph AI client tests: 31 passed.
+- Raw Gremlin focused MCP tests: 45 passed in the worker lane.
+- PyHugeGraph raw-query contract tests: 2 passed.
+- Complete MCP suite after integration: 689 passed, 15 skipped.
+- Focused Ruff and diff checks passed.
diff --git a/.workflow/pr73-write-safety/logs/tasks-9.1-9.3-public-contract.md b/.workflow/pr73-write-safety/logs/tasks-9.1-9.3-public-contract.md
new file mode 100644
index 00000000..9e0963dc
--- /dev/null
+++ b/.workflow/pr73-write-safety/logs/tasks-9.1-9.3-public-contract.md
@@ -0,0 +1,19 @@
+# Tasks 9.1–9.3 Log: Compatibility and Public Documentation
+
+## Compatibility adapter
+
+Legacy `plan_hash`, `nonce`, and `expires_at` fields are validated as an all-or-nothing group before business logic. Complete legacy confirmation responses carry `LEGACY_CONFIRMATION_DEPRECATED`. Canonical confirm, status, and reconcile tools accept only `plan_id`; legacy endpoints do not accept `plan_id`, preventing mixed protocol ambiguity.
+
+## Public contract
+
+Updated tool docstrings and contract tests for canonical lifecycle tools, status vocabulary, schema single-operation behavior, mutation and unsupported graph operations as preview-only, Raw Gremlin hard-budget gating, timeouts, PlanStore topology, and legacy warnings.
+
+## Documentation
+
+Updated both READMEs and the P0a checklist. The checklist uses canonical plan-ID flows, includes complete legacy examples, separates executable DELETE_EDGE from preview-only operations, documents `PARTIAL`/`UNKNOWN`, and points to executable fault and Docker tests.
+
+## Verification
+
+- Public docs, tool contract, confirmation contract, and schema tests: 156 passed.
+- Focused Ruff/format and diff checks passed.
+- Two independent reader passes reported no blocker or important documentation issue.
diff --git a/.workflow/pr73-write-safety/requirements.md b/.workflow/pr73-write-safety/requirements.md
new file mode 100644
index 00000000..5589a9e8
--- /dev/null
+++ b/.workflow/pr73-write-safety/requirements.md
@@ -0,0 +1,16 @@
+# Requirements: HugeGraph MCP Confirmed Write Safety
+
+The requirements below consolidate the user-approved PR #73 review package, the verified current implementation defects, and the approved design in `design.md`.
+
+- **R1 — Immutable authorization:** A confirmed write must execute the exact server-persisted plan approved during dry-run. Client-resubmitted mutation payloads are never authoritative.
+- **R2 — Stable graph identity:** Delete targets and edge endpoints must be bound to stable backend identities before a write. Write traversals must not re-evaluate mutable property predicates.
+- **R3 — Atomic conditional delete:** `cascade=false` vertex deletion must atomically enforce the no-incident-edge precondition at the HugeGraph boundary.
+- **R4 — Backend property CAS:** Property mutation must use backend-enforced expected-to-desired compare-and-set. It remains preview-only while that capability is unavailable.
+- **R5 — Durable outcome model:** Every confirmed operation must have a durable state and receipt. `APPLIED`, `ALREADY_APPLIED`, `REJECTED`, `CONFLICT`, `PARTIAL`, and `UNKNOWN` must retain distinct meanings.
+- **R6 — Reconciliation:** `UNKNOWN`, `PARTIAL`, and migrated legacy-unknown operations must be queryable and reconcilable. Another attempt is authorized only when durable state proves the operation was never dispatched, or a backend-enforced idempotency/fencing primitive proves replay safety; observing the expected state alone is insufficient after dispatch.
+- **R7 — Schema safety:** One schema plan creates one object, uses the write timeout, and treats manager construction through post-read verification as one UNKNOWN boundary.
+- **R8 — Durable import workflow:** Graph import must persist per-operation dependencies, results, and backend IDs. Partial progress must be explicit and resumable only after reconciliation.
+- **R9 — Query resource boundary:** Raw Gremlin remains disabled until a read-only principal, server execution timeout, server-side result cap, and streaming response-byte cap are enforceable.
+- **R10 — Typed configuration:** Timeout and budget configuration must reject invalid, non-finite, overflowing, and excessively large values without raising during server import.
+- **R11 — Stable public contracts:** Confirm, status, reconcile, errors, tool registration, documentation, and compatibility behavior must agree.
+- **R12 — Verifiable correctness:** Unit, migration, fault-injection, concurrency, and Docker HugeGraph 1.7 tests must prove the safety invariants and database state together.
diff --git a/.workflow/pr73-write-safety/tasks.md b/.workflow/pr73-write-safety/tasks.md
new file mode 100644
index 00000000..00c843a6
--- /dev/null
+++ b/.workflow/pr73-write-safety/tasks.md
@@ -0,0 +1,76 @@
+# Implementation Plan: HugeGraph MCP Confirmed Write Safety
+
+- [x] 1. **Research and current-state inventory** `[优先级: 高]`
+
+ - [x] 1.1. Map the current uncommitted plan-store, receipt, executor, graph-operation, schema, import, query-budget, configuration, and compatibility changes to requirements R1–R12; identify reusable code and code that conflicts with the approved design. `(关联需求: R1-R12)`
+ - [x] 1.2. Inspect HugeGraph 1.7 client/server capabilities for create-if-absent, edge identity, isolated vertex deletion, property CAS, evaluation timeout, result limiting, and response streaming; encode the supported capability matrix as test fixtures or constants. `(依赖于: 1.1)` `(关联需求: R2-R4, R7-R10)`
+ - [x] 1.3. Define the compatibility contract for the current `plan_hash`/`nonce` API and the new `plan_id` API in contract tests before changing public tools. `(依赖于: 1.1)` `(关联需求: R1, R11)`
+
+- [x] 2. **Canonical plan, receipt, and state model** `[优先级: 高]`
+
+ - [x] 2.1. **TDD:** Add tests for immutable `WritePlan`, ordered `OperationPlan`, operation identity, canonical serialization, and the complete status-transition matrix including `PARTIAL`. `(依赖于: 1.3)` `(关联需求: R1, R5)`
+ - [x] 2.2. Implement the canonical plan/operation/receipt dataclasses and central status aggregation rules; remove competing ad-hoc status strings from tool modules. `(依赖于: 2.1)` `(关联需求: R1, R5)`
+ - [x] 2.3. **TDD:** Add SQLite migration tests covering current confirmation tables, existing rows, corrupt payloads, `LEGACY_UNKNOWN`, schema versioning, and restart persistence. `(依赖于: 2.2)` `(关联需求: R5, R6, R11)`
+ - [x] 2.4. Replace the confirmation-only schema with versioned `write_plans` and per-operation `write_operations`; preserve immutable plan payloads and atomic state transitions. `(依赖于: 2.3)` `(关联需求: R1, R5, R6)`
+ - [x] 2.5. Add a PlanStore interface, retain SQLite as the declared single-instance implementation, and fail closed when a write-enabled multi-instance configuration lacks a shared store. `(依赖于: 2.4)` `(关联需求: R1, R5)`
+
+- [x] 3. **Unified confirm, status, and reconciliation entry points** `[优先级: 高]`
+
+ - [x] 3.1. **TDD:** Add contract and concurrency tests showing that `confirm_write_tool(plan_id)` executes the stored plan, allows one executor claim, and returns persisted state on repeated confirmation. `(依赖于: 2.5)` `(关联需求: R1, R5, R11)`
+ - [x] 3.2. Implement the unified confirm executor dispatch and atomically transition `ISSUED → EXECUTING` before invoking an adapter. `(依赖于: 3.1)` `(关联需求: R1, R5)`
+ - [x] 3.3. **TDD:** Add status tests for `APPLIED`, `ALREADY_APPLIED`, `REJECTED`, `CONFLICT`, `PARTIAL`, crash-left `EXECUTING`, and `UNKNOWN`, including payload redaction. `(依赖于: 3.2)` `(关联需求: R5, R11)`
+ - [x] 3.4. Implement `get_write_status_tool(plan_id)` using canonical plan and operation receipts; expose crash-left `EXECUTING` as `UNKNOWN`. `(依赖于: 3.3)` `(关联需求: R5, R11)`
+ - [x] 3.5. **TDD:** Add reconciliation decision tests for create, delete, schema create, and property replacement expected/desired/third-state outcomes. `(依赖于: 3.4)` `(关联需求: R6)`
+ - [x] 3.6. Implement `reconcile_write_tool(plan_id)` as a read-only, idempotent operation and permit resume only from a proven not-applied state. `(依赖于: 3.5)` `(关联需求: R6, R11)`
+
+- [x] 4. **Stable graph-operation compilation and execution** `[优先级: 高]`
+
+ - [x] 4.1. **TDD:** Add a barrier-controlled regression test in which edge endpoint predicates expand after planning; assert exactly one edge is created between the approved IDs. `(依赖于: 3.2)` `(关联需求: R2, R12)`
+ - [x] 4.2. Compile existing edge endpoints to backend IDs and compile newly created endpoints to dependency operation IDs; persist actual vertex IDs in create receipts. `(依赖于: 4.1)` `(关联需求: R2, R8)`
+ - [x] 4.3. Change edge execution to consume only persisted source/target IDs and stable sort-key or idempotency identity; remove predicate-based endpoint lookup from the write traversal. `(依赖于: 4.2)` `(关联需求: R2)`
+ - [x] 4.4. **TDD:** Add create-vertex and create-edge reconciliation tests for identical existing state, conflicting state, missing state, and indistinguishable duplicate-edge rejection. `(依赖于: 4.3)` `(关联需求: R2, R6)`
+ - [x] 4.5. Keep delete compilation bound to backend ID and label, and route delete receipts through the unified executor and reconciler. `(依赖于: 3.6)` `(关联需求: R2, R5, R6)`
+ - [x] 4.6. **TDD:** Add backend-specific concurrent add-edge versus isolated-delete tests with barriers and database-state assertions. `(依赖于: 1.2, 4.5)` `(关联需求: R3, R12)`
+ - [x] 4.7. Enable the isolated-delete adapter only for backends that pass the atomicity test; otherwise return `FEATURE_DISABLED` before confirmation consumption. `(依赖于: 4.6)` `(关联需求: R3)`
+
+- [x] 5. **Schema execution boundary** `[优先级: 高]`
+
+ - [x] 5.1. **TDD:** Add tests for one-operation plans, identical existing objects, same-name conflicts, manager construction failure, transport failure, post-read failure, and receipt persistence. `(依赖于: 3.2)` `(关联需求: R5, R7)`
+ - [x] 5.2. Move schema manager construction, request, response handling, and verification into one executor exception boundary that persists `UNKNOWN` when the result is ambiguous. `(依赖于: 5.1)` `(关联需求: R5, R7)`
+ - [x] 5.3. Construct schema write clients with `write_timeout_seconds` and preserve read timeout only for planning and reconciliation reads. `(依赖于: 5.2)` `(关联需求: R7, R10)`
+ - [x] 5.4. Remove schema batch-partial compatibility paths and route every schema create through the canonical receipt/status model. `(依赖于: 5.3)` `(关联需求: R5, R7, R11)`
+
+- [x] 6. **Property validation and backend CAS capability** `[优先级: 高]`
+
+ - [x] 6.1. **TDD:** Extend shared property-validation tests with arbitrary-precision integers, FLOAT/DOUBLE overflow, non-finite floats, collection elements, UUID, DATE, BLOB, and OBJECT cases. `(依赖于: 1.1)` `(关联需求: R4, R10)`
+ - [x] 6.2. Make numeric validation total and exception-free, and ensure import, vertex/edge creation, and mutation planners use the same validator. `(依赖于: 6.1)` `(关联需求: R4, R10)`
+ - [x] 6.3. Define and test the pyhugegraph contract for backend `replace_properties_if_match`, including operation ID, expected state, desired state, and result mapping. `(依赖于: 1.2, 6.2)` `(关联需求: R4, R5)`
+ - [x] 6.4. Keep confirmed property mutation fail-closed until the server capability exists and passes two-client SINGLE/LIST/SET concurrency tests; connect the adapter only after those tests pass. `(依赖于: 6.3)` `(关联需求: R4, R12)`
+
+- [x] 7. **Durable graph import workflow** `[优先级: 高]`
+
+ - [x] 7.1. **TDD:** Add workflow aggregation tests proving that known prior writes plus a later failure produce `PARTIAL`, never `REJECTED`. `(依赖于: 2.2, 3.4)` `(关联需求: R5, R8)`
+ - [x] 7.2. Compile imports into ordered vertex and edge operations with explicit dependencies, stable identities, and per-operation receipts. `(依赖于: 4.3, 7.1)` `(关联需求: R2, R8)`
+ - [x] 7.3. Replace the direct sequential import loop and legacy AI write path with the durable workflow executor; persist vertex result IDs before dependent edge execution. `(依赖于: 7.2)` `(关联需求: R5, R8)`
+ - [x] 7.4. **TDD:** Add crash/resume tests at every operation boundary and verify that resume reconciles before executing only proven-not-applied operations. `(依赖于: 7.3)` `(关联需求: R6, R8, R12)`
+
+- [x] 8. **Configuration and raw-query safety** `[优先级: 高]`
+
+ - [x] 8.1. **TDD:** Add typed configuration boundary tests for empty, negative, non-finite, overflowing, and excessively large integer/float timeout and budget values. `(依赖于: 1.1)` `(关联需求: R10)`
+ - [x] 8.2. Implement field-spec-based configuration parsing with explicit type, default, minimum, and maximum; ensure server import never raises for environment input. `(依赖于: 8.1)` `(关联需求: R10)`
+ - [x] 8.3. **TDD:** Add bounded nested-envelope tests and replace recursive upstream-envelope normalization with iterative depth-limited normalization. `(依赖于: 1.1)` `(关联需求: R10, R11)`
+ - [x] 8.4. Define pyhugegraph/server contracts and tests for a read-only principal, evaluation timeout, server result cap, and streaming HTTP byte cap. `(依赖于: 1.2)` `(关联需求: R9)`
+ - [x] 8.5. Keep public raw execution disabled until all hard-budget capabilities pass integration tests; retain post-materialization limits only as output guards and label them accordingly. `(依赖于: 8.4)` `(关联需求: R9, R11)`
+
+- [x] 9. **Public compatibility and documentation** `[优先级: 中]`
+
+ - [x] 9.1. Add the one-release compatibility adapter for `plan_hash`, `nonce`, and `expires_at`, including deprecation warnings and server-plan-only execution tests. `(依赖于: 3.6)` `(关联需求: R1, R11)`
+ - [x] 9.2. Update tool docstrings and generated/listed contracts for plan IDs, canonical statuses, mutation preview-only behavior, raw-query gating, timeout fields, and status/reconcile tools. `(依赖于: 5.4, 6.4, 8.5, 9.1)` `(关联需求: R11)`
+ - [x] 9.3. Update `README.md`, `README.zh-CN.md`, and the P0a integration checklist to match the tested runtime contract and single-operation schema workflow. `(依赖于: 9.2)` `(关联需求: R11)`
+
+- [x] 10. **Automated verification and regression gates** `[优先级: 高]`
+
+ - [x] 10.1. Run the affected MCP, HugeGraph Python client, and Thin API unit/contract suites and resolve regressions without weakening safety assertions. `(依赖于: 4.7, 5.4, 6.4, 7.4, 8.5, 9.3)` `(关联需求: R12)`
+ - [x] 10.2. Run SQLite migration and fault-injection suites in fresh processes, covering all documented crash points and durable status recovery. `(依赖于: 10.1)` `(关联需求: R5, R6, R12)`
+ - [x] 10.3. Run Docker HugeGraph 1.7 integration suites for supported backends, verifying graph state and ledger state together for edge creation, isolated deletion, schema create, import partials, and reconciliation. `(依赖于: 10.2)` `(关联需求: R2-R8, R12)`
+ - [x] 10.4. Run repository-declared lint, formatting, and diff-hygiene checks over the final changed paths. `(依赖于: 10.3)` `(关联需求: R12)`
diff --git a/hugegraph-mcp/README.md b/hugegraph-mcp/README.md
index 91db75b8..d1049bb1 100644
--- a/hugegraph-mcp/README.md
+++ b/hugegraph-mcp/README.md
@@ -2,13 +2,13 @@
[中文文档](README.zh-CN.md)
-HugeGraph MCP is a Model Context Protocol server for HugeGraph. It is designed as a safe, controlled, thin adapter layer: it exposes a small set of stable tools and centralizes configuration, permission checks, read-only Gremlin validation, the dry-run/confirm write safety chain, and the unified response envelope.
+HugeGraph MCP is a safe, controlled Model Context Protocol adapter for HugeGraph Server. It exposes stable structured tools, centralizes configuration and permission checks, and persists immutable write plans and outcomes.
-**Requires HugeGraph Server >= 1.7.0** (MCP defaults to `graphspace=DEFAULT` and relies on graphspace-scoped API routes that are not available in older versions).
+**Requires HugeGraph Server >= 1.7.0.** The default graph path is `DEFAULT/hugegraph` and uses graphspace-scoped APIs unavailable in older releases.
## Quick Start
-Install [uv](https://docs.astral.sh/uv/getting-started/installation/), set the connection variables, and run HugeGraph MCP with `uvx`. Replace `hugegraph.example.com` with your HugeGraph Server host:
+Install [uv](https://docs.astral.sh/uv/getting-started/installation/), then start the server in read-only mode:
```bash
export HUGEGRAPH_URL=http://hugegraph.example.com:8080
@@ -20,12 +20,9 @@ export HUGEGRAPH_MCP_READONLY=true
uvx --from hugegraph-mcp==1.7.0 hugegraph-mcp
```
-The process speaks MCP JSON-RPC on stdout; connect it from an MCP client. Keep
-`HUGEGRAPH_MCP_READONLY=true` unless you have a deliberate write-validation
-plan. The default toolset is `v2_core`; set `HUGEGRAPH_MCP_TOOLSET=v1` before
-startup when an older client requires the 10-tool compatibility contract.
+The process speaks MCP JSON-RPC on stdout. Keep `HUGEGRAPH_MCP_READONLY=true` unless controlled writes are required. The default toolset is `v2_core`; set `HUGEGRAPH_MCP_TOOLSET=v1` before startup only for the legacy 10-tool contract.
-For MCP clients that accept JSON server configuration, use `uvx` as the command:
+For MCP clients that accept JSON server configuration:
```json
{
@@ -47,68 +44,49 @@ For MCP clients that accept JSON server configuration, use `uvx` as the command:
## Developer Notes
-### Run from Source
-
-From the repository root:
+Run from the repository checkout while keeping the repository path explicit:
```bash
-uv venv
-uv pip install -e ../hugegraph-python-client -e .
-uv run --project hugegraph-mcp hugegraph-mcp
+export PYTHONPATH=/Users/uleng/Code/hugegraph-ai-pr73-mcp/hugegraph-mcp:/Users/uleng/Code/hugegraph-ai-pr73-mcp/hugegraph-python-client/src
+/Users/uleng/Code/hugegraph-ai-pr73-mcp/.venv/bin/python -m hugegraph_mcp.server
```
-### Design Boundary
-
-HugeGraph MCP does not turn MCP into a second business kernel. The MCP layer is responsible for:
-
-- Exposing stable MCP tool interfaces
-- Reading runtime configuration
-- Enforcing permission and readonly guards
-- Validating whether Gremlin is read-only
-- Generating and validating `plan_hash`
-- Returning a unified response envelope
-- Forwarding AI capabilities to HugeGraph-AI, or graph reads/writes to HugeGraph Server
-
-### Public Tool Surface
-
-The default `v2_core` toolset registers 13 MCP tools: 11 normal user-facing stable tools plus 2 admin/debug tools that are registered but blocked by default.
-
-The normal user-facing stable tools are:
-
-- `inspect_graph_tool`
-- `inspect_schema_tool`
-- `query_graph_data_tool`
-- `generate_gremlin_tool`
-- `execute_gremlin_read_tool`
-- `extract_graph_data_tool`
-- `design_schema_tool`
-- `apply_schema_tool`
-- `mutate_graph_properties_tool`
-- `import_graph_data_tool`
-- `delete_graph_data_tool`
-
-These tools are still registered in MCP, but they are admin/debug capabilities and are blocked by default when `HUGEGRAPH_MCP_ADMIN_MODE=false`. Write-capable admin tools also require `HUGEGRAPH_MCP_READONLY=false`:
+This command uses the checkout's existing root virtual environment and forces both
+`hugegraph-mcp` and its sibling Python client to resolve from the current checkout.
+It does not ask uv to solve the standalone `hugegraph-mcp` subproject, which would
+try to satisfy `hugegraph-python-client>=1.7.0` from a package index instead of the
+sibling source tree.
-- `execute_gremlin_write_tool`
-- `refresh_vid_embeddings_tool`
+The MCP layer exposes stable tools, reads runtime configuration, enforces permissions, validates structured requests, persists write plans and receipts, and delegates graph operations to HugeGraph Server or enabled AI operations to HugeGraph-AI.
-`execute_gremlin_write_tool` is the sole break-glass exception to the write safety chain. It executes arbitrary Gremlin writes without a preview, dry-run, plan hash, or confirmation. Enable it only on an isolated transport available to trusted administrators; do not enable it on a shared agent or client endpoint.
+## Public Tool Surface
-### Toolset Selection
+The default `v2_core` contract registers 16 tools. The `v1` compatibility contract registers 10 tools and omits the six v2 additions.
-`HUGEGRAPH_MCP_TOOLSET` controls the public tool contract:
+| Tool | Contract | Description |
+|------|----------|-------------|
+| `inspect_graph_tool` | v1, v2 | Inspect connection, schema summary, read-only state, and tool contract |
+| `generate_gremlin_tool` | v1, v2 | Generate Gremlin; execution is disabled until the hard-budget contract is verified |
+| `execute_gremlin_read_tool` | v1, v2 | Registered for compatibility; public raw execution currently returns `FEATURE_DISABLED` |
+| `extract_graph_data_tool` | v1, v2 | Extract candidate graph data without writing |
+| `design_schema_tool` | v1, v2 | Produce schema design guidance |
+| `apply_schema_tool` | v1, v2 | Validate or preview one schema create; v2 supports confirmed apply |
+| `import_graph_data_tool` | v1, v2 | Validate and preview structured vertex/edge creates; confirmation currently returns `FEATURE_DISABLED` |
+| `delete_graph_data_tool` | v1, v2 | Preview exact deletion; confirmed edge deletion is supported |
+| `refresh_vid_embeddings_tool` | v1, v2 | Admin write tool, disabled unless admin mode and writes are enabled |
+| `execute_gremlin_write_tool` | v1, v2 | Registered for compatibility; public raw execution currently returns `FEATURE_DISABLED` |
+| `inspect_schema_tool` | v2 | Inspect and filter schema objects and relations |
+| `query_graph_data_tool` | v2 | Perform typed, bounded vertex and edge reads |
+| `mutate_graph_properties_tool` | v2 | Preview property changes; confirmation is disabled without atomic CAS |
+| `confirm_write_tool` | v2 | Confirm one persisted plan by `plan_id` |
+| `get_write_status_tool` | v2 | Read the durable plan and operation outcome by `plan_id` |
+| `reconcile_write_tool` | v2 | Reconcile `UNKNOWN` or `PARTIAL` outcomes by `plan_id` using read-only checks |
-| Value | Tools | Intended use |
-|-------|------:|--------------|
-| `v1` | 10 | Compatibility mode for old clients; exposes the original stable tools and admin/debug tools, hides the three `v2_core` tools, and keeps `apply_schema_tool(mode="apply")` disabled |
-| `v2_core` | 13 | New deployment default; exposes the V1 tools plus `inspect_schema_tool`, `query_graph_data_tool`, and `mutate_graph_properties_tool`, and enables the P0a schema create apply path |
+Invalid `HUGEGRAPH_MCP_TOOLSET` values fail closed to `v1`. Tool registration is fixed at process startup, so restart the server after changing the toolset.
-When `HUGEGRAPH_MCP_TOOLSET` is unset, the server defaults to `v2_core`. Any value other than exact `v1` is treated as `v2_core`.
-Toolset selection is applied when the MCP server starts and registers tools; restart the MCP server after changing this variable.
+## Unified Response Envelope
-### Unified Response Envelope
-
-High-level tools return a unified envelope:
+High-level tools return:
```json
{
@@ -127,168 +105,75 @@ High-level tools return a unified envelope:
}
```
-When a call fails, `ok=false` and `error` uses this structure:
+Failures set `ok=false`; `error.type` is a stable machine-readable code and `error.retryable` must be observed.
-```json
-{
- "type": "READONLY_VIOLATION",
- "message": "DATA_WRITE capability is disabled in read-only mode",
- "suggestion": "Disable HUGEGRAPH_MCP_READONLY to allow this operation.",
- "retryable": false,
- "source": "hugegraph-mcp",
- "details": {}
-}
-```
+## Write Safety Contract
-## Tool Reference
-
-### User-Facing Tool Overview
-
-| Tool | Description |
-|------|-------------|
-| `inspect_graph_tool` | Inspect HugeGraph Server status, schema summary, readonly state, AI availability, and current MCP tool contract fields. Vertex/edge counts stay `null` unless `include_counts=true` |
-| `inspect_schema_tool` | Inspect schema objects, relations, and index labels; supports filtering by property key, vertex label, edge label, or index label |
-| `query_graph_data_tool` | Query vertices or edges by typed operations (`get_by_id`, `get_by_ids`, `page`, `condition`) with explicit limits and no Gremlin full-scan fallback |
-| `generate_gremlin_tool` | Generate Gremlin from natural language; defaults to generation only; `execute=true` still requires read-only validation |
-| `execute_gremlin_read_tool` | Execute read-only Gremlin queries; rejects queries whose safety cannot be confirmed and supports `limit_policy` for unbounded reads |
-| `extract_graph_data_tool` | Extract candidate graph data from natural language text and return vertex/edge structures without writing to HugeGraph |
-| `import_graph_data_tool` | Structured graph data import entrypoint; real writes must pass `dry_run -> plan_hash -> confirm` |
-| `delete_graph_data_tool` | Controlled delete entrypoint; supports only exact vertex or edge deletion, not conditional bulk delete or cascade delete |
-| `design_schema_tool` | Provide schema design guidance from proposed schema operations without modifying the database |
-| `apply_schema_tool` | Validate schema operations and dry-run the P0a apply scope; in `v2_core`, dry-run and confirmed `apply` support only `create_property_key`, `create_vertex_label`, and `create_edge_label`; in `v1`, real `apply` remains disabled |
-| `mutate_graph_properties_tool` | Append or eliminate properties on one exact vertex or edge; both operations require `dry_run -> plan_hash -> confirm` and reject stale targets |
-| `execute_gremlin_write_tool` | Execute direct Gremlin writes; disabled by default and available only when `HUGEGRAPH_MCP_ADMIN_MODE=true` and `HUGEGRAPH_MCP_READONLY=false` |
-| `refresh_vid_embeddings_tool` | Refresh VID embeddings and mutate index state; disabled by default and available only when `HUGEGRAPH_MCP_ADMIN_MODE=true` and `HUGEGRAPH_MCP_READONLY=false` |
-
-Schema apply uses a closed operation-field contract. The supported create fields
-are `data_type`, `cardinality`, `aggregate_type`, and `user_data` for property
-keys; `id_strategy`, `properties`, `primary_keys`, `nullable_keys`,
-`index_labels`, `enable_label_index`, and `user_data` for vertex labels; and
-`source_label`, `target_label`, `properties`, `nullable_keys`, `sort_keys`,
-`frequency`, `enable_label_index`, and `user_data` for edge labels. Fields not
-listed here, including `ttl*`, are rejected during validation before a plan is
-issued. Supported fields are forwarded to HugeGraph and checked by a post-read
-of the live schema.
-
-The old `query_graph_tool`, `manage_schema_tool`, and `manage_graph_data_tool` are no longer exposed as user interfaces. New integrations should use the stable tools listed above.
-
-## Write Safety Chain
-
-All normal user-facing write operations must follow this chain. The admin-only `execute_gremlin_write_tool` break-glass exception described above does not:
+Canonical writes use only a server-issued `plan_id`:
```text
-dry_run=true
- -> user/agent reviews preview, warnings, matched_count, mutation_summary
- -> records plan_hash, nonce, expires_at
- -> dry_run=false + confirm=true + original payload + plan_hash + nonce + expires_at
- -> MCP revalidates target, permission, schema, payload digest, and expiry
- -> executes the write
- -> returns write/delete results and failure details
+structured dry-run
+ -> review concrete targets, warnings, and mutation summary
+ -> receive immutable persisted plan_id
+ -> confirm_write_tool(plan_id)
+ -> get_write_status_tool(plan_id)
+ -> if required, reconcile_write_tool(plan_id)
```
-`plan_hash` is not just a payload hash. It binds at least:
-
-- Tool name
-- Operation mode
-- Graph URL
-- Graph name
-- Graph space
-- MCP readonly state
-- Current schema hash
-- Normalized payload digest
-- Nonce
-- Expiry
-
-The confirm phase must fully revalidate the plan. If the dry-run result expires, the target graph changes, the schema changes, the payload changes, or permissions change, confirm must fail and require a new dry run.
-
-### Import Semantics
+The confirmation call never accepts the original payload. The persisted plan is the sole execution authority. Each operation records a durable receipt, and reusing a completed `plan_id` returns its recorded outcome rather than applying the write again.
-`import_graph_data_tool(mode="ingest")` is the structured import path shared by `v2_core` and the `v1` compatibility toolset. It uses local schema validation, dry-run/hash/confirm, and direct Gremlin writes through `manage_graph_data()`; it does not call the HugeGraph-AI `/graph-import` HTTP path. The legacy/internal AI-backed function is named `ingest_graph_data_via_ai()`. This PR keeps both write kernels: public MCP import uses local Gremlin writes, while `ingest_graph_data_via_ai()` r [...]
+`APPLIED` means every operation is proven applied. `PARTIAL` means at least one operation was applied and the workflow did not completely apply. `UNKNOWN` means the service cannot prove whether an operation committed, so callers must query status and reconcile; they must not blindly repeat the write.
-Schema and graph-data dry-run share hard limits of `MAX_OPERATIONS = 200` and `MAX_PAYLOAD_BYTES = 1048576` (1 MiB). Exceeding either limit returns `VALIDATION_ERROR` before `plan_hash` is generated. The limits are not configurable in this release.
+The old `plan_hash`, `nonce`, and `expires_at` locator remains on legacy write entry points for the single compatibility release immediately following introduction of the plan-ID contract. All three fields are required together, cannot be mixed with `plan_id`, and produce a `LEGACY_CONFIRMATION_DEPRECATED` warning. New integrations must use `confirm_write_tool(plan_id)`.
-When `import_graph_data_tool(mode="ingest")` executes a create operation, it returns one of three states:
+The bundled SQLite plan store is safe for a single write-capable MCP instance. Configure a shared transactional store before deploying multiple write instances; the current package fails closed when `HUGEGRAPH_MCP_WRITE_INSTANCE_COUNT` is greater than one with the SQLite backend.
-- `success`: all writes succeeded
-- `partial` / `degraded`: some writes succeeded, some failed, or the final state cannot be fully confirmed
-- `error`: the write failed
+### Operation Boundaries
-The response should include written counts, failure details, and compensation suggestions to avoid an untraceable partial write.
+- A confirmable schema plan contains exactly one `create_property_key`, `create_vertex_label`, or `create_edge_label` operation. Success is reported as `APPLIED`. Index create, append/eliminate, and drop remain outside the apply scope.
+- Graph import is always preview-only because atomic create-if-absent capability has not been verified for HugeGraph 1.7.0. Its preview sets `confirmable=false` and `preview_only=true`, issues no `plan_id`, and confirmation returns `FEATURE_DISABLED` without writing.
+- Property mutation is preview-only because HugeGraph 1.7.0 does not expose an atomic compare-and-set property update. Confirmation returns `FEATURE_DISABLED`.
+- Isolated vertex deletion is preview-only. Docker concurrency testing proved that HugeGraph 1.7.0 cannot atomically guarantee “delete only if no incident edge”; confirmation returns `FEATURE_DISABLED`. Delete incident edges explicitly, then use an independently controlled maintenance path for the vertex.
+- Exact edge deletion remains confirmable because the plan binds the concrete edge ID.
-#### Edge Endpoint Contract
+Schema and graph-data dry-runs reject more than 200 operations or payloads larger than 1 MiB before returning a usable preview or plan.
-Edge endpoints accept both object and scalar forms:
+### Raw Gremlin Boundary
-```text
-object source/target -> forwarded as-is
- {"id": "1:Alice"} -> HugeGraph vertex id match
- {"name": "Alice"} -> complete primary-key match; arbitrary property
- matching is not part of the public graph_data contract
-
-scalar source/target -> if the live schema says the endpoint label has exactly
- one primary key, match by that primary key first;
- otherwise fall back to {"id": value}
-
-outV / inV / vertex id in payload -> always HugeGraph vertex id, with no
- primary-key remapping
-```
-
-The scalar endpoint form is a same-payload import convenience, but under a single-primary-key live schema it is resolved as a primary-key match and may match an already existing vertex in the graph. It is not limited to vertices in the current payload, so edge-only or edge-to-existing-vertex payloads are valid when the dry-run live match resolves each endpoint to exactly one vertex.
-
-### Delete Semantics
-
-`delete_graph_data_tool` is a controlled delete tool:
-
-- The dry-run phase must resolve the concrete objects that would be deleted
-- The confirm phase must re-match and verify that the target is unchanged
-- The tool must verify after deletion that the target no longer exists
-- Vertex deletion is rejected by default when the vertex has associated edges
+Every public raw Gremlin execution path is disabled, including `execute_gremlin_read_tool`, `execute_gremlin_write_tool`, and `generate_gremlin_tool(execute=true)`. Admin mode does not override this gate. Raw execution can be opened only after the deployment proves server evaluation and wait timeouts, a server-side result-item cap, a client streaming byte cap, and a read-only principal. Post-materialization item or byte checks are output guards, not hard resource budgets.
-Therefore, when deleting a vertex with associated edges, explicitly dry-run and delete the related edges first, then dry-run and delete the vertex.
+Structured reads and `generate_gremlin_tool(execute=false)` remain available.
## Configuration
-All configuration is read from environment variables.
-
| Variable | Default | Description |
|----------|---------|-------------|
| `HUGEGRAPH_URL` | `http://127.0.0.1:8080` | HugeGraph Server URL |
-| `HUGEGRAPH_GRAPH_PATH` | `DEFAULT/hugegraph` | Graph path in `GRAPH_SPACE/GRAPH_NAME` format |
-| `HUGEGRAPH_GRAPHSPACE` | unset | Override graph space separately |
-| `HUGEGRAPH_GRAPH` | unset | Override graph name separately |
+| `HUGEGRAPH_GRAPH_PATH` | `DEFAULT/hugegraph` | `GRAPH_SPACE/GRAPH_NAME` |
+| `HUGEGRAPH_GRAPHSPACE`, `HUGEGRAPH_GRAPH` | unset | Explicit values that override `HUGEGRAPH_GRAPH_PATH` |
| `HUGEGRAPH_USER` | `admin` | HugeGraph username |
-| `HUGEGRAPH_PASSWORD` | `""` | HugeGraph password |
-| `HUGEGRAPH_MCP_TOOLSET` | `v2_core` | Public tool contract: `v1` for 10-tool compatibility mode, `v2_core` for the 13-tool default |
-| `HUGEGRAPH_MCP_READONLY` | `true` | Whether readonly mode is enabled |
-| `HUGEGRAPH_MCP_ALLOW_AI` | `false` | Whether HugeGraph-AI calls are allowed |
-| `HUGEGRAPH_MCP_ADMIN_MODE` | `false` | Whether admin/debug tools are enabled |
+| `HUGEGRAPH_PASSWORD` | empty | HugeGraph password |
+| `HUGEGRAPH_MCP_TOOLSET` | `v2_core` | `v1` or `v2_core`; invalid values fall back to `v1` |
+| `HUGEGRAPH_MCP_READONLY` | `true` | Disable all controlled writes when true |
+| `HUGEGRAPH_MCP_ALLOW_AI` | `false` | Allow HugeGraph-AI calls |
+| `HUGEGRAPH_MCP_ADMIN_MODE` | `false` | Enable eligible admin/debug tools |
| `HUGEGRAPH_AI_URL` | `http://127.0.0.1:8001` | HugeGraph-AI URL |
-| `HUGEGRAPH_AI_TOKEN` | unset | Bearer token for an authenticated HugeGraph-AI service; public MCP tools configure this through the environment (an internal HTTP caller may override it per request) |
-| `HUGEGRAPH_AI_GRAPH_URL` | unset | Graph URL used by HugeGraph-AI; defaults to `HUGEGRAPH_URL` when unset |
-| `HUGEGRAPH_MCP_TIMEOUT_SECONDS` | `30` | AI call timeout in seconds |
-| `HUGEGRAPH_MCP_MAX_REPEAT_TIMES` | `10` | Recommended maximum for read-cost warnings on `repeat().times(n)` |
-| `HUGEGRAPH_MCP_STATE_DIR` | `$XDG_STATE_HOME/hugegraph-mcp`, or `~/.local/state/hugegraph-mcp` when `XDG_STATE_HOME` is unset | Local state directory for the persistent single-use confirmation ledger |
-
-`HUGEGRAPH_MCP_TIMEOUT_SECONDS` only applies to HugeGraph-AI HTTP calls; it does not apply to PyHugeClient Gremlin queries. Read-only Gremlin cost boundaries are reported as non-blocking read cost guard warnings for bare full-graph scans, `repeat()` without a `times()` bound, and `path` / `group` / `profile` without `limit` or `range`.
-
-Boolean configuration accepts `1`, `true`, `yes`, or `on` and `0`, `false`, `no`, or `off`, ignoring case and surrounding whitespace. Empty or invalid values fail closed: `HUGEGRAPH_MCP_READONLY` remains enabled, while `HUGEGRAPH_MCP_ALLOW_AI` and `HUGEGRAPH_MCP_ADMIN_MODE` remain disabled.
-
-The confirmation ledger persists server-issued dry-run plans and consumed nonce digests. Confirm accepts only a matching server-issued plan, enforces the server's 10-minute maximum TTL, and atomically consumes it so a write plan can be used only once across local process restarts and workers sharing the same state directory. On POSIX platforms, HugeGraph MCP restricts the state directory to mode `0700` and the ledger database to mode `0600`.
-
-Recommended safe defaults:
-
-- `HUGEGRAPH_MCP_READONLY=true`
-- `HUGEGRAPH_MCP_ALLOW_AI=false`
-- `HUGEGRAPH_MCP_ADMIN_MODE=false`
-
-Common combinations:
-
-| Scenario | Configuration |
-|----------|---------------|
-| Read-only graph query | `HUGEGRAPH_MCP_READONLY=true`, `HUGEGRAPH_MCP_ALLOW_AI=false` |
-| AI Gremlin generation / text extraction | `HUGEGRAPH_MCP_READONLY=true`, `HUGEGRAPH_MCP_ALLOW_AI=true` |
-| Controlled import and delete | `HUGEGRAPH_MCP_READONLY=false`, set `HUGEGRAPH_MCP_ALLOW_AI=true` as needed |
-| Administration/debugging | `HUGEGRAPH_MCP_READONLY=false`, `HUGEGRAPH_MCP_ADMIN_MODE=true` |
+| `HUGEGRAPH_AI_TOKEN` | unset | Optional HugeGraph-AI bearer token |
+| `HUGEGRAPH_AI_GRAPH_URL` | unset | Graph URL presented to HugeGraph-AI; falls back to `HUGEGRAPH_URL` |
+| `HUGEGRAPH_CONNECT_TIMEOUT_SECONDS` | `0.5` | HugeGraph connection timeout; range `0.001..86400` |
+| `HUGEGRAPH_READ_TIMEOUT_SECONDS` | `15` | Structured HugeGraph read timeout; range `0.001..86400` |
+| `HUGEGRAPH_WRITE_TIMEOUT_SECONDS` | `15` | HugeGraph data and schema write timeout; range `0.001..86400` |
+| `HUGEGRAPH_AI_TIMEOUT_SECONDS` | `30` | HugeGraph-AI HTTP timeout; range `1..86400` |
+| `HUGEGRAPH_MCP_TIMEOUT_SECONDS` | `30` | Deprecated fallback for AI timeout |
+| `HUGEGRAPH_MCP_MAX_RESULT_ITEMS` | `100` | Post-materialization output item guard; range `1..1000000` |
+| `HUGEGRAPH_MCP_MAX_RESULT_BYTES` | `1048576` | Post-materialization output byte guard; range `1..1073741824` |
+| `HUGEGRAPH_MCP_PLAN_STORE` | `sqlite` | Durable plan-store backend; only `sqlite` is currently supported |
+| `HUGEGRAPH_MCP_WRITE_INSTANCE_COUNT` | `1` | Declared count of write-capable MCP instances; SQLite requires exactly one |
+| `HUGEGRAPH_MCP_STATE_DIR` | `$XDG_STATE_HOME/hugegraph-mcp` or `~/.local/state/hugegraph-mcp` | Directory containing the plan, operation, and receipt database |
+
+Boolean values accept `1/true/yes/on` and `0/false/no/off`, case-insensitively. Invalid values use safe defaults. Invalid, non-finite, or out-of-range numeric values use the documented defaults.
+
+Recommended defaults are `HUGEGRAPH_MCP_READONLY=true`, `HUGEGRAPH_MCP_ALLOW_AI=false`, and `HUGEGRAPH_MCP_ADMIN_MODE=false`.
## License
diff --git a/hugegraph-mcp/README.zh-CN.md b/hugegraph-mcp/README.zh-CN.md
index f0708cbf..6a87c6da 100644
--- a/hugegraph-mcp/README.zh-CN.md
+++ b/hugegraph-mcp/README.zh-CN.md
@@ -2,13 +2,13 @@
[English](README.md)
-HugeGraph MCP 是 HugeGraph 的 Model Context Protocol Server。它的定位是安全、可控的薄适配层:对外暴露少量稳定工具,内部统一处理配置、权限、只读 Gremlin 校验、dry-run/confirm 写入安全链和统一响应格式。
+HugeGraph MCP 是 HugeGraph Server 的安全、可控 Model Context Protocol 适配层。它提供稳定的结构化工具,统一执行配置和权限检查,并持久化不可变写入计划与执行结果。
-**要求 HugeGraph Server >= 1.7.0**(MCP 默认使用 `graphspace=DEFAULT` 并依赖 graphspace 路由 API,旧版本不支持)。
+**要求 HugeGraph Server >= 1.7.0。** 默认图路径是 `DEFAULT/hugegraph`,依赖旧版本不具备的 graphspace 路由 API。
## 快速开始
-请先安装 [uv](https://docs.astral.sh/uv/getting-started/installation/),设置连接变量后通过 `uvx` 运行 HugeGraph MCP。请将 `hugegraph.example.com` 替换为实际的 HugeGraph Server 主机名:
+安装 [uv](https://docs.astral.sh/uv/getting-started/installation/),然后以只读模式启动:
```bash
export HUGEGRAPH_URL=http://hugegraph.example.com:8080
@@ -20,11 +20,9 @@ export HUGEGRAPH_MCP_READONLY=true
uvx --from hugegraph-mcp==1.7.0 hugegraph-mcp
```
-进程通过 stdout 输出 MCP JSON-RPC,请由 MCP 客户端连接。除非已经有明确的写入验证计划,否则保持
-`HUGEGRAPH_MCP_READONLY=true`。默认工具集是 `v2_core`;旧客户端需要 10 工具兼容契约时,在启动前设置
-`HUGEGRAPH_MCP_TOOLSET=v1`。
+进程通过 stdout 提供 MCP JSON-RPC。除非明确需要受控写入,请保持 `HUGEGRAPH_MCP_READONLY=true`。默认工具集为 `v2_core`;只有旧客户端需要 10 工具契约时才在启动前设置 `HUGEGRAPH_MCP_TOOLSET=v1`。
-对于支持 JSON Server 配置的 MCP 客户端,可使用 `uvx` 作为启动命令:
+支持 JSON Server 配置的 MCP 客户端可使用:
```json
{
@@ -46,71 +44,48 @@ uvx --from hugegraph-mcp==1.7.0 hugegraph-mcp
## 开发者说明
-### 从源码运行
-
-在仓库根目录执行:
+从代码仓运行:
```bash
-uv venv
-uv pip install -e ../hugegraph-python-client -e .
-uv run --project hugegraph-mcp hugegraph-mcp
+export PYTHONPATH=/Users/uleng/Code/hugegraph-ai-pr73-mcp/hugegraph-mcp:/Users/uleng/Code/hugegraph-ai-pr73-mcp/hugegraph-python-client/src
+/Users/uleng/Code/hugegraph-ai-pr73-mcp/.venv/bin/python -m hugegraph_mcp.server
```
-### 设计边界
-
-HugeGraph MCP 不把 MCP 做成另一套业务内核。MCP 层负责:
-
-- 暴露稳定 MCP 工具接口
-- 读取运行时配置
-- 执行权限和 readonly guard
-- 校验 Gremlin 是否只读
-- 生成并校验 `plan_hash`
-- 统一响应 envelope
-- 将 AI 能力转发给 HugeGraph-AI,或将图读写转发给 HugeGraph Server
-
-
-
-### 对外工具面
-
-默认 `v2_core` 工具集注册 13 个 MCP 工具:其中 11 个是普通用户稳定工具,另外 2 个是管理/调试工具,默认注册但受 admin mode 阻断。
-
-普通用户稳定工具包括:
-
-- `inspect_graph_tool`
-- `inspect_schema_tool`
-- `query_graph_data_tool`
-- `generate_gremlin_tool`
-- `execute_gremlin_read_tool`
-- `extract_graph_data_tool`
-- `design_schema_tool`
-- `apply_schema_tool`
-- `mutate_graph_properties_tool`
-- `import_graph_data_tool`
-- `delete_graph_data_tool`
-
-以下工具仍注册在 MCP 中,但属于管理/调试能力,默认受 `HUGEGRAPH_MCP_ADMIN_MODE=false` 阻断。具备写入能力的管理工具还要求 `HUGEGRAPH_MCP_READONLY=false`:
-
-- `execute_gremlin_write_tool`
-- `refresh_vid_embeddings_tool`
+此命令使用代码仓已有的根目录虚拟环境,并强制从当前 checkout 加载
+`hugegraph-mcp` 和同仓的 Python client。它不会让 uv 单独解析
+`hugegraph-mcp` 子项目,因此不会错误地从包索引解析
+`hugegraph-python-client>=1.7.0`,而会直接使用相邻源码。
-`execute_gremlin_write_tool` 是写入安全链唯一的 break-glass(紧急直通)例外。它不提供预览、dry-run、plan hash 或确认步骤,可直接执行任意 Gremlin 写语句。只能在隔离且仅受信管理员可访问的 transport 上启用,禁止在共享 Agent 或客户端端点启用。
+MCP 层负责稳定工具、运行时配置、权限、结构化请求校验、写入计划及回执持久化,并把图操作交给 HugeGraph Server,把已启用的 AI 操作交给 HugeGraph-AI。
-### 工具集选择
+## 对外工具面
-`HUGEGRAPH_MCP_TOOLSET` 控制对外工具契约:
+默认 `v2_core` 契约注册 16 个工具。`v1` 兼容契约注册 10 个工具,不包含六个 v2 新增工具。
-| 取值 | 工具数 | 用途 |
-|------|------:|------|
-| `v1` | 10 | 兼容旧客户端;只暴露原有稳定工具和管理/调试工具,隐藏三个 `v2_core` 新工具,并保持 `apply_schema_tool(mode="apply")` 禁用 |
-| `v2_core` | 13 | 新部署默认值;在 V1 工具基础上增加 `inspect_schema_tool`、`query_graph_data_tool`、`mutate_graph_properties_tool`,并启用 P0a 范围内的 schema create apply 路径 |
+| 工具 | 契约 | 说明 |
+|------|------|------|
+| `inspect_graph_tool` | v1、v2 | 查看连接、schema 摘要、只读状态和工具契约 |
+| `generate_gremlin_tool` | v1、v2 | 生成 Gremlin;在硬资源预算合同验证前禁止执行 |
+| `execute_gremlin_read_tool` | v1、v2 | 为兼容保留注册;公开 Raw Gremlin 执行当前返回 `FEATURE_DISABLED` |
+| `extract_graph_data_tool` | v1、v2 | 抽取候选图数据,不写图 |
+| `design_schema_tool` | v1、v2 | 提供 schema 设计建议 |
+| `apply_schema_tool` | v1、v2 | 校验或预览一个 schema create;v2 支持确认执行 |
+| `import_graph_data_tool` | v1、v2 | 校验并预览结构化点边创建;确认当前返回 `FEATURE_DISABLED` |
+| `delete_graph_data_tool` | v1、v2 | 预览精确删除;支持确认删除边 |
+| `refresh_vid_embeddings_tool` | v1、v2 | 管理写工具,仅在 admin mode 与写入均启用时开放 |
+| `execute_gremlin_write_tool` | v1、v2 | 为兼容保留注册;公开 Raw Gremlin 执行当前返回 `FEATURE_DISABLED` |
+| `inspect_schema_tool` | v2 | 查看并筛选 schema 对象及关系 |
+| `query_graph_data_tool` | v2 | 执行类型化、有界的点边读取 |
+| `mutate_graph_properties_tool` | v2 | 预览属性变更;缺少原子 CAS 时禁止确认 |
+| `confirm_write_tool` | v2 | 仅通过 `plan_id` 确认一个持久化计划 |
+| `get_write_status_tool` | v2 | 仅通过 `plan_id` 查询计划及操作的持久化结果 |
+| `reconcile_write_tool` | v2 | 仅通过 `plan_id` 和只读检查协调 `UNKNOWN` 或 `PARTIAL` 结果 |
-未设置 `HUGEGRAPH_MCP_TOOLSET` 时,服务默认使用 `v2_core`。除精确的 `v1` 之外,其他取值都会按 `v2_core` 处理。
-工具集选择在 MCP server 启动并注册工具时生效;修改该变量后需要重启 MCP server。
+非法 `HUGEGRAPH_MCP_TOOLSET` 值会收口到 `v1`。工具注册在进程启动时确定,修改工具集后需要重启服务。
+## 统一响应格式
-### 统一响应格式
-
-高层工具返回统一 envelope:
+高层工具返回:
```json
{
@@ -129,166 +104,75 @@ HugeGraph MCP 不把 MCP 做成另一套业务内核。MCP 层负责:
}
```
-失败时 `ok=false`,`error` 使用统一结构:
-
-```json
-{
- "type": "READONLY_VIOLATION",
- "message": "DATA_WRITE capability is disabled in read-only mode",
- "suggestion": "Disable HUGEGRAPH_MCP_READONLY to allow this operation.",
- "retryable": false,
- "source": "hugegraph-mcp",
- "details": {}
-}
-```
-
-
-## 工具参考
-
-### 用户可用工具总览
-
-| 工具 | 说明 |
-|------|------|
-| `inspect_graph_tool` | 查看 HugeGraph Server 状态、schema 摘要、readonly 状态、AI 可用性和当前 MCP 工具契约字段。默认不计数,`include_counts=true` 时才返回点边计数,否则 `vertex_count`/`edge_count` 为 `null` |
-| `inspect_schema_tool` | 查看 schema 对象、关系和索引标签;支持按 property key、vertex label、edge label 或 index label 过滤 |
-| `query_graph_data_tool` | 通过类型化操作查询点或边(`get_by_id`、`get_by_ids`、`page`、`condition`),有显式 limit,不自动降级为 Gremlin 全图扫描 |
-| `generate_gremlin_tool` | 根据自然语言生成 Gremlin;默认只生成,不执行;`execute=true` 时也必须通过只读校验 |
-| `execute_gremlin_read_tool` | 执行只读 Gremlin 查询;无法确认安全时拒绝执行,并支持 `limit_policy` 处理无界读取 |
-| `extract_graph_data_tool` | 从自然语言文本抽取候选图数据,返回点和边结构,不写入 HugeGraph |
-| `import_graph_data_tool` | 结构化图数据导入入口;真实写入必须经过 `dry_run -> plan_hash -> confirm` |
-| `delete_graph_data_tool` | 受控删除入口;只支持精确删除点或边,不支持条件批量删除和级联删除 |
-| `design_schema_tool` | 根据 schema 操作草案给出设计建议,不修改数据库 |
-| `apply_schema_tool` | 校验 schema 操作并 dry-run P0a apply 范围;`v2_core` 中 dry-run 和确认后的 `apply` 只支持 `create_property_key`、`create_vertex_label`、`create_edge_label`;`v1` 中真实 `apply` 仍禁用 |
-| `mutate_graph_properties_tool` | 对单个精确点或边追加/淘汰属性;两种操作都必须经过 `dry_run -> plan_hash -> confirm`,目标变更时拒绝执行 |
-| `execute_gremlin_write_tool` | 直接执行 Gremlin 写语句;默认禁用,仅 `HUGEGRAPH_MCP_ADMIN_MODE=true` 且 `HUGEGRAPH_MCP_READONLY=false` 时可用 |
-| `refresh_vid_embeddings_tool` | 刷新 VID embeddings,会改变索引状态;默认禁用,仅 `HUGEGRAPH_MCP_ADMIN_MODE=true` 且 `HUGEGRAPH_MCP_READONLY=false` 时可用 |
-
-Schema apply 使用闭合的操作字段契约。PropertyKey 支持
-`data_type`、`cardinality`、`aggregate_type`、`user_data`;VertexLabel 支持
-`id_strategy`、`properties`、`primary_keys`、`nullable_keys`、`index_labels`、
-`enable_label_index`、`user_data`;EdgeLabel 支持 `source_label`、`target_label`、
-`properties`、`nullable_keys`、`sort_keys`、`frequency`、`enable_label_index`、
-`user_data`。未列出的字段(包括 `ttl*`)会在签发 plan 前的校验阶段拒绝。
-支持的字段会转发到 HugeGraph,并通过 live schema 回读核对。
+失败响应设置 `ok=false`。调用方必须遵守稳定的 `error.type` 和 `error.retryable`。
+## 写入安全合同
-
-
-## 写入安全链
-
-所有普通用户可触达的写操作都必须经过以下安全链。上文所述仅管理员可用的 `execute_gremlin_write_tool` break-glass 例外不经过此链:
+规范写入流程仅使用服务端签发的 `plan_id`:
```text
-dry_run=true
- -> 用户/agent 审查 preview、warnings、matched_count、mutation_summary
- -> 记录 plan_hash、nonce、expires_at
- -> dry_run=false + confirm=true + 原始 payload + plan_hash + nonce + expires_at
- -> MCP 重新校验目标、权限、schema、payload digest 和过期时间
- -> 执行写入
- -> 返回写入/删除结果和失败明细
+结构化 dry-run
+ -> 审查具体目标、警告和变更摘要
+ -> 获取持久化不可变 plan_id
+ -> confirm_write_tool(plan_id)
+ -> get_write_status_tool(plan_id)
+ -> 必要时 reconcile_write_tool(plan_id)
```
-`plan_hash` 不是简单的 payload 哈希。它至少绑定:
-
-- 工具名
-- 操作 mode
-- graph url
-- graph name
-- graph space
-- MCP readonly 状态
-- 当前 schema hash
-- normalized payload digest
-- nonce
-- expiry
-
-confirm 阶段必须全量重验。dry-run 结果过期、目标图变化、schema 变化、payload 变化或权限变化时,confirm 必须失败并要求重新 dry-run。
-
-### 导入语义
-
-`import_graph_data_tool(mode="ingest")` 是 `v2_core` 与 `v1` 兼容工具集共用的结构化导入路径。它使用本地 schema 校验、dry-run/hash/confirm 和 `manage_graph_data()` 的 direct Gremlin 写入;不会调用 HugeGraph-AI `/graph-import` HTTP 路径。legacy/internal 的 AI-backed 函数命名为 `ingest_graph_data_via_ai()`。本 PR 保留双写入内核:公开 MCP 导入走本地 Gremlin,`ingest_graph_data_via_ai()` 仍是遗留的 HugeGraph-AI `/graph-import` 路径,本卡不关闭。
-
-schema 与 graph-data dry-run 共用硬上限:`MAX_OPERATIONS = 200`、`MAX_PAYLOAD_BYTES = 1048576`(1 MiB)。超限在生成 `plan_hash` 之前返回 `VALIDATION_ERROR`。本版本不提供可配置上限。
-
-`import_graph_data_tool(mode="ingest")` 执行创建时返回三类状态:
-
-- `success`:全部写入成功
-- `partial` / `degraded`:部分写入成功,部分失败或结果不可完全确认
-- `error`:写入失败
-
-响应需要包含已写数量、失败明细和可补偿建议,避免出现“半写半崩但不可追踪”。
+确认调用不接收原始 payload,持久化计划是唯一执行依据。每个操作都有持久化回执;再次提交已经完成的 `plan_id` 时返回已记录结果,不会再次写入。
-#### 边端点契约
+`APPLIED` 表示全部操作均已证明写入成功。`PARTIAL` 表示至少一个操作已写入,但整个 workflow 未完全成功。`UNKNOWN` 表示服务无法证明某个操作是否已经提交;调用方必须查询状态并执行 reconcile,禁止盲目重复写入。
-边端点同时支持 object 和 scalar 写法:
+旧的 `plan_hash`、`nonce`、`expires_at` locator 只在 plan-ID 合同引入后的首个兼容版本中保留于 legacy 写入入口。这三个字段必须同时提供,不得与 `plan_id` 混用,并会返回 `LEGACY_CONFIRMATION_DEPRECATED` 警告。新集成必须调用 `confirm_write_tool(plan_id)`。
-```text
-object source/target -> 原样透传
- {"id": "1:Alice"} -> 按 HugeGraph vertex id 匹配
- {"name": "Alice"} -> 按完整主键匹配;任意属性匹配不属于 public graph_data 契约
-
-scalar source/target -> 如果 live schema 中该端点 label 恰好是单主键,
- 优先按该主键匹配;否则回退为 {"id": value}
-
-outV / inV / payload 中显式 vertex id -> 始终表示 HugeGraph vertex id,
- 不做主键改写
-```
+内置 SQLite plan store 只适用于单个可写 MCP 实例。多实例部署前必须提供共享事务存储;当前使用 SQLite 且 `HUGEGRAPH_MCP_WRITE_INSTANCE_COUNT` 大于一时会 fail closed。
-scalar 端点是 same-payload import 的便捷写法,但在单主键 live schema 下会按主键解析,可能匹配图中已存在的顶点;它不只在当前 payload 内查找。因此 edge-only 或连接 payload 外已有顶点的输入是合法的,前提是 dry-run 的 live match 能把每个端点唯一解析到一个顶点。
+### 操作边界
-### 删除语义
+- 可确认的 schema plan 只能包含一个 `create_property_key`、`create_vertex_label` 或 `create_edge_label`。成功状态为 `APPLIED`。索引创建、append/eliminate 和 drop 不在 apply 范围内。
+- 图导入始终仅支持预览,因为 HugeGraph 1.7.0 的原子 create-if-absent 能力尚未得到验证。预览返回 `confirmable=false`、`preview_only=true`,不签发 `plan_id`;确认返回 `FEATURE_DISABLED` 且不写入。
+- 属性变更仅支持预览。HugeGraph 1.7.0 没有原子 compare-and-set 属性更新,确认请求返回 `FEATURE_DISABLED`。
+- 孤立点删除仅支持预览。Docker 并发测试已证明 HugeGraph 1.7.0 无法原子保证“仅在没有关联边时删除”;确认请求返回 `FEATURE_DISABLED`。请显式删除关联边,再通过独立受控维护路径处理顶点。
+- 精确边删除可确认,因为计划绑定了具体 edge ID。
-`delete_graph_data_tool` 的删除是受控删除:
+Schema 和图数据 dry-run 在操作数超过 200 或 payload 超过 1 MiB 时会拒绝返回可用预览或计划。
-- dry-run 阶段必须查出将被删除的具体对象
-- confirm 阶段必须重新匹配并校验目标一致
-- 删除后必须反查确认目标不存在
-- 顶点有关联边时默认拒绝删除
+### Raw Gremlin 边界
-因此,删除一个有关联边的点时,应先 dry-run 并删除相关边,再 dry-run 并删除该点。
+所有公开 Raw Gremlin 执行路径均被禁用,包括 `execute_gremlin_read_tool`、`execute_gremlin_write_tool` 和 `generate_gremlin_tool(execute=true)`。admin mode 不能绕过此限制。只有部署环境证明服务端 evaluation/wait timeout、服务端结果条数上限、客户端流式字节上限以及只读 principal 全部可用后,才可开放 Raw Gremlin 执行。响应完整落入内存后的条数或字节检查只是输出保护,不构成硬资源预算。
-## 配置说明
+结构化读取与 `generate_gremlin_tool(execute=false)` 仍可使用。
-所有配置通过环境变量读取。
+## 配置
-| 变量 | 默认值 | 说明 |
-|------|--------|------|
-| `HUGEGRAPH_URL` | `http://127.0.0.1:8080` | HugeGraph Server 地址 |
-| `HUGEGRAPH_GRAPH_PATH` | `DEFAULT/hugegraph` | 图路径,格式为 `GRAPH_SPACE/GRAPH_NAME` |
-| `HUGEGRAPH_GRAPHSPACE` | 未设置 | 单独覆盖 graph space |
-| `HUGEGRAPH_GRAPH` | 未设置 | 单独覆盖 graph name |
+| 环境变量 | 默认值 | 说明 |
+|----------|--------|------|
+| `HUGEGRAPH_URL` | `http://127.0.0.1:8080` | HugeGraph Server URL |
+| `HUGEGRAPH_GRAPH_PATH` | `DEFAULT/hugegraph` | `GRAPH_SPACE/GRAPH_NAME` |
+| `HUGEGRAPH_GRAPHSPACE`、`HUGEGRAPH_GRAPH` | 未设置 | 显式值会覆盖 `HUGEGRAPH_GRAPH_PATH` |
| `HUGEGRAPH_USER` | `admin` | HugeGraph 用户名 |
-| `HUGEGRAPH_PASSWORD` | `""` | HugeGraph 密码 |
-| `HUGEGRAPH_MCP_TOOLSET` | `v2_core` | 对外工具契约:`v1` 为 10 工具兼容模式,`v2_core` 为 13 工具默认模式 |
-| `HUGEGRAPH_MCP_READONLY` | `true` | 是否启用只读模式 |
+| `HUGEGRAPH_PASSWORD` | 空 | HugeGraph 密码 |
+| `HUGEGRAPH_MCP_TOOLSET` | `v2_core` | `v1` 或 `v2_core`;非法值回落到 `v1` |
+| `HUGEGRAPH_MCP_READONLY` | `true` | 为 true 时禁止全部受控写入 |
| `HUGEGRAPH_MCP_ALLOW_AI` | `false` | 是否允许调用 HugeGraph-AI |
-| `HUGEGRAPH_MCP_ADMIN_MODE` | `false` | 是否启用管理/调试工具 |
-| `HUGEGRAPH_AI_URL` | `http://127.0.0.1:8001` | HugeGraph-AI 地址 |
-| `HUGEGRAPH_AI_TOKEN` | 未设置 | HugeGraph-AI 开启认证时使用的 Bearer Token;公开 MCP 工具通过环境变量配置(内部 HTTP 调用方可按请求覆盖) |
-| `HUGEGRAPH_AI_GRAPH_URL` | 未设置 | AI 侧使用的图地址,未设置时使用 `HUGEGRAPH_URL` |
-| `HUGEGRAPH_MCP_TIMEOUT_SECONDS` | `30` | AI 调用超时时间 |
-| `HUGEGRAPH_MCP_MAX_REPEAT_TIMES` | `10` | `repeat().times(n)` 只读成本 warning 的建议最大值 |
-| `HUGEGRAPH_MCP_STATE_DIR` | `$XDG_STATE_HOME/hugegraph-mcp`;未设置 `XDG_STATE_HOME` 时为 `~/.local/state/hugegraph-mcp` | 持久化一次性确认账本的本地状态目录 |
-
-`HUGEGRAPH_MCP_TIMEOUT_SECONDS` 仅作用于 HugeGraph-AI HTTP 调用,不作用于 PyHugeClient 的 Gremlin 查询。只读 Gremlin 的成本边界由 read cost guard 以非阻塞 warning 形式提示,包括裸全图扫描、`repeat()` 无 `times()` 上限、以及 `path` / `group` / `profile` 无 `limit` 或 `range`。
-
-Boolean 配置只接受 `1`、`true`、`yes`、`on` 和 `0`、`false`、`no`、`off`,忽略大小写和首尾空白。空值或非法值会 fail closed:`HUGEGRAPH_MCP_READONLY` 保持开启,`HUGEGRAPH_MCP_ALLOW_AI` 和 `HUGEGRAPH_MCP_ADMIN_MODE` 保持关闭。
-
-确认账本记录服务端签发的 dry-run 计划和已消费 nonce 的摘要。confirm 只接受匹配的服务端签发计划,执行服务端 10 分钟最大 TTL,并通过原子消费保证一个写计划在本地进程重启后以及共享同一状态目录的多个 worker 之间最多使用一次。POSIX 平台上,HugeGraph MCP 将状态目录权限限制为 `0700`,账本数据库权限限制为 `0600`。状态无法安全持久化时,写入会在执行前被阻断。
-
-推荐默认安全姿态:
-
-- `HUGEGRAPH_MCP_READONLY=true`
-- `HUGEGRAPH_MCP_ALLOW_AI=false`
-- `HUGEGRAPH_MCP_ADMIN_MODE=false`
-
-常见组合:
-
-| 场景 | 配置 |
-|------|------|
-| 只读图查询 | `HUGEGRAPH_MCP_READONLY=true`,`HUGEGRAPH_MCP_ALLOW_AI=false` |
-| AI 生成只读 Gremlin / 文本抽取 | `HUGEGRAPH_MCP_READONLY=true`,`HUGEGRAPH_MCP_ALLOW_AI=true` |
-| 允许受控导入和删除 | `HUGEGRAPH_MCP_READONLY=false`,按需设置 `HUGEGRAPH_MCP_ALLOW_AI=true` |
-| 管理/调试 | `HUGEGRAPH_MCP_READONLY=false`,`HUGEGRAPH_MCP_ADMIN_MODE=true` |
+| `HUGEGRAPH_MCP_ADMIN_MODE` | `false` | 是否启用符合条件的管理/调试工具 |
+| `HUGEGRAPH_AI_URL` | `http://127.0.0.1:8001` | HugeGraph-AI URL |
+| `HUGEGRAPH_AI_TOKEN` | 未设置 | 可选的 HugeGraph-AI bearer token |
+| `HUGEGRAPH_AI_GRAPH_URL` | 未设置 | 提供给 HugeGraph-AI 的图 URL;默认使用 `HUGEGRAPH_URL` |
+| `HUGEGRAPH_CONNECT_TIMEOUT_SECONDS` | `0.5` | HugeGraph 连接超时;范围 `0.001..86400` |
+| `HUGEGRAPH_READ_TIMEOUT_SECONDS` | `15` | 结构化 HugeGraph 读取超时;范围 `0.001..86400` |
+| `HUGEGRAPH_WRITE_TIMEOUT_SECONDS` | `15` | HugeGraph 数据与 schema 写入超时;范围 `0.001..86400` |
+| `HUGEGRAPH_AI_TIMEOUT_SECONDS` | `30` | HugeGraph-AI HTTP 超时;范围 `1..86400` |
+| `HUGEGRAPH_MCP_TIMEOUT_SECONDS` | `30` | AI 超时的废弃兼容配置 |
+| `HUGEGRAPH_MCP_MAX_RESULT_ITEMS` | `100` | 响应完整落入内存后的条数保护;范围 `1..1000000` |
+| `HUGEGRAPH_MCP_MAX_RESULT_BYTES` | `1048576` | 响应完整落入内存后的字节保护;范围 `1..1073741824` |
+| `HUGEGRAPH_MCP_PLAN_STORE` | `sqlite` | 持久化 plan store;当前仅支持 `sqlite` |
+| `HUGEGRAPH_MCP_WRITE_INSTANCE_COUNT` | `1` | 可写 MCP 实例数;SQLite 要求精确为一 |
+| `HUGEGRAPH_MCP_STATE_DIR` | `$XDG_STATE_HOME/hugegraph-mcp` 或 `~/.local/state/hugegraph-mcp` | plan、operation 与 receipt 数据库目录 |
+
+布尔值不区分大小写,支持 `1/true/yes/on` 和 `0/false/no/off`。非法值采用安全默认值;非法、非有限或越界的数值采用文档默认值。
+
+推荐默认配置为 `HUGEGRAPH_MCP_READONLY=true`、`HUGEGRAPH_MCP_ALLOW_AI=false`、`HUGEGRAPH_MCP_ADMIN_MODE=false`。
## License
diff --git a/hugegraph-mcp/docs/p0a-integration-checklist.md b/hugegraph-mcp/docs/p0a-integration-checklist.md
index 62b3f1a5..1c50c290 100644
--- a/hugegraph-mcp/docs/p0a-integration-checklist.md
+++ b/hugegraph-mcp/docs/p0a-integration-checklist.md
@@ -1,131 +1,71 @@
# P0a Integration Checklist
-This checklist is for the next person running P0a integration validation against a real HugeGraph Server. It expands the six-step path from the P0a design into concrete MCP tool calls and expected responses.
+Use this checklist to validate the current write-safety contract against a disposable HugeGraph 1.7.0 Docker instance. Never use a production graph.
-Use a disposable graph. Do not run this checklist against a production graph.
+## 1. Start an Isolated Server
-## Prerequisites
-
-### Start HugeGraph Server
-
-Use the repository's existing Docker path:
+Run from `/Users/uleng/Code` (replace the checkout path if needed):
```bash
-cd <YOUR_PROJECT_PATH>
-cp docker/env.template docker/.env
-sed -i.bak "s|PROJECT_PATH=path_to_project|PROJECT_PATH=<YOUR_PROJECT_PATH>|" docker/.env
-cd docker
-docker compose -f docker-compose-network.yml up -d --wait hugegraph-server
-curl -fsS http://127.0.0.1:8080/versions
-```
-
-`docker-compose-network.yml` pins HugeGraph Server 1.7.0 and also defines the RAG service, but P0a validation below only needs HugeGraph Server. HugeGraph MCP requires HugeGraph Server >= 1.7.0 because the default graphspace route is `DEFAULT/<graph>`. The `--wait` option prevents the checklist from continuing before the server health check passes.
-
-### Configure MCP
+docker pull hugegraph/hugegraph:1.7.0
+docker run --rm -d --name hg-p0a-check -p 127.0.0.1:18080:8080 hugegraph/hugegraph:1.7.0
+until curl -fsS http://127.0.0.1:18080/versions >/dev/null; do sleep 1; done
-Set these variables in the shell or MCP client process that launches `hugegraph-mcp`:
-
-```bash
-cd <YOUR_PROJECT_PATH>/hugegraph-mcp
-export HUGEGRAPH_URL=http://127.0.0.1:8080
-export HUGEGRAPH_GRAPHSPACE=DEFAULT
-export YOUR_GRAPH_NAME=p0a_check_20260705
-export HUGEGRAPH_GRAPH_PATH="$HUGEGRAPH_GRAPHSPACE/$YOUR_GRAPH_NAME"
+export HUGEGRAPH_URL=http://127.0.0.1:18080
+export HUGEGRAPH_GRAPH_PATH=DEFAULT/hugegraph
export HUGEGRAPH_USER=admin
export HUGEGRAPH_PASSWORD=admin
export HUGEGRAPH_MCP_TOOLSET=v2_core
export HUGEGRAPH_MCP_READONLY=false
export HUGEGRAPH_MCP_ALLOW_AI=false
export HUGEGRAPH_MCP_ADMIN_MODE=false
-```
-
-Use a fresh `$YOUR_GRAPH_NAME`, for example `p0a_check_20260705`. First list
-the graphs in the target graphspace and verify that the graph exists:
+export HUGEGRAPH_MCP_PLAN_STORE=sqlite
+export HUGEGRAPH_MCP_WRITE_INSTANCE_COUNT=1
+export HUGEGRAPH_MCP_STATE_DIR=/tmp/hg-p0a-plan-store
+export HUGEGRAPH_CONNECT_TIMEOUT_SECONDS=0.5
+export HUGEGRAPH_READ_TIMEOUT_SECONDS=15
+export HUGEGRAPH_WRITE_TIMEOUT_SECONDS=15
-```bash
-curl -fsS -u "$HUGEGRAPH_USER:$HUGEGRAPH_PASSWORD" \
- "$HUGEGRAPH_URL/graphspaces/$HUGEGRAPH_GRAPHSPACE/graphs"
+export PYTHONPATH=/Users/uleng/Code/hugegraph-ai-pr73-mcp/hugegraph-mcp:/Users/uleng/Code/hugegraph-ai-pr73-mcp/hugegraph-python-client/src
+/Users/uleng/Code/hugegraph-ai-pr73-mcp/.venv/bin/python -m hugegraph_mcp.server
```
-If the graph is missing, create it through the HugeGraph REST API before
-continuing. For a RocksDB graph on a server version that supports dynamic graph
-creation:
-
-```bash
-curl -fsS -u "$HUGEGRAPH_USER:$HUGEGRAPH_PASSWORD" \
- -H 'Content-Type: application/json' \
- -X POST \
- -d "{\"gremlin.graph\":\"org.apache.hugegraph.auth.HugeFactoryAuthProxy\",\"backend\":\"rocksdb\",\"serializer\":\"binary\",\"store\":\"$YOUR_GRAPH_NAME\"}" \
- "$HUGEGRAPH_URL/graphspaces/$HUGEGRAPH_GRAPHSPACE/graphs/$YOUR_GRAPH_NAME"
-```
+The launch command deliberately uses the checkout's root virtual environment and
+binds both source trees through `PYTHONPATH`. Do not replace it with
+`uv run --project .../hugegraph-mcp`: resolving that standalone subproject can
+request the unavailable package-index release `hugegraph-python-client==0.1.1`
+instead of loading the sibling client checkout.
-HugeGraph 1.7.0 has a known dynamic-graph-creation limitation; use the
-pre-created default graph or a server version containing the fix when that
-endpoint is unavailable. Never run the checklist against a production graph.
+Use a second MCP client process for the calls below. Replace `<RUN_ID>` with one unique suffix and preserve exact IDs returned by the server. Each JSON block is the `tools/call` name and arguments payload.
-Start the MCP server with the same environment:
+Clean up after validation:
```bash
-uv run hugegraph-mcp
+docker stop hg-p0a-check
+rm -rf -- /tmp/hg-p0a-plan-store
```
-Confirmed write plans are single-use. The server persists only a SHA-256 digest
-of each nonce and atomically consumes it before the first write side effect. A
-second call with the same confirmation returns `PLAN_ALREADY_USED`, including
-after an execution timeout, failure, or partial apply. Inspect the target state
-and run a new dry-run instead of retrying an already submitted confirmation.
-
-The request examples below show MCP `tools/call` payloads. Paste the `name` and `arguments` into your MCP client. Replace every `<RUN_ID>` with one short unique suffix, for example `20260705a`, and keep the same suffix through all steps. Replace `<EXPIRES_AT_...>` placeholders with the numeric `expires_at` value from the dry-run response, not a quoted string.
-
-## Step 1: Inspect Tool Contract
-
-Safety decision verified: `inspect_graph_tool` must expose the runtime contract version and confirm that this server is running the `v2_core` toolset before any write path is tested.
-
-Call:
+## 2. Inspect the Contract
```json
{
"name": "inspect_graph_tool",
- "arguments": {
- "include_raw_schema": false
- }
-}
-```
-
-Expected key fields:
-
-```json
-{
- "ok": true,
- "data": {
- "graph": "<YOUR_GRAPH_NAME>",
- "graphspace": "DEFAULT",
- "hugegraph_server_status": "available",
- "readonly": false,
- "mcp_tool_contract_version": "2.0",
- "toolset": "v2_core"
- },
- "meta": {
- "readonly": false,
- "mcp_tool_contract_version": "2.0",
- "toolset": "v2_core"
- }
+ "arguments": {"include_raw_schema": false, "include_counts": false}
}
```
-Continue only when `data.toolset` is `v2_core` and `data.readonly` is `false`. If `hugegraph_ai_status` is `unavailable`, ignore it for this P0a checklist. Default inspect leaves `vertex_count` and `edge_count` as `null`; pass `include_counts=true` only when a real count is required.
+Require `ok=true`, `data.readonly=false`, `data.toolset="v2_core"`, and `data.mcp_tool_contract_version="2.0"`. Confirm that the MCP client lists all 16 v2 tools, including `confirm_write_tool`, `get_write_status_tool`, and `reconcile_write_tool`.
-## Step 2: Create Minimal Schema Through Dry-Run And Confirm
+## 3. Create Schema One Object Per Plan
-Safety decision verified: `apply_schema_tool(mode="apply")` is unlocked only for P0a create operations, and real schema writes must use the `dry_run -> plan_hash -> confirm` chain. The confirm step also verifies by post-reading live schema.
+A confirmable schema plan contains exactly one create operation. Repeat the dry-run and confirmation sequence below in dependency order:
-Schema create operations use a closed field contract. `enable_label_index`,
-`index_labels`, and `user_data` are forwarded and checked when supplied;
-unimplemented fields such as `ttl*` must fail validation before a plan is
-issued. Keep any optional fields identical between the dry-run and confirm
-requests.
+1. `create_property_key` for `p0a_name_<RUN_ID>`.
+2. `create_property_key` for `p0a_note_<RUN_ID>`.
+3. `create_vertex_label` for `p0a_person_<RUN_ID>`.
+4. `create_edge_label` for `p0a_knows_<RUN_ID>`.
-Dry-run call:
+Example dry-run for the first object:
```json
{
@@ -138,224 +78,80 @@ Dry-run call:
"name": "p0a_name_<RUN_ID>",
"data_type": "TEXT",
"cardinality": "SINGLE"
- },
- {
- "type": "create_property_key",
- "name": "p0a_note_<RUN_ID>",
- "data_type": "TEXT",
- "cardinality": "SINGLE"
- },
- {
- "type": "create_vertex_label",
- "name": "p0a_person_<RUN_ID>",
- "properties": ["p0a_name_<RUN_ID>", "p0a_note_<RUN_ID>"],
- "primary_keys": ["p0a_name_<RUN_ID>"],
- "nullable_keys": ["p0a_note_<RUN_ID>"]
- },
- {
- "type": "create_edge_label",
- "name": "p0a_knows_<RUN_ID>",
- "source_label": "p0a_person_<RUN_ID>",
- "target_label": "p0a_person_<RUN_ID>"
}
]
}
}
```
-Expected dry-run fields:
+Require `ok=true`, `data.valid=true`, `data.confirmable=true`, `data.status="ISSUED"`, and a non-empty `data.plan_id`. Confirm only the returned ID:
```json
{
- "ok": true,
- "data": {
- "valid": true,
- "confirmable": true,
- "plan_hash": "<32 hex chars>",
- "plan_context": {
- "nonce": "<nonce>",
- "expires_at": 1780000000,
- "graph_name": "<YOUR_GRAPH_NAME>",
- "graphspace": "DEFAULT",
- "readonly": false
- },
- "mutation_summary": "Schema operations planned: create_edge_label=1, create_property_key=2, create_vertex_label=1"
- }
+ "name": "confirm_write_tool",
+ "arguments": {"plan_id": "<PLAN_ID>"}
}
```
-Confirm call. Copy `plan_hash`, `plan_context.nonce`, and `plan_context.expires_at` exactly from the dry-run response:
+Require `ok=true` and `data.status="APPLIED"`. Query its durable result:
```json
{
- "name": "apply_schema_tool",
- "arguments": {
- "mode": "apply",
- "operations": [
- {
- "type": "create_property_key",
- "name": "p0a_name_<RUN_ID>",
- "data_type": "TEXT",
- "cardinality": "SINGLE"
- },
- {
- "type": "create_property_key",
- "name": "p0a_note_<RUN_ID>",
- "data_type": "TEXT",
- "cardinality": "SINGLE"
- },
- {
- "type": "create_vertex_label",
- "name": "p0a_person_<RUN_ID>",
- "properties": ["p0a_name_<RUN_ID>", "p0a_note_<RUN_ID>"],
- "primary_keys": ["p0a_name_<RUN_ID>"],
- "nullable_keys": ["p0a_note_<RUN_ID>"]
- },
- {
- "type": "create_edge_label",
- "name": "p0a_knows_<RUN_ID>",
- "source_label": "p0a_person_<RUN_ID>",
- "target_label": "p0a_person_<RUN_ID>"
- }
- ],
- "confirm": true,
- "plan_hash": "<PLAN_HASH_FROM_DRY_RUN>",
- "nonce": "<NONCE_FROM_DRY_RUN>",
- "expires_at": <EXPIRES_AT_FROM_DRY_RUN>
- }
+ "name": "get_write_status_tool",
+ "arguments": {"plan_id": "<PLAN_ID>"}
}
```
-Expected confirm fields:
+Require `data.status="APPLIED"`. Repeating `confirm_write_tool` with the same ID must return the recorded terminal result without creating the object twice.
+
+Use the same sequence for the remaining schema objects. Their operation bodies are:
```json
-{
- "ok": true,
- "data": {
- "status": "applied",
- "valid": true,
- "applied_operations": [
- {"type": "create_property_key", "name": "p0a_name_<RUN_ID>"},
- {"type": "create_property_key", "name": "p0a_note_<RUN_ID>"},
- {"type": "create_vertex_label", "name": "p0a_person_<RUN_ID>"},
- {"type": "create_edge_label", "name": "p0a_knows_<RUN_ID>"}
- ],
- "schema_summary": {
- "propertykeys": [
- {"name": "p0a_name_<RUN_ID>"},
- {"name": "p0a_note_<RUN_ID>"}
- ],
- "vertexlabels": [
- {"name": "p0a_person_<RUN_ID>"}
- ],
- "edgelabels": [
- {"name": "p0a_knows_<RUN_ID>"}
- ]
- }
- }
-}
+{"type":"create_property_key","name":"p0a_note_<RUN_ID>","data_type":"TEXT","cardinality":"SINGLE"}
```
-Verify with `inspect_schema_tool`:
-
```json
-{
- "name": "inspect_schema_tool",
- "arguments": {
- "include_raw_schema": false,
- "include_relations": true,
- "include_index_labels": true,
- "filter_kind": "vertex_label",
- "filter_name": "p0a_person_<RUN_ID>"
- }
-}
+{"type":"create_vertex_label","name":"p0a_person_<RUN_ID>","properties":["p0a_name_<RUN_ID>","p0a_note_<RUN_ID>"],"primary_keys":["p0a_name_<RUN_ID>"],"nullable_keys":["p0a_note_<RUN_ID>"]}
```
-Expected key fields:
-
```json
-{
- "ok": true,
- "data": {
- "filtered": {
- "name": "p0a_person_<RUN_ID>",
- "properties": ["p0a_name_<RUN_ID>", "p0a_note_<RUN_ID>"],
- "primary_keys": ["p0a_name_<RUN_ID>"],
- "nullable_keys": ["p0a_note_<RUN_ID>"]
- }
- }
-}
+{"type":"create_edge_label","name":"p0a_knows_<RUN_ID>","source_label":"p0a_person_<RUN_ID>","target_label":"p0a_person_<RUN_ID>"}
```
-Do not add `create_index_label` to this step. P0a apply intentionally rejects index create and rebuild work.
-
-## Step 3: Import Two Vertices And One Edge
-
-Safety decision verified: `import_graph_data_tool(mode="ingest")` remains the structured graph-data write path, and it must keep the same `dry_run -> plan_hash -> confirm` safety chain before writing data.
-
-Dry-run call:
+Verify the final vertex label:
```json
{
- "name": "import_graph_data_tool",
+ "name": "inspect_schema_tool",
"arguments": {
- "mode": "ingest",
- "graph_data": {
- "vertices": [
- {
- "label": "p0a_person_<RUN_ID>",
- "properties": {
- "p0a_name_<RUN_ID>": "Alice"
- }
- },
- {
- "label": "p0a_person_<RUN_ID>",
- "properties": {
- "p0a_name_<RUN_ID>": "Bob"
- }
- }
- ],
- "edges": [
- {
- "label": "p0a_knows_<RUN_ID>",
- "source_label": "p0a_person_<RUN_ID>",
- "source": {
- "p0a_name_<RUN_ID>": "Alice"
- },
- "target_label": "p0a_person_<RUN_ID>",
- "target": {
- "p0a_name_<RUN_ID>": "Bob"
- }
- }
- ]
- }
+ "filter_kind": "vertex_label",
+ "filter_name": "p0a_person_<RUN_ID>",
+ "include_relations": true,
+ "include_index_labels": true
}
}
```
-Expected dry-run fields:
+Also submit a dry-run containing two schema operations:
```json
{
- "ok": true,
- "data": {
- "valid": true,
- "confirmable": true,
- "plan_hash": "<32 hex chars>",
- "plan_context": {
- "nonce": "<nonce>",
- "expires_at": 1780000000,
- "readonly": false
- },
- "mutation_summary": {
- "create_vertex": 2,
- "create_edge": 1
- }
+ "name": "apply_schema_tool",
+ "arguments": {
+ "mode": "dry_run",
+ "operations": [
+ {"type":"create_property_key","name":"p0a_extra_a_<RUN_ID>","data_type":"TEXT","cardinality":"SINGLE"},
+ {"type":"create_property_key","name":"p0a_extra_b_<RUN_ID>","data_type":"TEXT","cardinality":"SINGLE"}
+ ]
}
}
```
-Confirm call. Copy `plan_hash`, `nonce`, and `expires_at` exactly from the dry-run response:
+Require `data.valid=false` and an error explaining that a confirmable schema plan must contain exactly one create operation.
+
+## 4. Verify Import Is Preview-Only
+
+Dry-run:
```json
{
@@ -364,70 +160,65 @@ Confirm call. Copy `plan_hash`, `nonce`, and `expires_at` exactly from the dry-r
"mode": "ingest",
"graph_data": {
"vertices": [
- {
- "label": "p0a_person_<RUN_ID>",
- "properties": {
- "p0a_name_<RUN_ID>": "Alice"
- }
- },
- {
- "label": "p0a_person_<RUN_ID>",
- "properties": {
- "p0a_name_<RUN_ID>": "Bob"
- }
- }
+ {"label":"p0a_person_<RUN_ID>","properties":{"p0a_name_<RUN_ID>":"Alice"}},
+ {"label":"p0a_person_<RUN_ID>","properties":{"p0a_name_<RUN_ID>":"Bob"}}
],
"edges": [
{
- "label": "p0a_knows_<RUN_ID>",
- "source_label": "p0a_person_<RUN_ID>",
- "source": {
- "p0a_name_<RUN_ID>": "Alice"
- },
- "target_label": "p0a_person_<RUN_ID>",
- "target": {
- "p0a_name_<RUN_ID>": "Bob"
- }
+ "label":"p0a_knows_<RUN_ID>",
+ "source_label":"p0a_person_<RUN_ID>",
+ "source":{"p0a_name_<RUN_ID>":"Alice"},
+ "target_label":"p0a_person_<RUN_ID>",
+ "target":{"p0a_name_<RUN_ID>":"Bob"}
}
]
- },
- "dry_run": false,
- "confirm": true,
- "plan_hash": "<PLAN_HASH_FROM_DRY_RUN>",
- "nonce": "<NONCE_FROM_DRY_RUN>",
- "expires_at": <EXPIRES_AT_FROM_DRY_RUN>
- }
-}
-```
-
-Expected confirm fields:
-
-```json
-{
- "ok": true,
- "data": {
- "status": "success",
- "success": true,
- "planned": {
- "create_vertex": 2,
- "create_edge": 1
- },
- "written": {
- "create_vertex": 2,
- "create_edge": 1
- },
- "failed_items": []
+ }
}
}
```
-If this returns `INVALID_GRAPH_DATA` or `SCHEMA_MISMATCH`, verify that Step 2 used the same `<RUN_ID>` and that both vertex payloads include the primary key property `p0a_name_<RUN_ID>`.
+Require `ok=true`, `data.confirmable=false`, `data.preview_only=true`, and no
+`data.plan_id`. The warnings must explain that atomic vertex and edge
+create-if-absent capabilities are not verified for this backend. Preserve the
+returned `plan_hash`, `plan_context.nonce`, and numeric
+`plan_context.expires_at`, then repeat the same ingest request with
+`dry_run=false`, `confirm=true`, and that complete legacy locator. Require
+`ok=false`, `error.type="FEATURE_DISABLED"`, and verify that neither vertex nor
+edge was created. Do not call `confirm_write_tool` for an import preview because
+there is no canonical plan to confirm.
-## Step 4: Query A Vertex By ID
+The remaining checks need disposable fixtures but must not weaken the public MCP
+import boundary. Seed them directly through the sibling Python client after the
+preview-only assertion:
-Safety decision verified: `query_graph_data_tool` must provide typed bounded reads without falling back to unbounded Gremlin scans. This step proves that exact ID lookup works after import.
-
-HugeGraph backend vertex IDs may encode the label and primary key together in a format that varies by server version and configuration. Do not assume a specific format; always read the exact ID string back from a bounded query before using it. If your import response does not show the exact ID, first find Alice with a bounded page query:
+```bash
+export RUN_ID=<RUN_ID>
+PYTHONPATH=/Users/uleng/Code/hugegraph-ai-pr73-mcp/hugegraph-python-client/src \
+ /Users/uleng/Code/hugegraph-ai-pr73-mcp/.venv/bin/python - <<'PY'
+import os
+
+from pyhugegraph.client import PyHugeClient
+
+suffix = os.environ["RUN_ID"]
+name_key = f"p0a_name_{suffix}"
+person_label = f"p0a_person_{suffix}"
+knows_label = f"p0a_knows_{suffix}"
+client = PyHugeClient(
+ url="http://127.0.0.1:18080",
+ graph="hugegraph",
+ graphspace="DEFAULT",
+ user="admin",
+ pwd="admin",
+)
+graph = client.graph()
+alice = graph.addVertex(person_label, {name_key: "Alice"})
+bob = graph.addVertex(person_label, {name_key: "Bob"})
+edge = graph.addEdge(knows_label, alice.id, bob.id, {})
+print({"alice_id": alice.id, "bob_id": bob.id, "edge_id": edge.id})
+PY
+```
+
+Locate Alice and preserve the returned backend ID:
```json
{
@@ -441,73 +232,11 @@ HugeGraph backend vertex IDs may encode the label and primary key together in a
}
```
-Expected page fields:
+Require two vertices and copy Alice's exact `items[].id` value. Do not construct a backend ID from the label or primary key.
-```json
-{
- "ok": true,
- "data": {
- "target": "vertex",
- "operation": "page",
- "items": [
- {
- "id": "<ALICE_VERTEX_ID>",
- "label": "p0a_person_<RUN_ID>",
- "properties": {
- "p0a_name_<RUN_ID>": "Alice"
- }
- }
- ],
- "count": 2,
- "limit": 10
- }
-}
-```
+## 5. Verify Preview-Only Operations
-Copy Alice's `items[].id` into `<ALICE_VERTEX_ID>`, then run the exact lookup:
-
-```json
-{
- "name": "query_graph_data_tool",
- "arguments": {
- "target": "vertex",
- "operation": "get_by_id",
- "id": "<ALICE_VERTEX_ID>"
- }
-}
-```
-
-Expected key fields:
-
-```json
-{
- "ok": true,
- "data": {
- "target": "vertex",
- "operation": "get_by_id",
- "items": [
- {
- "id": "<ALICE_VERTEX_ID>",
- "label": "p0a_person_<RUN_ID>",
- "properties": {
- "p0a_name_<RUN_ID>": "Alice"
- }
- }
- ],
- "count": 1
- }
-}
-```
-
-If you use `operation="condition"` instead, HugeGraph may return `NO_INDEX` because P0a intentionally does not create indexes. That is expected for this checklist and is covered in the troubleshooting table.
-
-## Step 5: Append A Property And Reject A Stale Eliminate Plan
-
-Safety decision verified: `mutate_graph_properties_tool` treats append and eliminate as write operations with the same dry-run/confirm chain. The stale eliminate confirm must be rejected with `TARGET_CHANGED`, proving the target snapshot digest blocks concurrent changes from being confirmed with an old plan.
-
-### 5.1 Append A Property
-
-Dry-run append call:
+### Property mutation
```json
{
@@ -516,44 +245,13 @@ Dry-run append call:
"target": "vertex",
"operation": "append",
"id": "<ALICE_VERTEX_ID>",
- "properties": {
- "p0a_note_<RUN_ID>": "first-note"
- }
- }
-}
-```
-
-Expected dry-run fields:
-
-```json
-{
- "ok": true,
- "data": {
- "status": "planned",
- "confirmable": true,
- "risk_level": "medium",
- "before": {
- "properties": {
- "p0a_name_<RUN_ID>": "Alice"
- }
- },
- "after": {
- "properties": {
- "p0a_name_<RUN_ID>": "Alice",
- "p0a_note_<RUN_ID>": "first-note"
- }
- },
- "plan_hash": "<32 hex chars>",
- "plan_context": {
- "nonce": "<nonce>|ts:<snapshot-prefix>",
- "expires_at": 1780000000,
- "readonly": false
- }
+ "properties": {"p0a_note_<RUN_ID>": "preview-only"},
+ "dry_run": true
}
}
```
-Confirm append:
+Require `ok=true`, `data.confirmable=false`, and a warning that atomic conditional property update is unavailable. Copy the complete legacy locator from the preview only to verify the compatibility gate:
```json
{
@@ -562,273 +260,124 @@ Confirm append:
"target": "vertex",
"operation": "append",
"id": "<ALICE_VERTEX_ID>",
- "properties": {
- "p0a_note_<RUN_ID>": "first-note"
- },
+ "properties": {"p0a_note_<RUN_ID>": "preview-only"},
"dry_run": false,
"confirm": true,
- "plan_hash": "<APPEND_PLAN_HASH>",
- "nonce": "<APPEND_NONCE>",
- "expires_at": <APPEND_EXPIRES_AT>
+ "plan_hash": "<PLAN_HASH_FROM_PREVIEW>",
+ "nonce": "<NONCE_FROM_PREVIEW>",
+ "expires_at": <NUMERIC_EXPIRES_AT_FROM_PREVIEW>
}
}
```
-Expected confirm fields:
+Pass `expires_at` as a JSON number, without quotes. Require `FEATURE_DISABLED` and verify Alice remains unchanged.
-```json
-{
- "ok": true,
- "data": {
- "status": "applied",
- "after": {
- "properties": {
- "p0a_name_<RUN_ID>": "Alice",
- "p0a_note_<RUN_ID>": "first-note"
- }
- }
- }
-}
-```
-
-### 5.2 Prepare An Eliminate Plan
-
-Dry-run eliminate call. Do not confirm it yet:
+### Isolated vertex deletion
```json
{
- "name": "mutate_graph_properties_tool",
+ "name": "delete_graph_data_tool",
"arguments": {
- "target": "vertex",
- "operation": "eliminate",
- "id": "<ALICE_VERTEX_ID>",
- "properties": {
- "p0a_note_<RUN_ID>": "first-note"
- }
- }
-}
-```
-
-Expected dry-run fields:
-
-```json
-{
- "ok": true,
- "data": {
- "status": "planned",
- "risk_level": "high",
- "before": {
- "properties": {
- "p0a_name_<RUN_ID>": "Alice",
- "p0a_note_<RUN_ID>": "first-note"
- }
- },
- "after": {
- "properties": {
- "p0a_name_<RUN_ID>": "Alice"
- }
+ "change_plan": {
+ "operations": [
+ {
+ "op": "delete_vertex",
+ "label": "p0a_person_<RUN_ID>",
+ "match": {"p0a_name_<RUN_ID>": "Alice"},
+ "cascade": false
+ }
+ ]
},
- "plan_hash": "<ELIMINATE_PLAN_HASH>",
- "plan_context": {
- "nonce": "<ELIMINATE_NONCE>",
- "expires_at": 1780000000
- }
+ "dry_run": true
}
}
```
-Save `ELIMINATE_PLAN_HASH`, `ELIMINATE_NONCE`, and `ELIMINATE_EXPIRES_AT`.
+Require `ok=true`, `data.confirmable=false`, `data.preview_only=true`, and no `plan_id`. Confirmation must return `FEATURE_DISABLED`. This is required even when the preview reports zero incident edges: Docker concurrency testing demonstrated that HugeGraph 1.7.0 cannot atomically exclude a concurrently added edge.
-### 5.3 Change The Target Before Confirm
+## 6. Delete an Exact Edge
-Use another append dry-run/confirm to modify Alice after the eliminate dry-run:
+Dry-run an edge delete whose label and endpoint primary keys resolve exactly one edge. The persisted plan binds the resulting edge ID:
```json
{
- "name": "mutate_graph_properties_tool",
+ "name": "delete_graph_data_tool",
"arguments": {
- "target": "vertex",
- "operation": "append",
- "id": "<ALICE_VERTEX_ID>",
- "properties": {
- "p0a_note_<RUN_ID>": "changed-before-eliminate-confirm"
- }
+ "change_plan": {
+ "operations": [
+ {
+ "op":"delete_edge",
+ "label":"p0a_knows_<RUN_ID>",
+ "source_label":"p0a_person_<RUN_ID>",
+ "source_match":{"p0a_name_<RUN_ID>":"Alice"},
+ "target_label":"p0a_person_<RUN_ID>",
+ "target_match":{"p0a_name_<RUN_ID>":"Bob"}
+ }
+ ]
+ },
+ "dry_run": true
}
}
```
-Confirm that second append with the returned `plan_hash`, `nonce`, and `expires_at`:
+Require a `plan_id`, confirm it through `confirm_write_tool`, and require `APPLIED`. A bounded edge page for `p0a_knows_<RUN_ID>` must then return no edge.
-```json
-{
- "name": "mutate_graph_properties_tool",
- "arguments": {
- "target": "vertex",
- "operation": "append",
- "id": "<ALICE_VERTEX_ID>",
- "properties": {
- "p0a_note_<RUN_ID>": "changed-before-eliminate-confirm"
- },
- "dry_run": false,
- "confirm": true,
- "plan_hash": "<SECOND_APPEND_PLAN_HASH>",
- "nonce": "<SECOND_APPEND_NONCE>",
- "expires_at": <SECOND_APPEND_EXPIRES_AT>
- }
-}
-```
+## 7. Verify Failure-State Semantics
-Expected second append result:
+For every submitted plan:
-```json
-{
- "ok": true,
- "data": {
- "status": "applied",
- "after": {
- "properties": {
- "p0a_note_<RUN_ID>": "changed-before-eliminate-confirm"
- }
- }
- }
-}
-```
+- `APPLIED` means all operations are proven applied.
+- `PARTIAL` means at least one operation applied and the workflow did not completely apply. It must never be reported as `REJECTED`.
+- `UNKNOWN` means the commit outcome cannot be proven. Do not repeat the original write or create an equivalent replacement plan.
-### 5.4 Confirm The Old Eliminate Plan
+The public tools intentionally provide no fault-injection switch. Run the automated durable-executor fault suite from `/Users/uleng/Code`:
-Now confirm the old eliminate plan saved in 5.2:
+```bash
+/Users/uleng/Code/hugegraph-ai-pr73-mcp/.venv/bin/pytest \
+ /Users/uleng/Code/hugegraph-ai-pr73-mcp/hugegraph-mcp/tests/test_write_executor_faults.py -q
+```
+
+The suite must cover failures before claim, after claim, before write, after write, and during receipt persistence. When a controlled test produces `PARTIAL` or `UNKNOWN`, call:
```json
-{
- "name": "mutate_graph_properties_tool",
- "arguments": {
- "target": "vertex",
- "operation": "eliminate",
- "id": "<ALICE_VERTEX_ID>",
- "properties": {
- "p0a_note_<RUN_ID>": "first-note"
- },
- "dry_run": false,
- "confirm": true,
- "plan_hash": "<ELIMINATE_PLAN_HASH>",
- "nonce": "<ELIMINATE_NONCE>",
- "expires_at": <ELIMINATE_EXPIRES_AT>
- }
-}
+{"name":"get_write_status_tool","arguments":{"plan_id":"<PLAN_ID>"}}
```
-Expected rejection:
+Then call:
```json
-{
- "ok": false,
- "error": {
- "type": "TARGET_CHANGED",
- "message": "Target changed since dry_run; property mutation was not applied.",
- "source": "mutate_graph_properties_tool"
- },
- "next_actions": [
- "Call query_graph_data_tool to inspect the current target."
- ]
-}
+{"name":"reconcile_write_tool","arguments":{"plan_id":"<PLAN_ID>"}}
```
-This failure is the expected pass condition for stale-plan protection. If this call succeeds, P0a concurrency protection failed.
+Require reconciliation to use only read checks. Resume is allowed only when durable state proves the operation was never dispatched, or when a backend-enforced idempotency/fencing primitive proves replay safety. Reading the expected state after a dispatched request is not enough because the old request may still commit; retain `UNKNOWN` in that case.
-## Step 6: Reject An Unbounded Gremlin Read
+## 8. Verify Legacy and Raw-Gremlin Gates
-Safety decision verified: `execute_gremlin_read_tool(limit_policy="reject_unbounded")` must reject safe-but-unbounded full graph reads instead of silently appending a limit or executing an unbounded scan.
+Legacy compatibility accepts `plan_hash`, `nonce`, and `expires_at` only when all three are supplied together and returns `LEGACY_CONFIRMATION_DEPRECATED`. A partial legacy locator must return `VALIDATION_ERROR`. Canonical lifecycle tools accept only `plan_id`; never mix the two protocols.
-Call:
+Each call below must return `FEATURE_DISABLED`, even if admin mode is enabled:
```json
-{
- "name": "execute_gremlin_read_tool",
- "arguments": {
- "gremlin_query": "g.V()",
- "limit_policy": "reject_unbounded"
- }
-}
+{"name":"execute_gremlin_read_tool","arguments":{"gremlin_query":"g.V().limit(1)"}}
```
-Expected fields:
-
```json
-{
- "ok": false,
- "error": {
- "type": "VALIDATION_ERROR",
- "message": "Gremlin read query is unbounded and limit_policy='reject_unbounded'."
- },
- "warnings": [
- "Unbounded traversal ..."
- ]
-}
+{"name":"execute_gremlin_write_tool","arguments":{"gremlin_query":"g.addV('unsafe')"}}
```
-Confirm that `executed_gremlin` is absent. The query must be rejected before execution.
-
-Run a bounded read to confirm the read path itself still works:
-
```json
-{
- "name": "execute_gremlin_read_tool",
- "arguments": {
- "gremlin_query": "g.V().hasLabel('p0a_person_<RUN_ID>').limit(10)",
- "limit_policy": "reject_unbounded"
- }
-}
+{"name":"generate_gremlin_tool","arguments":{"query":"list one vertex","execute":true}}
```
-Expected bounded-read fields:
+`generate_gremlin_tool(execute=false)` and structured query tools must remain available. The item and byte configuration values are post-materialization output guards and do not satisfy the raw-query hard-budget contract.
-```json
-{
- "ok": true,
- "data": {
- "is_read": true,
- "limit_policy": "reject_unbounded",
- "original_gremlin": "g.V().hasLabel('p0a_person_<RUN_ID>').limit(10)",
- "executed_gremlin": "g.V().hasLabel('p0a_person_<RUN_ID>').limit(10)",
- "total": 2
- }
-}
-```
+## Pass Criteria
-## Troubleshooting
-
-| Error type | Where it appears | Meaning | Action |
-|---|---|---|---|
-| `CONNECTION_FAILED` | `inspect_schema_tool`, `apply_schema_tool`, `import_graph_data_tool`, `mutate_graph_properties_tool`, Gremlin reads | MCP could not reach HugeGraph Server, read schema, or read a target object. | Check `docker compose ps`, `curl http://127.0.0.1:8080/versions`, `HUGEGRAPH_URL`, `HUGEGRAPH_GRAPH_PATH`, `HUGEGRAPH_USER`, and `HUGEGRAPH_PASSWORD`. Retry after the server is healthy. |
-| `READONLY_VIOLATION` | Any confirm call for schema/data/property writes | `HUGEGRAPH_MCP_READONLY=true` at confirm time. Dry-run may be preview-only in readonly mode. | Set `HUGEGRAPH_MCP_READONLY=false`, restart or reconfigure the MCP process, rerun dry-run, then confirm with the new `plan_hash`. |
-| `PLAN_HASH_MISMATCH` | Confirm calls for schema/data/property writes | The submitted payload, graph target, schema hash, readonly state, principal, `nonce`, or `expires_at` no longer matches the dry-run plan. | Do not edit the payload between dry-run and confirm. Rerun dry-run and copy the new `plan_hash`, `nonce`, and `expires_at`. |
-| `PLAN_EXPIRED` | Confirm calls after waiting too long | The dry-run plan expired. Default plan TTL is 10 minutes. | Rerun dry-run and confirm within the returned `expires_at` window. |
-| `PLAN_ALREADY_USED` | A confirm call repeats a nonce that already entered execution | Confirmation nonces are globally single-use. A prior call may have succeeded, failed, timed out, or partially applied. | Inspect the current graph or schema state, then rerun dry-run and use its new `plan_hash`, `nonce`, and `expires_at`. Do not retry the old confirmation. |
-| `TARGET_CHANGED` | Step 5 stale eliminate confirm | The vertex or edge was changed after dry-run and before confirm. | Treat this as a successful stale-plan rejection in Step 5. For real work, rerun `query_graph_data_tool`, review current state, then create a new dry-run plan. |
-| `NO_INDEX` | `query_graph_data_tool(operation="condition")` or Gremlin `has()` filters | HugeGraph requires an index for that property query, and P0a does not create indexes. | Use `get_by_id` or bounded `page` for this checklist. Index create/rebuild belongs to the P0b workflow. |
-| `PARTIAL_APPLY` | `apply_schema_tool(mode="apply")` | At least one schema operation may have been applied before a later operation failed. | Call `inspect_schema_tool`, remove already-applied operations, rerun dry-run for the remaining operations only. Do not blindly retry the original full batch. |
-| `INVALID_GRAPH_DATA` or `SCHEMA_MISMATCH` | Import dry-run or property mutation dry-run | Payload does not match live schema: missing primary key, unknown property, wrong endpoint label, duplicate identity, or property not defined on the target label. | Compare the payload with `inspect_schema_tool`. In this checklist, ensure all names use the same `<RUN_ID>` and that `p0a_note_<RUN_ID>` was included in the vertex label properties. |
-| `VALIDATION_ERROR` | Query parameter validation or Step 6 unbounded Gremlin rejection | Input violates the public tool contract, or the unbounded Gremlin query was rejected by policy. | Fix the input. For Step 6, `VALIDATION_ERROR` is expected for `g.V()` with `reject_unbounded`. |
-
-## Acceptance Conclusion Template
-
-Use this checklist as the final run record:
-
-```text
-P0a integration acceptance
-Graph path: DEFAULT/<YOUR_GRAPH_NAME>
-Run id: <RUN_ID>
-HugeGraph Server URL: <HUGEGRAPH_URL>
-Executed by:
-Date:
-
-[ ] Step 1 passed: inspect_graph_tool returned ok=true, toolset=v2_core, readonly=false.
-[ ] Step 2 passed: apply_schema_tool dry-run returned plan_hash and confirm applied property keys, vertex label, and edge label.
-[ ] Step 3 passed: import_graph_data_tool dry-run returned plan_hash and confirm wrote 2 vertices + 1 edge.
-[ ] Step 4 passed: query_graph_data_tool get_by_id returned Alice by exact vertex id.
-[ ] Step 5 passed: append property applied, and stale eliminate confirm returned TARGET_CHANGED.
-[ ] Step 6 passed: g.V() with reject_unbounded was rejected before execution, while bounded read succeeded.
-
-Overall result: PASS / FAIL
-Notes:
-```
+- Tool contract and count match `v2_core`.
+- Four single-operation schema plans reach `APPLIED` and schema post-reads match.
+- Vertex/edge import remains preview-only, issues no `plan_id`, returns `FEATURE_DISABLED` on confirmation, and creates no graph elements.
+- Property mutation and isolated vertex deletion remain preview-only and return `FEATURE_DISABLED` on confirmation.
+- Exact edge deletion reaches `APPLIED`.
+- Status and reconciliation preserve `PARTIAL` and `UNKNOWN` semantics.
+- Partial legacy locators and every public Raw Gremlin execution path fail closed.
+- Docker container is removed after the run.
diff --git a/hugegraph-mcp/hugegraph_mcp/backend_capabilities.py b/hugegraph-mcp/hugegraph_mcp/backend_capabilities.py
index 6568b03b..09f581f8 100644
--- a/hugegraph-mcp/hugegraph_mcp/backend_capabilities.py
+++ b/hugegraph-mcp/hugegraph_mcp/backend_capabilities.py
@@ -1,7 +1,15 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
"""Evidence-backed HugeGraph backend capability profiles.
diff --git a/hugegraph-mcp/hugegraph_mcp/gremlin_policy.py b/hugegraph-mcp/hugegraph_mcp/gremlin_policy.py
index 6b31651f..d7b1c966 100644
--- a/hugegraph-mcp/hugegraph_mcp/gremlin_policy.py
+++ b/hugegraph-mcp/hugegraph_mcp/gremlin_policy.py
@@ -399,9 +399,7 @@ def _analyze_tokens(lexed: _GremlinLexResult) -> _GremlinTokenAnalysis:
uncertain = True
continue
- if token.kind == "string" and any(
- marker in token.value for marker in _DYNAMIC_MARKERS
- ):
+ if token.kind == "string" and any(marker in token.value for marker in _DYNAMIC_MARKERS):
# GStrings can interpolate arbitrary Groovy expressions. Treat
# them as ambiguous even though their contents are not code tokens.
uncertain = True
@@ -532,16 +530,12 @@ def _extract_method_names(query_without_strings: str) -> list[str]:
return list(_analyze_tokens(lexed).method_names)
-def _has_unsafe_write_steps(
- query_without_strings: str, lowered_methods: list[str]
-) -> bool:
+def _has_unsafe_write_steps(query_without_strings: str, lowered_methods: list[str]) -> bool:
del lowered_methods # The shared lexer is the source of truth.
return _analyze_tokens(_lex_gremlin_query(query_without_strings)).has_write_method
-def _has_dynamic_construction_markers(
- original_query: str, query_without_strings: str
-) -> bool:
+def _has_dynamic_construction_markers(original_query: str, query_without_strings: str) -> bool:
del query_without_strings
# Analyze the original source so interpolation and dynamic member markers
# that occur inside a quoted literal are still visible to this compatibility
@@ -598,10 +592,7 @@ class GremlinPolicy:
classification="unsafe",
reason="Query contains write or mutate operations.",
error_type="UNSAFE_GREMLIN",
- suggestion=(
- "Use execute_gremlin_write for write operations "
- "when write access is enabled."
- ),
+ suggestion=("Use execute_gremlin_write for write operations when write access is enabled."),
)
# classification == "uncertain"
@@ -640,39 +631,27 @@ def gremlin_cost_warnings(gremlin_query: str) -> list[str]:
"""轻量成本边界检查:只产 warning 不阻断。非完整 parser,仅挡明显风险。"""
query_without_strings = _strip_string_literals(gremlin_query)
- methods = [
- method.lower() for method in _extract_method_names(query_without_strings)
- ]
+ methods = [method.lower() for method in _extract_method_names(query_without_strings)]
method_set = set(methods)
warnings: list[str] = []
if method_set.isdisjoint({"limit", "range", "count"}):
- warnings.append(
- "Unbounded traversal: result set is not limited; consider adding "
- ".limit() or .range()."
- )
+ warnings.append("Unbounded traversal: result set is not limited; consider adding .limit() or .range().")
if "repeat" in method_set:
if "times" not in method_set:
- warnings.append(
- "repeat() without times() may recurse without an explicit depth bound."
- )
+ warnings.append("repeat() without times() may recurse without an explicit depth bound.")
else:
max_times = _parse_repeat_threshold()
match = re.search(r"times\(\s*(\d+)\s*\)", query_without_strings)
if match is not None:
depth = int(match.group(1))
if depth > max_times:
- warnings.append(
- f"repeat().times(n) depth {depth} exceeds recommended "
- f"maximum {max_times}."
- )
-
- if any(
- method in method_set for method in ("path", "group", "profile")
- ) and method_set.isdisjoint({"limit", "range"}):
- warnings.append(
- "Heavy step (path/group/profile) without limit/range may be expensive."
- )
+ warnings.append(f"repeat().times(n) depth {depth} exceeds recommended maximum {max_times}.")
+
+ if any(method in method_set for method in ("path", "group", "profile")) and method_set.isdisjoint(
+ {"limit", "range"}
+ ):
+ warnings.append("Heavy step (path/group/profile) without limit/range may be expensive.")
return list(dict.fromkeys(warnings))
diff --git a/hugegraph-mcp/hugegraph_mcp/gremlin_tools.py b/hugegraph-mcp/hugegraph_mcp/gremlin_tools.py
index 0cfd8b95..c803dc66 100644
--- a/hugegraph-mcp/hugegraph_mcp/gremlin_tools.py
+++ b/hugegraph-mcp/hugegraph_mcp/gremlin_tools.py
@@ -303,7 +303,7 @@ def _execute_gremlin_with_error_handling(client, gremlin_query: str, operation_t
or error_json.get("error")
or str(error_json)
)
- except Exception: # noqa: BLE001, S110 - best-effort error-body parsing
+ except Exception:
pass
if detail_message:
@@ -385,7 +385,7 @@ def _execute_gremlin_with_error_handling(client, gremlin_query: str, operation_t
"operation_type": operation_type,
}
- except Exception as e: # noqa: BLE001 - normalize every client failure
+ except Exception as e:
duration_ms = (time.perf_counter() - start) * 1000.0
message = f"Unexpected error: {e!s}"
if _is_no_index_error(message):
diff --git a/hugegraph-mcp/hugegraph_mcp/plan_store.py b/hugegraph-mcp/hugegraph_mcp/plan_store.py
index 42ba3fa7..dca8d801 100644
--- a/hugegraph-mcp/hugegraph_mcp/plan_store.py
+++ b/hugegraph-mcp/hugegraph_mcp/plan_store.py
@@ -1,7 +1,15 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
"""Versioned SQLite persistence for immutable write plans and operations."""
diff --git a/hugegraph-mcp/hugegraph_mcp/reconciler.py b/hugegraph-mcp/hugegraph_mcp/reconciler.py
index 05a33579..40d72de5 100644
--- a/hugegraph-mcp/hugegraph_mcp/reconciler.py
+++ b/hugegraph-mcp/hugegraph_mcp/reconciler.py
@@ -1,7 +1,15 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
"""Read-only reconciliation for ambiguous persisted write outcomes."""
@@ -167,10 +175,10 @@ class WriteReconciler:
DEFAULT_RECONCILE_READERS = ReconcileReaderRegistry()
-from hugegraph_mcp.tools.graph_write_adapter import (
+from hugegraph_mcp.tools.graph_write_adapter import ( # noqa: E402
register_graph_reconcile_readers,
)
-from hugegraph_mcp.tools.schema_write_adapter import (
+from hugegraph_mcp.tools.schema_write_adapter import ( # noqa: E402
register_schema_reconcile_readers,
)
@@ -181,7 +189,7 @@ register_schema_reconcile_readers(DEFAULT_RECONCILE_READERS)
def reconcile_write(plan_id: str) -> dict:
try:
store = plan_store_from_config()
- except Exception as exc: # noqa: BLE001 - public tool must remain enveloped
+ except Exception as exc:
return envelope_err(
ErrorType.SERVER_ERROR,
"The durable plan store is unavailable.",
diff --git a/hugegraph-mcp/hugegraph_mcp/schema_tools.py b/hugegraph-mcp/hugegraph_mcp/schema_tools.py
index 238077be..483c5374 100644
--- a/hugegraph-mcp/hugegraph_mcp/schema_tools.py
+++ b/hugegraph-mcp/hugegraph_mcp/schema_tools.py
@@ -40,21 +40,13 @@ def _simple_schema(schema: dict[str, Any]) -> dict[str, Any]:
if schema.get("vertexlabels"):
mini_schema["vertexlabels"] = []
for vertex in schema["vertexlabels"]:
- new_vertex = {
- key: vertex[key]
- for key in ("id", "name", "properties")
- if key in vertex
- }
+ new_vertex = {key: vertex[key] for key in ("id", "name", "properties") if key in vertex}
mini_schema["vertexlabels"].append(new_vertex)
if schema.get("edgelabels"):
mini_schema["edgelabels"] = []
for edge in schema["edgelabels"]:
- new_edge = {
- key: edge[key]
- for key in ("name", "source_label", "target_label", "properties")
- if key in edge
- }
+ new_edge = {key: edge[key] for key in ("name", "source_label", "target_label", "properties") if key in edge}
mini_schema["edgelabels"].append(new_edge)
return mini_schema
diff --git a/hugegraph-mcp/hugegraph_mcp/server.py b/hugegraph-mcp/hugegraph_mcp/server.py
index 157891d9..679d1ad4 100644
--- a/hugegraph-mcp/hugegraph_mcp/server.py
+++ b/hugegraph-mcp/hugegraph_mcp/server.py
@@ -89,7 +89,7 @@ def _call_public_tool(tool_name: str, func, *args, **kwargs) -> dict[str, Any]:
start = time.perf_counter()
try:
result = func(*args, **kwargs)
- except Exception as exc: # noqa: BLE001 - tool boundary returns an envelope
+ except Exception as exc:
return envelope_err(
ErrorType.FLOW_EXECUTION_FAILED,
f"{tool_name} failed: {exc!s}",
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/extract_graph_data.py b/hugegraph-mcp/hugegraph_mcp/tools/extract_graph_data.py
index 0b1da056..15c88884 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/extract_graph_data.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/extract_graph_data.py
@@ -17,6 +17,9 @@
{vertices: [...], edges: [...]} 图数据,供用户审阅后再导入。
"""
+# The extraction prompt contains intentionally long, copy-pasteable JSON contracts.
+# ruff: noqa: E501
+
import hashlib
import json
from typing import Any
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/generate_gremlin.py b/hugegraph-mcp/hugegraph_mcp/tools/generate_gremlin.py
index 4fabeac4..55b10ffe 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/generate_gremlin.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/generate_gremlin.py
@@ -50,8 +50,7 @@ def generate_gremlin(
{
output_type if isinstance(output_type, str) else repr(output_type)
for output_type in (output_types or [])
- if not isinstance(output_type, str)
- or output_type not in _GENERATION_OUTPUT_TYPES
+ if not isinstance(output_type, str) or output_type not in _GENERATION_OUTPUT_TYPES
}
)
if invalid_output_types:
@@ -105,16 +104,10 @@ def generate_gremlin(
if not requested:
gremlin_candidates.append(ai_data.get("gremlin"))
gremlin = next(
- (
- candidate
- for candidate in gremlin_candidates
- if isinstance(candidate, str) and candidate.strip()
- ),
+ (candidate for candidate in gremlin_candidates if isinstance(candidate, str) and candidate.strip()),
None,
)
- requires_gremlin = not requested or bool(
- requested & {"template_gremlin", "raw_gremlin"}
- )
+ requires_gremlin = not requested or bool(requested & {"template_gremlin", "raw_gremlin"})
if requires_gremlin and gremlin is None:
return envelope_err(
ErrorType.FLOW_EXECUTION_FAILED,
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/graph_data_mapping.py b/hugegraph-mcp/hugegraph_mcp/tools/graph_data_mapping.py
index 3af2a5e4..f57789ff 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/graph_data_mapping.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/graph_data_mapping.py
@@ -178,18 +178,8 @@ def _edge_endpoint_match(
if explicit_endpoint:
primary_key = single_primary_keys.get(endpoint_label)
primary_key_value = primary_key_values.get(identity_key)
- if (
- primary_key_value is None
- and isinstance(fallback_id, str)
- and ":" in fallback_id
- ):
- primary_key_value = primary_key_values.get(
- (endpoint_label, fallback_id.split(":", 1)[1])
- )
+ if primary_key_value is None and isinstance(fallback_id, str) and ":" in fallback_id:
+ primary_key_value = primary_key_values.get((endpoint_label, fallback_id.split(":", 1)[1]))
if primary_key is not None:
- return {
- primary_key: primary_key_value
- if primary_key_value is not None
- else fallback_id
- }
+ return {primary_key: primary_key_value if primary_key_value is not None else fallback_id}
return {"id": fallback_id}
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/graph_write_adapter.py b/hugegraph-mcp/hugegraph_mcp/tools/graph_write_adapter.py
index 4f00176a..9615dfcb 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/graph_write_adapter.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/graph_write_adapter.py
@@ -1,7 +1,15 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
"""Canonical graph write adapters and read-only reconciliation readers."""
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/graph_write_plan.py b/hugegraph-mcp/hugegraph_mcp/tools/graph_write_plan.py
index b13c8923..d4205948 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/graph_write_plan.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/graph_write_plan.py
@@ -1,7 +1,15 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
"""Compile validated graph changes into the canonical durable write model."""
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/ingest_graph_data.py b/hugegraph-mcp/hugegraph_mcp/tools/ingest_graph_data.py
index 6b8b8ff2..8fbe0973 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/ingest_graph_data.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/ingest_graph_data.py
@@ -850,11 +850,12 @@ def ingest_graph_data_via_ai(
next_action=("Set HUGEGRAPH_MCP_READONLY=false and rerun dry_run before confirm."),
)
warnings = list(warnings) + readonly_warnings
- warnings = list(warnings) + [
+ warnings = [
+ *warnings,
(
"Legacy HugeGraph-AI graph import is preview-only because it has no "
"durable per-operation receipts or verified create-if-absent semantics."
- )
+ ),
]
next_actions.append("Do not confirm this import until atomic graph-create capabilities are verified.")
return envelope_ok(payload, warnings=warnings, next_actions=next_actions)
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/inspect_graph.py b/hugegraph-mcp/hugegraph_mcp/tools/inspect_graph.py
index 81e135bb..e3192468 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/inspect_graph.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/inspect_graph.py
@@ -136,7 +136,7 @@ def inspect_graph(
schema_summary = simple_schema
raw_schema = schema_result.get("schema")
readonly = schema_result.get("readonly")
- except Exception as exc: # noqa: BLE001 - inspect is best effort
+ except Exception as exc:
server_status = "unavailable"
warnings.append(_warning_from_exception("HugeGraph Server is unavailable", exc))
@@ -152,9 +152,7 @@ def inspect_graph(
"graphspace": cfg.graphspace,
"hugegraph_server_status": server_status,
"hugegraph_ai_status": ai_status,
- "vid_embedding_status": "available"
- if _has_graph_index_info(graph_index_info)
- else "unknown",
+ "vid_embedding_status": "available" if _has_graph_index_info(graph_index_info) else "unknown",
"schema_summary": schema_summary,
"vertex_count": vertex_count,
"edge_count": edge_count,
@@ -182,7 +180,7 @@ def _run_count_query(query: str, label: str, warnings: list[str]) -> int | None:
if count is None:
warnings.append(f"Failed to fetch {label} count")
return count
- except Exception as exc: # noqa: BLE001 - inspect is best effort
+ except Exception as exc:
warnings.append(_warning_from_exception(f"Failed to fetch {label} count", exc))
return None
@@ -198,7 +196,5 @@ def _next_actions(data: dict[str, Any]) -> list[str]:
else:
actions.append("Check HugeGraph Server URL, graph name, and credentials")
if data.get("hugegraph_ai_status") != "available":
- actions.append(
- "Check HugeGraph-AI URL if embedding or graph index features are needed"
- )
+ actions.append("Check HugeGraph-AI URL if embedding or graph index features are needed")
return actions
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/inspect_schema.py b/hugegraph-mcp/hugegraph_mcp/tools/inspect_schema.py
index d1966e44..10cfeb6e 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/inspect_schema.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/inspect_schema.py
@@ -32,9 +32,7 @@ FILTER_KINDS = frozenset({"property_key", "vertex_label", "edge_label", "index_l
def _schema_manager():
- return build_hugegraph_client(
- MCPConfig.from_env(), client_cls=PyHugeClient
- ).schema()
+ return build_hugegraph_client(MCPConfig.from_env(), client_cls=PyHugeClient).schema()
def inspect_schema(
@@ -75,16 +73,12 @@ def inspect_schema(
],
)
relations = _safe_get_relations(manager) if include_relations else []
- except Exception as exc: # noqa: BLE001 - return structured dependency error
+ except Exception as exc:
return _schema_dependency_error(exc)
raw_payload = schema_payload(raw_schema) or raw_schema
summary = _build_summary(raw_payload, include_index_labels=include_index_labels)
- all_summary = (
- summary
- if include_index_labels
- else _build_summary(raw_payload, include_index_labels=True)
- )
+ all_summary = summary if include_index_labels else _build_summary(raw_payload, include_index_labels=True)
filtered = _filter_schema(raw_payload, filter_kind, filter_name, all_summary)
if filtered is None:
return envelope_err(
@@ -139,9 +133,7 @@ def _validate_filter(
return envelope_err(
ErrorType.VALIDATION_ERROR,
f"Unsupported filter_kind: {filter_kind!r}.",
- suggestion=(
- "Use one of: property_key, vertex_label, edge_label, index_label."
- ),
+ suggestion=("Use one of: property_key, vertex_label, edge_label, index_label."),
source="inspect_schema_tool",
details={"filter_kind": filter_kind},
)
@@ -151,7 +143,7 @@ def _validate_filter(
def _safe_get_relations(manager) -> list[str]:
try:
relations = manager.getRelations()
- except Exception: # noqa: BLE001 - relations are optional
+ except Exception:
return []
return [str(item) for item in relations or []]
@@ -173,19 +165,11 @@ def _build_summary(
*,
include_index_labels: bool,
) -> dict[str, Any]:
- property_keys = [
- _normalize_schema_item(item) for item in _items(raw_schema, "propertykeys")
- ]
- vertex_labels = [
- _normalize_vertex_label(item) for item in _items(raw_schema, "vertexlabels")
- ]
- edge_labels = [
- _normalize_edge_label(item) for item in _items(raw_schema, "edgelabels")
- ]
+ property_keys = [_normalize_schema_item(item) for item in _items(raw_schema, "propertykeys")]
+ vertex_labels = [_normalize_vertex_label(item) for item in _items(raw_schema, "vertexlabels")]
+ edge_labels = [_normalize_edge_label(item) for item in _items(raw_schema, "edgelabels")]
index_labels = (
- [_normalize_schema_item(item) for item in _items(raw_schema, "indexlabels")]
- if include_index_labels
- else []
+ [_normalize_schema_item(item) for item in _items(raw_schema, "indexlabels")] if include_index_labels else []
)
return {
"property_key_count": len(property_keys),
@@ -224,9 +208,7 @@ def _filter_schema(
"edge_label": "edgelabels",
"index_label": "indexlabels",
}[filter_kind]
- normalized = [
- _normalize_kind_item(filter_kind, item) for item in _items(raw_schema, key)
- ]
+ normalized = [_normalize_kind_item(filter_kind, item) for item in _items(raw_schema, key)]
if filter_name is None:
return normalized
for item in normalized:
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/manage_graph_data.py b/hugegraph-mcp/hugegraph_mcp/tools/manage_graph_data.py
index 5b9b97a0..91e9b613 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/manage_graph_data.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/manage_graph_data.py
@@ -332,7 +332,7 @@ def manage_graph_data(
tool_name=plan_tool_name,
)
plan_store_from_config().save_plan(canonical_plan)
- except Exception as exc: # noqa: BLE001 - fail closed at persistence boundary
+ except Exception as exc:
return envelope_err(
ErrorType.FEATURE_DISABLED,
"The graph change cannot be persisted as an executable atomic plan.",
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/manage_schema.py b/hugegraph-mcp/hugegraph_mcp/tools/manage_schema.py
index 2f4252ad..4a39cbaf 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/manage_schema.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/manage_schema.py
@@ -946,7 +946,10 @@ def validate_schema_operations(
idx,
operation,
f"{field} references undefined vertex label: {label}",
- "Create the referenced vertex label first and rerun validation after it exists in the live schema.",
+ (
+ "Create the referenced vertex label first and rerun validation "
+ "after it exists in the live schema."
+ ),
)
)
if operation.get("frequency") not in (None, ""):
@@ -998,7 +1001,10 @@ def validate_schema_operations(
idx,
operation,
f"base_label references undefined vertex label: {base_label}",
- "Create the referenced vertex label first and rerun validation after it exists in the live schema.",
+ (
+ "Create the referenced vertex label first and rerun validation "
+ "after it exists in the live schema."
+ ),
)
)
elif base_type == "EDGE":
@@ -1008,7 +1014,10 @@ def validate_schema_operations(
idx,
operation,
f"base_label references undefined edge label: {base_label}",
- "Create the referenced edge label first and rerun validation after it exists in the live schema.",
+ (
+ "Create the referenced edge label first and rerun validation "
+ "after it exists in the live schema."
+ ),
)
)
else:
@@ -1168,7 +1177,7 @@ def _mutation_summary(operations: list[dict[str, Any]]) -> str:
def _safe_fetch_live_schema() -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
try:
return current_live_schema(), None
- except Exception as exc: # noqa: BLE001 - return structured schema error
+ except Exception as exc:
return None, envelope_err(
ErrorType.CONNECTION_FAILED,
"Cannot read live schema from HugeGraph Server for schema validation.",
@@ -1294,7 +1303,7 @@ def apply_schema_operations(
_apply_one_operation(manager, operation)
failure_stage = "post-read schema"
observed_schema = current_live_schema()
- except Exception as exc: # noqa: BLE001 - request may already have committed
+ except Exception as exc:
return ApplyReceipt(
status=ApplyStatus.UNKNOWN,
operation=operation,
@@ -1774,7 +1783,7 @@ def manage_schema(
live_schema=live_schema,
)
plan_store_from_config().save_plan(canonical_plan)
- except Exception as exc: # noqa: BLE001 - fail closed at persistence boundary
+ except Exception as exc:
return envelope_err(
ErrorType.SERVER_ERROR,
"The schema create plan could not be persisted safely.",
@@ -1789,12 +1798,12 @@ def manage_schema(
result["readonly_preview_only"] = True
return envelope_ok(
result,
- warnings=list(result.get("warnings", []))
- + [
+ warnings=[
+ *result.get("warnings", []),
(
"This dry-run was generated while HUGEGRAPH_MCP_READONLY=true. "
"Set HUGEGRAPH_MCP_READONLY=false and rerun dry_run before confirming writes."
- )
+ ),
],
next_actions=["Set HUGEGRAPH_MCP_READONLY=false and rerun dry_run before confirm."],
)
@@ -1880,7 +1889,9 @@ def manage_schema(
return envelope_err(
ErrorType.WRITE_OUTCOME_UNKNOWN,
"The schema create may have committed; reconcile before retrying.",
- suggestion="Inspect the schema and compare it with the confirmed operation before issuing another write.",
+ suggestion=(
+ "Inspect the schema and compare it with the confirmed operation before issuing another write."
+ ),
retryable=False,
details=apply_result,
next_actions=["Call inspect_schema_tool and reconcile the confirmed schema object."],
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/mutate_graph_properties.py b/hugegraph-mcp/hugegraph_mcp/tools/mutate_graph_properties.py
index 44ce88d1..d7565262 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/mutate_graph_properties.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/mutate_graph_properties.py
@@ -257,7 +257,7 @@ def _read_schema_and_target(
) -> tuple[dict[str, Any] | None, dict[str, Any] | None, dict[str, Any] | None]:
try:
live_schema = current_live_schema()
- except Exception as exc: # noqa: BLE001 - normalize external schema failure
+ except Exception as exc:
return (
None,
None,
@@ -274,7 +274,7 @@ def _read_schema_and_target(
try:
manager = _graph_manager()
raw_item = manager.getVertexById(id) if target == "vertex" else manager.getEdgeById(id)
- except Exception as exc: # noqa: BLE001 - normalize external graph failure
+ except Exception as exc:
classification = classify_hugegraph_exception(exc)
return (
live_schema,
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/query_graph_data.py b/hugegraph-mcp/hugegraph_mcp/tools/query_graph_data.py
index 0c05970c..8a48f4af 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/query_graph_data.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/query_graph_data.py
@@ -83,11 +83,7 @@ def query_graph_data(
normalized_direction = _normalize_direction(direction)
try:
- normalized_ids = (
- _normalize_ids(ids, warnings, target=target)
- if operation == "get_by_ids"
- else []
- )
+ normalized_ids = _normalize_ids(ids, warnings, target=target) if operation == "get_by_ids" else []
manager = _graph_manager()
result, next_page = _execute_query(
manager=manager,
@@ -102,7 +98,7 @@ def query_graph_data(
vertex_id=vertex_id,
direction=normalized_direction,
)
- except Exception as exc: # noqa: BLE001 - return structured query error
+ except Exception as exc:
return _query_error(exc)
items = _normalize_items(result)
@@ -443,9 +439,7 @@ def _plain_item(item: Any) -> dict[str, Any]:
return result
-def _normalize_ids(
- ids: list[Any] | None, warnings: list[str], *, target: str
-) -> list[Any]:
+def _normalize_ids(ids: list[Any] | None, warnings: list[str], *, target: str) -> list[Any]:
seen: set[str] = set()
normalized: list[Any] = []
duplicate_count = 0
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/schema_contract.py b/hugegraph-mcp/hugegraph_mcp/tools/schema_contract.py
index e04f9bea..37efaf9b 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/schema_contract.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/schema_contract.py
@@ -72,9 +72,7 @@ def _field(
# validation but is not a field returned by HugeGraph's schema GET response.
SUPPORTED_FIELD_SPECS: dict[str, dict[str, SchemaFieldSpec]] = {
"create_property_key": {
- "type": _field(
- "type", kind="identity", include_in_summary=False, apply_mode="identity"
- ),
+ "type": _field("type", kind="identity", include_in_summary=False, apply_mode="identity"),
"name": _field(
"name",
"property_name",
@@ -92,20 +90,12 @@ SUPPORTED_FIELD_SPECS: dict[str, dict[str, SchemaFieldSpec]] = {
default="SINGLE",
apply_mode="typed",
),
- "aggregate_type": _field(
- "aggregate_type", "aggregateType", kind="enum", apply_mode="typed"
- ),
- "user_data": _field(
- "user_data", "userData", "userdata", kind="mapping", apply_mode="parameter"
- ),
+ "aggregate_type": _field("aggregate_type", "aggregateType", kind="enum", apply_mode="typed"),
+ "user_data": _field("user_data", "userData", "userdata", kind="mapping", apply_mode="parameter"),
},
"create_vertex_label": {
- "type": _field(
- "type", kind="identity", include_in_summary=False, apply_mode="identity"
- ),
- "name": _field(
- "name", kind="identity", include_in_summary=False, apply_mode="identity"
- ),
+ "type": _field("type", kind="identity", include_in_summary=False, apply_mode="identity"),
+ "name": _field("name", kind="identity", include_in_summary=False, apply_mode="identity"),
"id_strategy": _field(
"id_strategy",
"idStrategy",
@@ -114,61 +104,31 @@ SUPPORTED_FIELD_SPECS: dict[str, dict[str, SchemaFieldSpec]] = {
apply_mode="typed",
),
"properties": _field("properties", kind="list", apply_mode="typed"),
- "primary_keys": _field(
- "primary_keys", "primaryKeys", kind="list", apply_mode="typed"
- ),
- "nullable_keys": _field(
- "nullable_keys", "nullableKeys", kind="list", apply_mode="typed"
- ),
- "index_labels": _field(
- "index_labels", "indexLabels", kind="list", apply_mode="parameter"
- ),
- "enable_label_index": _field(
- "enable_label_index", "enableLabelIndex", kind="boolean", apply_mode="typed"
- ),
- "user_data": _field(
- "user_data", "userData", "userdata", kind="mapping", apply_mode="parameter"
- ),
+ "primary_keys": _field("primary_keys", "primaryKeys", kind="list", apply_mode="typed"),
+ "nullable_keys": _field("nullable_keys", "nullableKeys", kind="list", apply_mode="typed"),
+ "index_labels": _field("index_labels", "indexLabels", kind="list", apply_mode="parameter"),
+ "enable_label_index": _field("enable_label_index", "enableLabelIndex", kind="boolean", apply_mode="typed"),
+ "user_data": _field("user_data", "userData", "userdata", kind="mapping", apply_mode="parameter"),
},
"create_edge_label": {
- "type": _field(
- "type", kind="identity", include_in_summary=False, apply_mode="identity"
- ),
- "name": _field(
- "name", kind="identity", include_in_summary=False, apply_mode="identity"
- ),
- "source_label": _field(
- "source_label", "sourceLabel", kind="scalar", apply_mode="link"
- ),
- "target_label": _field(
- "target_label", "targetLabel", kind="scalar", apply_mode="link"
- ),
+ "type": _field("type", kind="identity", include_in_summary=False, apply_mode="identity"),
+ "name": _field("name", kind="identity", include_in_summary=False, apply_mode="identity"),
+ "source_label": _field("source_label", "sourceLabel", kind="scalar", apply_mode="link"),
+ "target_label": _field("target_label", "targetLabel", kind="scalar", apply_mode="link"),
"properties": _field("properties", kind="list", apply_mode="typed"),
- "nullable_keys": _field(
- "nullable_keys", "nullableKeys", kind="list", apply_mode="typed"
- ),
+ "nullable_keys": _field("nullable_keys", "nullableKeys", kind="list", apply_mode="typed"),
"sort_keys": _field("sort_keys", "sortKeys", kind="list", apply_mode="typed"),
"frequency": _field("frequency", kind="enum", apply_mode="typed"),
- "enable_label_index": _field(
- "enable_label_index", "enableLabelIndex", kind="boolean", apply_mode="typed"
- ),
- "user_data": _field(
- "user_data", "userData", "userdata", kind="mapping", apply_mode="parameter"
- ),
+ "enable_label_index": _field("enable_label_index", "enableLabelIndex", kind="boolean", apply_mode="typed"),
+ "user_data": _field("user_data", "userData", "userdata", kind="mapping", apply_mode="parameter"),
},
# Index creation is validate-only for P0a, but it still has a closed
# validate/hash contract so fields cannot be silently accepted and dropped.
"create_index_label": {
- "type": _field(
- "type", kind="identity", include_in_summary=False, apply_mode="identity"
- ),
- "name": _field(
- "name", kind="identity", include_in_summary=False, apply_mode="identity"
- ),
+ "type": _field("type", kind="identity", include_in_summary=False, apply_mode="identity"),
+ "name": _field("name", kind="identity", include_in_summary=False, apply_mode="identity"),
"base_type": _field("base_type", "baseType", kind="enum"),
- "base_label": _field(
- "base_label", "baseLabel", "base_value", "baseValue", kind="scalar"
- ),
+ "base_label": _field("base_label", "baseLabel", "base_value", "baseValue", kind="scalar"),
"fields": _field("fields", kind="list"),
"index_type": _field("index_type", "indexType", kind="enum"),
"unique": _field("unique", kind="boolean"),
@@ -214,11 +174,7 @@ def field_specs_for_kind(kind: str) -> dict[str, SchemaFieldSpec]:
def apply_fields_for_operation(op_type: str) -> frozenset[str]:
"""Return fields with an explicit builder-forwarding contract."""
- return frozenset(
- field
- for field, spec in field_specs_for_operation(op_type).items()
- if spec.apply_mode != "none"
- )
+ return frozenset(field for field, spec in field_specs_for_operation(op_type).items() if spec.apply_mode != "none")
def schema_collection_for_operation(op_type: str) -> str | None:
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/schema_utils.py b/hugegraph-mcp/hugegraph_mcp/tools/schema_utils.py
index 85ea2290..fb9e3a9e 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/schema_utils.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/schema_utils.py
@@ -94,10 +94,7 @@ def schema_payload(live_schema: dict[str, Any] | None) -> dict[str, Any] | None:
# itself; unwrap it in one shared entry point instead of in each tool.
if live_schema is None or not isinstance(live_schema, dict):
return None
- if "schema" in live_schema:
- raw = live_schema.get("schema")
- else:
- raw = live_schema
+ raw = live_schema.get("schema") if "schema" in live_schema else live_schema
return raw if isinstance(raw, dict) else None
@@ -110,11 +107,7 @@ def user_data_without_server_metadata(value: Any) -> dict[Any, Any] | None:
"""
if not isinstance(value, dict):
return None
- return {
- key: item
- for key, item in value.items()
- if not (isinstance(key, str) and key.startswith("~"))
- }
+ return {key: item for key, item in value.items() if not (isinstance(key, str) and key.startswith("~"))}
def edge_schema_endpoint_label(edge_schema: dict[str, Any], endpoint: str) -> Any:
@@ -171,11 +164,7 @@ def _normalize_schema_items(
list_fields = {
field
for field, spec in field_specs.items()
- if (
- spec.include_in_summary
- and spec.kind == "list"
- and (include_user_data or spec.kind != "mapping")
- )
+ if (spec.include_in_summary and spec.kind == "list" and (include_user_data or spec.kind != "mapping"))
}
normalized: list[dict[str, Any]] = []
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/schema_write_adapter.py b/hugegraph-mcp/hugegraph_mcp/tools/schema_write_adapter.py
index 46b367a4..7094ab4d 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/schema_write_adapter.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/schema_write_adapter.py
@@ -1,7 +1,15 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
"""Canonical plan compiler, executor adapter, and reader for schema creates."""
diff --git a/hugegraph-mcp/hugegraph_mcp/write_contract.py b/hugegraph-mcp/hugegraph_mcp/write_contract.py
index d648acc3..aecd0f13 100644
--- a/hugegraph-mcp/hugegraph_mcp/write_contract.py
+++ b/hugegraph-mcp/hugegraph_mcp/write_contract.py
@@ -1,7 +1,15 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
"""Compatibility rules for canonical and legacy confirmation locators."""
diff --git a/hugegraph-mcp/hugegraph_mcp/write_executor.py b/hugegraph-mcp/hugegraph_mcp/write_executor.py
index 38105b03..6258e159 100644
--- a/hugegraph-mcp/hugegraph_mcp/write_executor.py
+++ b/hugegraph-mcp/hugegraph_mcp/write_executor.py
@@ -1,7 +1,15 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
"""Unified plan-ID confirmation and executor dispatch."""
@@ -183,7 +191,7 @@ class WriteExecutor:
)
try:
receipt = adapter(claimed_plan, operation, attempt)
- except Exception as exc: # noqa: BLE001 - persist ambiguous adapter outcome
+ except Exception as exc:
receipt = ApplyReceipt(
plan_id=plan_id,
operation_id=operation.operation_id,
@@ -270,10 +278,10 @@ DEFAULT_WRITE_EXECUTOR_REGISTRY = WriteExecutorRegistry()
# Graph adapters are registered here, after the registry type is defined, to
# keep the adapter module independent of executor orchestration.
-from hugegraph_mcp.tools.graph_write_adapter import (
+from hugegraph_mcp.tools.graph_write_adapter import ( # noqa: E402
register_graph_write_adapters,
)
-from hugegraph_mcp.tools.schema_write_adapter import (
+from hugegraph_mcp.tools.schema_write_adapter import ( # noqa: E402
register_schema_write_adapters,
)
@@ -287,7 +295,7 @@ def confirm_write(plan_id: str) -> dict:
cfg = MCPConfig.from_env()
store = plan_store_from_config(cfg)
- except Exception as exc: # noqa: BLE001 - public tool must remain enveloped
+ except Exception as exc:
return envelope_err(
ErrorType.SERVER_ERROR,
"The durable plan store is unavailable.",
@@ -304,7 +312,7 @@ def confirm_write(plan_id: str) -> dict:
def get_write_status(plan_id: str) -> dict:
try:
store = plan_store_from_config()
- except Exception as exc: # noqa: BLE001 - public tool must remain enveloped
+ except Exception as exc:
return envelope_err(
ErrorType.SERVER_ERROR,
"The durable plan store is unavailable.",
diff --git a/hugegraph-mcp/hugegraph_mcp/write_limits.py b/hugegraph-mcp/hugegraph_mcp/write_limits.py
index ab64afab..1589500c 100644
--- a/hugegraph-mcp/hugegraph_mcp/write_limits.py
+++ b/hugegraph-mcp/hugegraph_mcp/write_limits.py
@@ -41,13 +41,9 @@ def collect_write_limit_errors(
{
"operation_index": -1,
"operation": None,
- "reason": (
- f"operation count {operation_count} exceeds "
- f"MAX_OPERATIONS ({MAX_OPERATIONS})"
- ),
+ "reason": (f"operation count {operation_count} exceeds MAX_OPERATIONS ({MAX_OPERATIONS})"),
"suggestion": (
- f"Split the request so it contains at most {MAX_OPERATIONS} "
- "operations, then retry dry-run."
+ f"Split the request so it contains at most {MAX_OPERATIONS} operations, then retry dry-run."
),
"error_type": ErrorType.VALIDATION_ERROR.value,
}
@@ -58,14 +54,8 @@ def collect_write_limit_errors(
{
"operation_index": -1,
"operation": None,
- "reason": (
- f"payload size {size} exceeds MAX_PAYLOAD_BYTES "
- f"({MAX_PAYLOAD_BYTES})"
- ),
- "suggestion": (
- "Reduce the JSON payload to at most 1 MiB (1048576 bytes), "
- "then retry dry-run."
- ),
+ "reason": (f"payload size {size} exceeds MAX_PAYLOAD_BYTES ({MAX_PAYLOAD_BYTES})"),
+ "suggestion": ("Reduce the JSON payload to at most 1 MiB (1048576 bytes), then retry dry-run."),
"error_type": ErrorType.VALIDATION_ERROR.value,
}
)
diff --git a/hugegraph-mcp/hugegraph_mcp/write_plan.py b/hugegraph-mcp/hugegraph_mcp/write_plan.py
index 03c87b14..687c5d40 100644
--- a/hugegraph-mcp/hugegraph_mcp/write_plan.py
+++ b/hugegraph-mcp/hugegraph_mcp/write_plan.py
@@ -1,7 +1,15 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
"""Canonical immutable models and state rules for confirmed writes."""
diff --git a/hugegraph-mcp/tests/integration/test_real_write_path.py b/hugegraph-mcp/tests/integration/test_real_write_path.py
index 86218a3f..64f5fadc 100644
--- a/hugegraph-mcp/tests/integration/test_real_write_path.py
+++ b/hugegraph-mcp/tests/integration/test_real_write_path.py
@@ -21,12 +21,13 @@ from threading import Barrier
from uuid import uuid4
import pytest
+from pyhugegraph.client import PyHugeClient
+
from hugegraph_mcp import server
from hugegraph_mcp.config import MCPConfig
from hugegraph_mcp.hugegraph_client import build_hugegraph_client
from hugegraph_mcp.tools import manage_graph_data as manage_graph_data_module
from hugegraph_mcp.tools.graph_data_gremlin import _g
-from pyhugegraph.client import PyHugeClient
pytestmark = [pytest.mark.integration, pytest.mark.real_hugegraph]
@@ -51,7 +52,7 @@ def hugegraph_client(monkeypatch):
try:
client.schema().getSchema()
break
- except Exception as exc: # noqa: BLE001 - service startup is asynchronous
+ except Exception as exc:
last_error = exc
time.sleep(1.0)
else:
@@ -605,7 +606,7 @@ def test_rocksdb_isolates_conditional_vertex_delete_from_concurrent_edge_add(
start.wait(timeout=10.0)
try:
result = _extract_count(_exec(client, query))
- except Exception as exc: # noqa: BLE001 - record concurrent backend outcome
+ except Exception as exc:
return None, str(exc)
return result, None
@@ -1103,7 +1104,7 @@ def _wait_for_schema_visibility(
):
_write_and_drop_schema_probe(client, names, custom_id=custom_id)
return
- except Exception as exc: # noqa: BLE001 - retry until service is ready
+ except Exception as exc:
last_error = exc
time.sleep(0.1)
pytest.fail(f"Timed out waiting for HugeGraph schema visibility: {names.vertex_label}, last_error={last_error}")
diff --git a/hugegraph-mcp/tests/test_backend_capabilities.py b/hugegraph-mcp/tests/test_backend_capabilities.py
index e0607315..f2bfddac 100644
--- a/hugegraph-mcp/tests/test_backend_capabilities.py
+++ b/hugegraph-mcp/tests/test_backend_capabilities.py
@@ -1,11 +1,20 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
from dataclasses import FrozenInstanceError
import pytest
+
from hugegraph_mcp.backend_capabilities import (
BackendFeature,
SupportStatus,
diff --git a/hugegraph-mcp/tests/test_config.py b/hugegraph-mcp/tests/test_config.py
index 4f3b87ae..175502b3 100644
--- a/hugegraph-mcp/tests/test_config.py
+++ b/hugegraph-mcp/tests/test_config.py
@@ -14,8 +14,9 @@
import logging
from dataclasses import FrozenInstanceError
-import hugegraph_mcp.config as config_module
import pytest
+
+import hugegraph_mcp.config as config_module
from hugegraph_mcp.config import MCPConfig, config
CONFIG_ENV_VARS = (
diff --git a/hugegraph-mcp/tests/test_confirm_write.py b/hugegraph-mcp/tests/test_confirm_write.py
index 88b210f0..b30ca28f 100644
--- a/hugegraph-mcp/tests/test_confirm_write.py
+++ b/hugegraph-mcp/tests/test_confirm_write.py
@@ -1,7 +1,15 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
import sqlite3
import time
@@ -9,6 +17,7 @@ from concurrent.futures import ThreadPoolExecutor
from threading import Barrier, Event, Lock
import pytest
+
from hugegraph_mcp.envelope import envelope_ok
from hugegraph_mcp.plan_store import PlanTransitionError, SQLitePlanStore
from hugegraph_mcp.reconciler import ReconcileReaderRegistry, WriteReconciler
diff --git a/hugegraph-mcp/tests/test_error_handling.py b/hugegraph-mcp/tests/test_error_handling.py
index d1851f74..dfdef74a 100644
--- a/hugegraph-mcp/tests/test_error_handling.py
+++ b/hugegraph-mcp/tests/test_error_handling.py
@@ -17,6 +17,16 @@ from unittest.mock import Mock, patch
import pytest
import requests
+from pyhugegraph.utils.exceptions import (
+ DataFormatError,
+ InvalidParameterError,
+ NotAuthorizedError,
+ NotFoundError,
+ ResponseParseError,
+ ServerError,
+ ServiceUnavailableError,
+)
+
from hugegraph_mcp.confirmable_workflow import (
confirm_required_error,
mark_readonly_preview,
@@ -33,15 +43,6 @@ from hugegraph_mcp.error_mapping import (
classify_hugegraph_exception,
)
from hugegraph_mcp.gremlin_tools import execute_gremlin_read, execute_gremlin_write
-from pyhugegraph.utils.exceptions import (
- DataFormatError,
- InvalidParameterError,
- NotAuthorizedError,
- NotFoundError,
- ResponseParseError,
- ServerError,
- ServiceUnavailableError,
-)
def test_connection_error_handling():
diff --git a/hugegraph-mcp/tests/test_generate_gremlin.py b/hugegraph-mcp/tests/test_generate_gremlin.py
index e27a6396..20529383 100644
--- a/hugegraph-mcp/tests/test_generate_gremlin.py
+++ b/hugegraph-mcp/tests/test_generate_gremlin.py
@@ -103,14 +103,10 @@ def test_generate_gremlin_returns_match_result_without_requiring_gremlin(monkeyp
monkeypatch.setattr(generate_gremlin_module, "post", post)
monkeypatch.setattr(generate_gremlin_module, "execute_gremlin_read", execute_read)
- result = generate_gremlin_module.generate_gremlin(
- "count vertices", output_types=["match_result"]
- )
+ result = generate_gremlin_module.generate_gremlin("count vertices", output_types=["match_result"])
assert result["ok"] is True
- assert result["data"]["match_result"] == [
- {"query": "count", "gremlin": "g.V().count()"}
- ]
+ assert result["data"]["match_result"] == [{"query": "count", "gremlin": "g.V().count()"}]
assert result["data"]["gremlin"] is None
assert result["data"]["is_readonly"] is False
assert result["data"]["executed"] is False
@@ -153,9 +149,7 @@ def test_generate_gremlin_rejects_execution_output_types_before_ai_call(monkeypa
assert result["ok"] is False
assert result["error"]["type"] == "VALIDATION_ERROR"
- assert result["error"]["details"]["invalid_output_types"] == [
- "raw_execution_result"
- ]
+ assert result["error"]["details"]["invalid_output_types"] == ["raw_execution_result"]
post.assert_not_called()
execute_read.assert_not_called()
@@ -164,9 +158,7 @@ def test_generate_gremlin_rejects_non_string_output_type(monkeypatch):
post = Mock()
monkeypatch.setattr(generate_gremlin_module, "post", post)
- result = generate_gremlin_module.generate_gremlin(
- "count vertices", output_types=[1]
- )
+ result = generate_gremlin_module.generate_gremlin("count vertices", output_types=[1])
assert result["ok"] is False
assert result["error"]["type"] == "VALIDATION_ERROR"
@@ -174,11 +166,7 @@ def test_generate_gremlin_rejects_non_string_output_type(monkeypatch):
def test_generate_gremlin_rejects_missing_gremlin(monkeypatch):
- post = Mock(
- return_value=envelope_ok(
- {"requires_index": False, "assumptions": ["no query generated"]}
- )
- )
+ post = Mock(return_value=envelope_ok({"requires_index": False, "assumptions": ["no query generated"]}))
execute_read = Mock()
monkeypatch.setattr(generate_gremlin_module, "post", post)
monkeypatch.setattr(generate_gremlin_module, "execute_gremlin_read", execute_read)
@@ -303,9 +291,7 @@ def test_generate_gremlin_propagates_execute_failure(monkeypatch):
assert result["ok"] is False
assert result["error"]["type"] == "NO_INDEX"
assert result["error"]["details"]["gremlin"] == "g.V().has('name','Alice')"
- assert result["error"]["details"]["execution_error"]["message"] == (
- "Query requires an index"
- )
+ assert result["error"]["details"]["execution_error"]["message"] == ("Query requires an index")
def test_generate_gremlin_unsafe_no_execute(monkeypatch):
@@ -318,10 +304,7 @@ def test_generate_gremlin_unsafe_no_execute(monkeypatch):
assert result["ok"] is False
assert result["error"]["type"] == "UNSAFE_GREMLIN"
- assert (
- result["error"]["message"]
- == "Generated Gremlin is not safe to execute automatically"
- )
+ assert result["error"]["message"] == "Generated Gremlin is not safe to execute automatically"
assert result["error"]["details"]["classification"] == "unsafe"
execute_read.assert_not_called()
diff --git a/hugegraph-mcp/tests/test_get_live_schema.py b/hugegraph-mcp/tests/test_get_live_schema.py
index e1e56a2b..45e46ca4 100644
--- a/hugegraph-mcp/tests/test_get_live_schema.py
+++ b/hugegraph-mcp/tests/test_get_live_schema.py
@@ -24,9 +24,7 @@ class FakePyHugeClient:
last_init_kwargs: dict | None = None
schema_data: dict | None = None
- def __init__(
- self, url: str, graph: str, user: str, pwd: str, graphspace=None, timeout=None
- ):
+ def __init__(self, url: str, graph: str, user: str, pwd: str, graphspace=None, timeout=None):
FakePyHugeClient.last_init_kwargs = {
"url": url,
"graph": graph,
@@ -136,9 +134,7 @@ def test_current_live_schema_respects_explicit_empty_schema(monkeypatch):
from hugegraph_mcp.tools.live_schema import current_live_schema
def fail_fetch():
- raise AssertionError(
- "current_live_schema should not fetch when schema is provided"
- )
+ raise AssertionError("current_live_schema should not fetch when schema is provided")
empty_schema = {}
monkeypatch.setattr(schema_tools, "get_live_schema", fail_fetch)
diff --git a/hugegraph-mcp/tests/test_graph_write_workflow.py b/hugegraph-mcp/tests/test_graph_write_workflow.py
index 2ef4ea7a..80d136c9 100644
--- a/hugegraph-mcp/tests/test_graph_write_workflow.py
+++ b/hugegraph-mcp/tests/test_graph_write_workflow.py
@@ -1,7 +1,15 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
from __future__ import annotations
diff --git a/hugegraph-mcp/tests/test_gremlin_policy.py b/hugegraph-mcp/tests/test_gremlin_policy.py
index bfa76ba6..addfcf1e 100644
--- a/hugegraph-mcp/tests/test_gremlin_policy.py
+++ b/hugegraph-mcp/tests/test_gremlin_policy.py
@@ -45,9 +45,7 @@ def test_uncertain_query_returns_blocked():
assert decision.allowed is False
assert decision.classification == "uncertain"
assert decision.error_type == "UNSAFE_GREMLIN"
- assert (
- "ambiguous" in decision.reason.lower() or "unknown" in decision.reason.lower()
- )
+ assert "ambiguous" in decision.reason.lower() or "unknown" in decision.reason.lower()
def test_decision_is_frozen_dataclass():
@@ -56,7 +54,7 @@ def test_decision_is_frozen_dataclass():
assert isinstance(decision, GremlinDecision)
try:
decision.allowed = False
- assert False, "Should be frozen"
+ raise AssertionError("Should be frozen")
except AttributeError:
pass
diff --git a/hugegraph-mcp/tests/test_gremlin_safety.py b/hugegraph-mcp/tests/test_gremlin_safety.py
index f1cbf9f2..0e83b3f9 100644
--- a/hugegraph-mcp/tests/test_gremlin_safety.py
+++ b/hugegraph-mcp/tests/test_gremlin_safety.py
@@ -90,9 +90,7 @@ def test_newly_denied_high_risk_steps():
]
for query in queries:
- assert classify_gremlin_read_safety(query) == "unsafe", (
- f"Expected unsafe: {query}"
- )
+ assert classify_gremlin_read_safety(query) == "unsafe", f"Expected unsafe: {query}"
def test_side_effect_accumulators_are_uncertain():
@@ -105,6 +103,4 @@ def test_side_effect_accumulators_are_uncertain():
]
for query in queries:
- assert classify_gremlin_read_safety(query) == "uncertain", (
- f"Expected uncertain: {query}"
- )
+ assert classify_gremlin_read_safety(query) == "uncertain", f"Expected uncertain: {query}"
diff --git a/hugegraph-mcp/tests/test_hugegraph_ai_client.py b/hugegraph-mcp/tests/test_hugegraph_ai_client.py
index 37b03204..b581f982 100644
--- a/hugegraph-mcp/tests/test_hugegraph_ai_client.py
+++ b/hugegraph-mcp/tests/test_hugegraph_ai_client.py
@@ -15,6 +15,7 @@ from unittest.mock import Mock
import pytest
import requests
+
from hugegraph_mcp.config import MCPConfig
from hugegraph_mcp.hugegraph_ai_client import get, health_check, post, request
diff --git a/hugegraph-mcp/tests/test_import_graph_data_tool.py b/hugegraph-mcp/tests/test_import_graph_data_tool.py
index 443f76a9..b0fb8e9d 100644
--- a/hugegraph-mcp/tests/test_import_graph_data_tool.py
+++ b/hugegraph-mcp/tests/test_import_graph_data_tool.py
@@ -33,9 +33,7 @@ def test_import_graph_data_tool_extract_routes_to_extract(monkeypatch):
assert result["ok"] is True
assert "duration_ms" in result["meta"]
- assert calls == [
- ("Alice knows Bob.", {"vertexlabels": ["person"]}, "extract people")
- ]
+ assert calls == [("Alice knows Bob.", {"vertexlabels": ["person"]}, "extract people")]
def test_import_graph_data_tool_ingest_routes_to_mcp_import(monkeypatch):
@@ -172,10 +170,7 @@ def test_import_graph_data_tool_table_returns_feature_disabled():
assert result["ok"] is False
assert result["error"]["type"] == "FEATURE_DISABLED"
assert result["error"]["source"] == "import_graph_data_tool"
- assert (
- result["error"]["message"]
- == "Table import is not supported by the current MCP contract."
- )
+ assert result["error"]["message"] == "Table import is not supported by the current MCP contract."
assert "import_graph_data_tool(mode='extract')" in result["error"]["suggestion"]
assert "import_graph_data_tool(mode='ingest')" in result["error"]["suggestion"]
assert "V1" not in result["error"]["message"]
diff --git a/hugegraph-mcp/tests/test_ingest_graph_data.py b/hugegraph-mcp/tests/test_ingest_graph_data.py
index 5e46d5bb..4329377c 100644
--- a/hugegraph-mcp/tests/test_ingest_graph_data.py
+++ b/hugegraph-mcp/tests/test_ingest_graph_data.py
@@ -15,6 +15,7 @@ import re
from unittest.mock import Mock
import pytest
+
from hugegraph_mcp.envelope import envelope_ok
from hugegraph_mcp.tools import ingest_graph_data as ingest_graph_data_module
from hugegraph_mcp.tools import manage_graph_data as manage_graph_data_module
diff --git a/hugegraph-mcp/tests/test_inspect_graph.py b/hugegraph-mcp/tests/test_inspect_graph.py
index 351ec2a5..5abb481e 100644
--- a/hugegraph-mcp/tests/test_inspect_graph.py
+++ b/hugegraph-mcp/tests/test_inspect_graph.py
@@ -66,9 +66,7 @@ def _patch_ai_available(monkeypatch, inspect_graph_module):
def test_inspect_graph_basic(monkeypatch):
from hugegraph_mcp.tools import inspect_graph as inspect_graph_module
- monkeypatch.setattr(
- inspect_graph_module, "get_live_schema", lambda: _schema_result()
- )
+ monkeypatch.setattr(inspect_graph_module, "get_live_schema", lambda: _schema_result())
execute_read = Mock(
side_effect=[
{"data": [3], "total": 1, "duration_ms": 1, "is_read": True},
@@ -96,9 +94,7 @@ def test_inspect_graph_basic(monkeypatch):
def test_inspect_graph_include_counts_true(monkeypatch):
from hugegraph_mcp.tools import inspect_graph as inspect_graph_module
- monkeypatch.setattr(
- inspect_graph_module, "get_live_schema", lambda: _schema_result()
- )
+ monkeypatch.setattr(inspect_graph_module, "get_live_schema", lambda: _schema_result())
execute_read = Mock(
side_effect=[
{"data": [3], "total": 1, "duration_ms": 1, "is_read": True},
@@ -124,9 +120,7 @@ def test_inspect_graph_ai_status_uses_unified_health_check_config(monkeypatch):
monkeypatch.setenv("HUGEGRAPH_PASSWORD", "secret")
monkeypatch.setenv("HUGEGRAPH_AI_URL", "http://ai.example:18001")
monkeypatch.setenv("HUGEGRAPH_MCP_TIMEOUT_SECONDS", "9")
- monkeypatch.setattr(
- inspect_graph_module, "get_live_schema", lambda: _schema_result()
- )
+ monkeypatch.setattr(inspect_graph_module, "get_live_schema", lambda: _schema_result())
monkeypatch.setattr(
inspect_graph_module,
"execute_gremlin_read",
@@ -152,9 +146,7 @@ def test_inspect_graph_ai_status_uses_unified_health_check_config(monkeypatch):
def test_inspect_graph_nested_count_result(monkeypatch):
from hugegraph_mcp.tools import inspect_graph as inspect_graph_module
- monkeypatch.setattr(
- inspect_graph_module, "get_live_schema", lambda: _schema_result()
- )
+ monkeypatch.setattr(inspect_graph_module, "get_live_schema", lambda: _schema_result())
execute_read = Mock(
side_effect=[
{
@@ -229,9 +221,7 @@ def test_inspect_graph_server_unavailable(monkeypatch):
def test_inspect_graph_ai_unavailable(monkeypatch):
from hugegraph_mcp.tools import inspect_graph as inspect_graph_module
- monkeypatch.setattr(
- inspect_graph_module, "get_live_schema", lambda: _schema_result()
- )
+ monkeypatch.setattr(inspect_graph_module, "get_live_schema", lambda: _schema_result())
monkeypatch.setattr(
inspect_graph_module,
"execute_gremlin_read",
@@ -261,9 +251,7 @@ def test_inspect_graph_ai_unavailable(monkeypatch):
def test_inspect_graph_ai_available_when_openapi_fallback_works(monkeypatch):
from hugegraph_mcp.tools import inspect_graph as inspect_graph_module
- monkeypatch.setattr(
- inspect_graph_module, "get_live_schema", lambda: _schema_result()
- )
+ monkeypatch.setattr(inspect_graph_module, "get_live_schema", lambda: _schema_result())
monkeypatch.setattr(
inspect_graph_module,
"execute_gremlin_read",
@@ -281,9 +269,7 @@ def test_inspect_graph_ai_available_when_openapi_fallback_works(monkeypatch):
"health_endpoint": "/openapi.json",
"openapi": "3.1.0",
},
- "warnings": [
- "/graph-index-info: HugeGraph-AI graph index info is unavailable"
- ],
+ "warnings": ["/graph-index-info: HugeGraph-AI graph index info is unavailable"],
}
),
)
@@ -299,9 +285,7 @@ def test_inspect_graph_ai_available_when_openapi_fallback_works(monkeypatch):
def test_inspect_graph_requires_explicit_vid_index_status(monkeypatch):
from hugegraph_mcp.tools import inspect_graph as inspect_graph_module
- monkeypatch.setattr(
- inspect_graph_module, "get_live_schema", lambda: _schema_result()
- )
+ monkeypatch.setattr(inspect_graph_module, "get_live_schema", lambda: _schema_result())
monkeypatch.setattr(
inspect_graph_module,
"execute_gremlin_read",
@@ -332,9 +316,7 @@ def test_inspect_graph_requires_explicit_vid_index_status(monkeypatch):
def test_inspect_graph_includes_next_actions(monkeypatch):
from hugegraph_mcp.tools import inspect_graph as inspect_graph_module
- monkeypatch.setattr(
- inspect_graph_module, "get_live_schema", lambda: _schema_result()
- )
+ monkeypatch.setattr(inspect_graph_module, "get_live_schema", lambda: _schema_result())
monkeypatch.setattr(
inspect_graph_module,
"execute_gremlin_read",
@@ -345,26 +327,16 @@ def test_inspect_graph_includes_next_actions(monkeypatch):
result = inspect_graph_module.inspect_graph()
assert result["next_actions"]
- assert any(
- "inspect_graph_tool with include_raw_schema=true" in action
- for action in result["next_actions"]
- )
- assert any(
- "inspect_graph_tool with include_counts=true" in action
- for action in result["next_actions"]
- )
- assert any(
- "execute_gremlin_read_tool" in action for action in result["next_actions"]
- )
+ assert any("inspect_graph_tool with include_raw_schema=true" in action for action in result["next_actions"])
+ assert any("inspect_graph_tool with include_counts=true" in action for action in result["next_actions"])
+ assert any("execute_gremlin_read_tool" in action for action in result["next_actions"])
assert not any("query_graph_tool" in action for action in result["next_actions"])
def test_inspect_graph_readonly_flag(monkeypatch):
from hugegraph_mcp.tools import inspect_graph as inspect_graph_module
- monkeypatch.setattr(
- inspect_graph_module, "get_live_schema", lambda: _schema_result(readonly=True)
- )
+ monkeypatch.setattr(inspect_graph_module, "get_live_schema", lambda: _schema_result(readonly=True))
monkeypatch.setattr(
inspect_graph_module,
"execute_gremlin_read",
@@ -381,9 +353,7 @@ def test_inspect_graph_readonly_flag(monkeypatch):
def test_inspect_graph_include_counts_failure_keeps_null(monkeypatch):
from hugegraph_mcp.tools import inspect_graph as inspect_graph_module
- monkeypatch.setattr(
- inspect_graph_module, "get_live_schema", lambda: _schema_result()
- )
+ monkeypatch.setattr(inspect_graph_module, "get_live_schema", lambda: _schema_result())
execute_read = Mock(side_effect=RuntimeError("timeout"))
monkeypatch.setattr(inspect_graph_module, "execute_gremlin_read", execute_read)
_patch_ai_available(monkeypatch, inspect_graph_module)
diff --git a/hugegraph-mcp/tests/test_manage_schema.py b/hugegraph-mcp/tests/test_manage_schema.py
index ab0bf95e..62f46a59 100644
--- a/hugegraph-mcp/tests/test_manage_schema.py
+++ b/hugegraph-mcp/tests/test_manage_schema.py
@@ -15,6 +15,7 @@ import re
from unittest.mock import Mock
import pytest
+
from hugegraph_mcp.tools import manage_schema as manage_schema_module
from hugegraph_mcp.tools.manage_schema import manage_schema
@@ -336,7 +337,7 @@ def test_manage_schema_validate_rejects_unknown_edge_endpoint(monkeypatch):
assert result["data"]["valid"] is False
error = result["data"]["errors"][0]
assert error["operation_index"] == 0
- assert "target_label references undefined vertex label: software" == error["reason"]
+ assert error["reason"] == "target_label references undefined vertex label: software"
def test_manage_schema_validate_rejects_duplicate_vertex_label(monkeypatch):
@@ -1279,20 +1280,22 @@ def test_dry_run_accepts_valid_edge_frequencies(monkeypatch):
def test_enum_tables_stay_in_sync_with_apply_methods():
- assert manage_schema_module.PROPERTY_KEY_DATA_TYPES == frozenset(
- manage_schema_module.PROPERTY_KEY_DATA_TYPE_METHODS
+ assert (
+ frozenset(manage_schema_module.PROPERTY_KEY_DATA_TYPE_METHODS) == manage_schema_module.PROPERTY_KEY_DATA_TYPES
)
- assert manage_schema_module.PROPERTY_KEY_CARDINALITIES == frozenset(
- manage_schema_module.PROPERTY_KEY_CARDINALITY_METHODS
+ assert (
+ frozenset(manage_schema_module.PROPERTY_KEY_CARDINALITY_METHODS)
+ == manage_schema_module.PROPERTY_KEY_CARDINALITIES
)
- assert manage_schema_module.PROPERTY_KEY_AGGREGATE_TYPES == (
+ assert (
frozenset(manage_schema_module.PROPERTY_KEY_AGGREGATE_METHODS)
| manage_schema_module.PROPERTY_KEY_DIRECT_AGGREGATE_TYPES
+ ) == manage_schema_module.PROPERTY_KEY_AGGREGATE_TYPES
+ assert (
+ frozenset(manage_schema_module.VERTEX_LABEL_ID_STRATEGY_METHODS)
+ == manage_schema_module.VERTEX_LABEL_ID_STRATEGIES
)
- assert manage_schema_module.VERTEX_LABEL_ID_STRATEGIES == frozenset(
- manage_schema_module.VERTEX_LABEL_ID_STRATEGY_METHODS
- )
- assert manage_schema_module.EDGE_LABEL_FREQUENCIES == frozenset(manage_schema_module.EDGE_LABEL_FREQUENCY_METHODS)
+ assert frozenset(manage_schema_module.EDGE_LABEL_FREQUENCY_METHODS) == manage_schema_module.EDGE_LABEL_FREQUENCIES
def test_invalid_enum_input_never_reaches_apply_stage_error(monkeypatch):
diff --git a/hugegraph-mcp/tests/test_plan_hash.py b/hugegraph-mcp/tests/test_plan_hash.py
index 68e9622f..fabcbb7d 100644
--- a/hugegraph-mcp/tests/test_plan_hash.py
+++ b/hugegraph-mcp/tests/test_plan_hash.py
@@ -318,7 +318,7 @@ def test_plan_context_is_frozen():
try:
context.tool_name = "other"
- assert False, "Should be frozen"
+ raise AssertionError("Should be frozen")
except AttributeError:
pass
@@ -398,7 +398,7 @@ def test_confirmation_store_rejects_far_future_expiry(monkeypatch):
plan_hash="client-computed",
expires_at=int(time.time()) + 601,
)
- assert False, "Plans beyond the server TTL must not be issued"
+ raise AssertionError("Plans beyond the server TTL must not be issued")
except ConfirmationPlanExpiredError:
pass
@@ -738,7 +738,7 @@ def test_confirmation_cleanup_failure_does_not_block_current_nonce(monkeypatch):
plan_hash="current",
expires_at=expires_at,
)
- assert False, "The current nonce must remain globally single-use"
+ raise AssertionError("The current nonce must remain globally single-use")
except ConfirmationAlreadyUsedError:
pass
diff --git a/hugegraph-mcp/tests/test_plan_store_interface.py b/hugegraph-mcp/tests/test_plan_store_interface.py
index 09dfe52f..7e009891 100644
--- a/hugegraph-mcp/tests/test_plan_store_interface.py
+++ b/hugegraph-mcp/tests/test_plan_store_interface.py
@@ -1,9 +1,18 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
import pytest
+
from hugegraph_mcp.config import MCPConfig
from hugegraph_mcp.guard import Capability, guard
from hugegraph_mcp.plan_store import (
diff --git a/hugegraph-mcp/tests/test_plan_store_migration.py b/hugegraph-mcp/tests/test_plan_store_migration.py
index 181fba51..96ed3d7f 100644
--- a/hugegraph-mcp/tests/test_plan_store_migration.py
+++ b/hugegraph-mcp/tests/test_plan_store_migration.py
@@ -1,13 +1,22 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
import hashlib
import json
import sqlite3
import pytest
+
from hugegraph_mcp.plan_store import (
SCHEMA_VERSION,
PlanStoreUnavailableError,
diff --git a/hugegraph-mcp/tests/test_property_validation.py b/hugegraph-mcp/tests/test_property_validation.py
index 32abc007..7c49ed05 100644
--- a/hugegraph-mcp/tests/test_property_validation.py
+++ b/hugegraph-mcp/tests/test_property_validation.py
@@ -12,6 +12,7 @@
# limitations under the License.
import pytest
+
from hugegraph_mcp.tools.property_validation import (
property_specs,
property_value_error,
diff --git a/hugegraph-mcp/tests/test_public_docs_contract.py b/hugegraph-mcp/tests/test_public_docs_contract.py
new file mode 100644
index 00000000..1b488c36
--- /dev/null
+++ b/hugegraph-mcp/tests/test_public_docs_contract.py
@@ -0,0 +1,128 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+from pathlib import Path
+
+PROJECT_ROOT = Path(__file__).resolve().parents[1]
+README = PROJECT_ROOT / "README.md"
+README_ZH = PROJECT_ROOT / "README.zh-CN.md"
+CHECKLIST = PROJECT_ROOT / "docs" / "p0a-integration-checklist.md"
+PUBLIC_DOCS = (README, README_ZH, CHECKLIST)
+
+
+def _text(path: Path) -> str:
+ return path.read_text(encoding="utf-8")
+
+
+def _section(content: str, heading: str, *, next_level: int) -> str:
+ start = content.index(heading)
+ remainder = content[start + len(heading) :]
+ match = re.search(rf"^{'#' * next_level} (?!#)", remainder, flags=re.MULTILINE)
+ return remainder if match is None else remainder[: match.start()]
+
+
+def test_public_docs_describe_canonical_plan_lifecycle_as_id_only():
+ english = _section(_text(README), "## Write Safety Contract", next_level=2)
+ chinese = _section(_text(README_ZH), "## 写入安全合同", next_level=2)
+ checklist = _section(_text(CHECKLIST), "## 3. Create Schema One Object Per Plan", next_level=2)
+
+ assert "The confirmation call never accepts the original payload" in english
+ assert "confirm_write_tool(plan_id)" in english
+ assert "get_write_status_tool(plan_id)" in english
+ assert "确认调用不接收原始 payload" in chinese
+ assert "confirm_write_tool(plan_id)" in chinese
+ assert "get_write_status_tool(plan_id)" in chinese
+ assert '"arguments": {"plan_id": "<PLAN_ID>"}' in checklist
+ assert 'Require `data.status="APPLIED"`' in checklist
+
+
+def test_import_contract_is_preview_only_in_every_public_document():
+ english = _text(README)
+ chinese = _text(README_ZH)
+ checklist = _section(_text(CHECKLIST), "## 4. Verify Import Is Preview-Only", next_level=2)
+
+ assert re.search(
+ r"`import_graph_data_tool`.*preview.*confirmation currently returns `FEATURE_DISABLED`",
+ english,
+ )
+ assert (
+ "Its preview sets `confirmable=false` and `preview_only=true`, issues no "
+ "`plan_id`, and confirmation returns `FEATURE_DISABLED` without writing."
+ ) in english
+ assert re.search(
+ r"`import_graph_data_tool`.*预览.*确认当前返回 `FEATURE_DISABLED`",
+ chinese,
+ )
+ assert (
+ "预览返回 `confirmable=false`、`preview_only=true`,不签发 `plan_id`;确认返回 `FEATURE_DISABLED` 且不写入。"
+ ) in chinese
+
+ assert "`data.confirmable=false`" in checklist
+ assert "`data.preview_only=true`" in checklist
+ assert "no\n`data.plan_id`" in checklist
+ assert '`error.type="FEATURE_DISABLED"`' in checklist
+ assert "neither vertex nor\nedge was created" in checklist
+ assert "Do not call `confirm_write_tool` for an import preview" in checklist
+ assert 'data.status="ISSUED"' not in checklist
+ assert 'data.status="APPLIED"' not in checklist
+ assert "confirm_write_tool(plan_id)" not in checklist
+
+
+def test_checklist_pass_criteria_do_not_claim_import_writes():
+ criteria = _section(_text(CHECKLIST), "## Pass Criteria", next_level=2)
+
+ assert (
+ "Vertex/edge import remains preview-only, issues no `plan_id`, returns "
+ "`FEATURE_DISABLED` on confirmation, and creates no graph elements."
+ ) in criteria
+ assert "Vertex/edge import reaches `APPLIED`" not in criteria
+
+
+def test_source_launch_contract_binds_both_checkout_packages():
+ english = _section(_text(README), "## Developer Notes", next_level=2)
+ chinese = _section(_text(README_ZH), "## 开发者说明", next_level=2)
+ checklist = _section(_text(CHECKLIST), "## 1. Start an Isolated Server", next_level=2)
+
+ for section in (english, chinese, checklist):
+ assert "/hugegraph-mcp:/Users/uleng/Code/hugegraph-ai-pr73-mcp/hugegraph-python-client/src" in section
+ assert ("/Users/uleng/Code/hugegraph-ai-pr73-mcp/.venv/bin/python -m hugegraph_mcp.server") in section
+ assert "uv run --project hugegraph-mcp hugegraph-mcp" not in section
+
+ assert "uv run --project .../hugegraph-mcp" in checklist
+ assert "Do not replace it with" in checklist
+
+
+def test_public_docs_fail_closed_for_other_unproved_write_boundaries():
+ english = _text(README)
+ chinese = _text(README_ZH)
+ checklist = _text(CHECKLIST)
+
+ assert "Property mutation is preview-only" in english
+ assert "Isolated vertex deletion is preview-only" in english
+ assert "属性变更仅支持预览" in chinese
+ assert "孤立点删除仅支持预览" in chinese
+ assert "data.preview_only=true" in checklist
+ assert "Each call below must return `FEATURE_DISABLED`" in checklist
+
+
+def test_public_docs_keep_legacy_locator_all_or_nothing():
+ expected = ("plan_hash", "nonce", "expires_at")
+ for path in PUBLIC_DOCS:
+ content = _text(path)
+ assert all(field in content for field in expected), path
+ assert "LEGACY_CONFIRMATION_DEPRECATED" in content, path
+
+ checklist = _text(CHECKLIST)
+ assert "only when all three are supplied together" in checklist
+ assert "A partial legacy locator must return `VALIDATION_ERROR`" in checklist
diff --git a/hugegraph-mcp/tests/test_query_graph_data_tool.py b/hugegraph-mcp/tests/test_query_graph_data_tool.py
index 1d328787..d593688d 100644
--- a/hugegraph-mcp/tests/test_query_graph_data_tool.py
+++ b/hugegraph-mcp/tests/test_query_graph_data_tool.py
@@ -30,10 +30,10 @@ class Edge:
id = "edge-1"
label = "knows"
type = "edge"
- outV = "1:alice"
- outVLabel = "person"
- inV = "1:bob"
- inVLabel = "person"
+ outV = "1:alice" # noqa: N815 - HugeGraph wire field
+ outVLabel = "person" # noqa: N815 - HugeGraph wire field
+ inV = "1:bob" # noqa: N815 - HugeGraph wire field
+ inVLabel = "person" # noqa: N815 - HugeGraph wire field
properties: ClassVar[dict[str, int]] = {"since": 2024}
@@ -57,12 +57,8 @@ class FakeGraphManager:
self.calls.append(("getVertexByCondition", label, limit, page, properties))
return [Vertex()]
- def getVertexByConditionWithPage(
- self, label="", limit=0, page=None, properties=None
- ):
- self.calls.append(
- ("getVertexByConditionWithPage", label, limit, page, properties)
- )
+ def getVertexByConditionWithPage(self, label="", limit=0, page=None, properties=None):
+ self.calls.append(("getVertexByConditionWithPage", label, limit, page, properties))
return [Vertex()], "condition-next"
def getEdgeById(self, edge_id):
@@ -82,9 +78,7 @@ class FakeGraphManager:
page=None,
properties=None,
):
- self.calls.append(
- ("getEdgeByPage", label, vertex_id, direction, limit, page, properties)
- )
+ self.calls.append(("getEdgeByPage", label, vertex_id, direction, limit, page, properties))
return [Edge()], "edge-next"
@@ -240,9 +234,7 @@ def test_query_rejects_limit_over_500():
],
)
@pytest.mark.parametrize("limit", ["abc", 0, -1, query_module.MAX_LIMIT + 1])
-def test_query_by_id_operations_reject_invalid_limit_before_manager_call(
- monkeypatch, operation, operation_args, limit
-):
+def test_query_by_id_operations_reject_invalid_limit_before_manager_call(monkeypatch, operation, operation_args, limit):
manager = FakeGraphManager()
manager_factory = Mock(return_value=manager)
monkeypatch.setattr(query_module, "_graph_manager", manager_factory)
@@ -317,9 +309,7 @@ def test_query_edge_page_by_vertex_without_page(monkeypatch):
assert result["ok"] is True
assert result["data"]["next_page"] == "edge-next"
- assert manager.calls == [
- ("getEdgeByPage", "knows", "1:alice", "OUT", 5, None, None)
- ]
+ assert manager.calls == [("getEdgeByPage", "knows", "1:alice", "OUT", 5, None, None)]
def test_query_edge_page_by_vertex_allows_empty_page(monkeypatch):
@@ -370,9 +360,7 @@ def test_query_vertex_condition_returns_next_page(monkeypatch):
assert result["ok"] is True
assert result["data"]["next_page"] == "condition-next"
- assert manager.calls == [
- ("getVertexByConditionWithPage", "person", 5, "p1", {"name": "Alice"})
- ]
+ assert manager.calls == [("getVertexByConditionWithPage", "person", 5, "p1", {"name": "Alice"})]
def test_query_vertex_condition_supports_legacy_client_without_page(monkeypatch):
@@ -418,9 +406,7 @@ def test_query_no_index_returns_no_index(monkeypatch):
def test_query_not_indexed_message_returns_no_index(monkeypatch):
class BrokenManager:
def getVertexByCondition(self, **kwargs):
- raise RuntimeError(
- "The property key 'name' is not indexed and may not match secondary condition"
- )
+ raise RuntimeError("The property key 'name' is not indexed and may not match secondary condition")
monkeypatch.setattr(query_module, "_graph_manager", lambda: BrokenManager())
diff --git a/hugegraph-mcp/tests/test_reconciler.py b/hugegraph-mcp/tests/test_reconciler.py
index e651162c..70f6a0a7 100644
--- a/hugegraph-mcp/tests/test_reconciler.py
+++ b/hugegraph-mcp/tests/test_reconciler.py
@@ -1,11 +1,20 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
import sqlite3
import pytest
+
from hugegraph_mcp.envelope import envelope_ok
from hugegraph_mcp.plan_store import PlanTransitionError, SQLitePlanStore
from hugegraph_mcp.reconciler import (
diff --git a/hugegraph-mcp/tests/test_refresh_vid_embeddings.py b/hugegraph-mcp/tests/test_refresh_vid_embeddings.py
index 74fd72c8..04067108 100644
--- a/hugegraph-mcp/tests/test_refresh_vid_embeddings.py
+++ b/hugegraph-mcp/tests/test_refresh_vid_embeddings.py
@@ -19,11 +19,7 @@ from hugegraph_mcp.tools import refresh_vid_embeddings as refresh_vid_embeddings
def test_refresh_vid_embeddings_success(monkeypatch):
monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
- post = Mock(
- return_value=envelope_ok(
- {"ok": True, "data": {"added": 3, "removed": 1, "summary": "done"}}
- )
- )
+ post = Mock(return_value=envelope_ok({"ok": True, "data": {"added": 3, "removed": 1, "summary": "done"}}))
monkeypatch.setattr(refresh_vid_embeddings_module, "post", post)
result = refresh_vid_embeddings_module.refresh_vid_embeddings(confirm=True)
diff --git a/hugegraph-mcp/tests/test_schema_utils.py b/hugegraph-mcp/tests/test_schema_utils.py
index aff226ca..d5da1382 100644
--- a/hugegraph-mcp/tests/test_schema_utils.py
+++ b/hugegraph-mcp/tests/test_schema_utils.py
@@ -110,9 +110,7 @@ def test_property_cardinalities_supports_wrappers_and_field_aliases():
}
) == {"name": "SINGLE", "aliases": "LIST", "tags": "SET"}
- assert property_cardinalities(
- {"propertykeys": [{"name": "values", "cardinality": "LIST"}]}
- ) == {"values": "LIST"}
+ assert property_cardinalities({"propertykeys": [{"name": "values", "cardinality": "LIST"}]}) == {"values": "LIST"}
def test_normalized_schema_summary_binds_property_key_cardinality_aliases():
@@ -128,9 +126,7 @@ def test_normalized_schema_summary_binds_property_key_cardinality_aliases():
]
}
}
- )["propertykeys"] == [
- {"name": "aliases", "data_type": "TEXT", "cardinality": "LIST"}
- ]
+ )["propertykeys"] == [{"name": "aliases", "data_type": "TEXT", "cardinality": "LIST"}]
def test_normalized_schema_summary_binds_supported_schema_fields():
diff --git a/hugegraph-mcp/tests/test_schema_write_workflow.py b/hugegraph-mcp/tests/test_schema_write_workflow.py
index f5a98a1c..66a864c0 100644
--- a/hugegraph-mcp/tests/test_schema_write_workflow.py
+++ b/hugegraph-mcp/tests/test_schema_write_workflow.py
@@ -1,11 +1,20 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
import sqlite3
import pytest
+
from hugegraph_mcp.plan_store import plan_store_from_config
from hugegraph_mcp.reconciler import reconcile_write
from hugegraph_mcp.tools import manage_schema as manage_schema_module
diff --git a/hugegraph-mcp/tests/test_v1_stable_tools.py b/hugegraph-mcp/tests/test_v1_stable_tools.py
index 7eb9ad01..4580b7bf 100644
--- a/hugegraph-mcp/tests/test_v1_stable_tools.py
+++ b/hugegraph-mcp/tests/test_v1_stable_tools.py
@@ -20,6 +20,7 @@ from unittest.mock import Mock
import pytest
from fastmcp import Client
+
from hugegraph_mcp import server
from hugegraph_mcp.envelope import envelope_ok
from hugegraph_mcp.write_contract import LEGACY_DEPRECATION_CODE
diff --git a/hugegraph-mcp/tests/test_write_confirmation_contract.py b/hugegraph-mcp/tests/test_write_confirmation_contract.py
index 927d427a..c7a4693d 100644
--- a/hugegraph-mcp/tests/test_write_confirmation_contract.py
+++ b/hugegraph-mcp/tests/test_write_confirmation_contract.py
@@ -1,13 +1,22 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
import inspect
import json
from pathlib import Path
import pytest
+
from hugegraph_mcp import server
from hugegraph_mcp.write_contract import (
LEGACY_DEPRECATION_CODE,
diff --git a/hugegraph-mcp/tests/test_write_executor_faults.py b/hugegraph-mcp/tests/test_write_executor_faults.py
index 8abb894f..d09aafdc 100644
--- a/hugegraph-mcp/tests/test_write_executor_faults.py
+++ b/hugegraph-mcp/tests/test_write_executor_faults.py
@@ -1,7 +1,15 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
"""Fault-injection coverage for durable confirmed-write execution."""
@@ -10,6 +18,7 @@ from __future__ import annotations
from collections.abc import Callable
import pytest
+
from hugegraph_mcp.plan_store import PlanTransitionError, SQLitePlanStore
from hugegraph_mcp.reconciler import ReconcileReaderRegistry, WriteReconciler
from hugegraph_mcp.write_executor import WriteExecutor, WriteExecutorRegistry
diff --git a/hugegraph-mcp/tests/test_write_plan.py b/hugegraph-mcp/tests/test_write_plan.py
index 01f89227..977cc617 100644
--- a/hugegraph-mcp/tests/test_write_plan.py
+++ b/hugegraph-mcp/tests/test_write_plan.py
@@ -1,11 +1,20 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
from dataclasses import FrozenInstanceError, replace
import pytest
+
from hugegraph_mcp.write_plan import (
ALLOWED_PLAN_TRANSITIONS,
ApplyReceipt,
@@ -191,7 +200,7 @@ def test_plan_transition_matrix_is_complete_and_fail_closed():
PlanStatus.EXPIRED: set(),
}
- assert ALLOWED_PLAN_TRANSITIONS == expected
+ assert expected == ALLOWED_PLAN_TRANSITIONS
for current in PlanStatus:
for target in PlanStatus:
assert can_transition(current, target) is (target in expected[current])
diff --git a/hugegraph-python-client/src/pyhugegraph/api/property_cas.py b/hugegraph-python-client/src/pyhugegraph/api/property_cas.py
index 1dc6a1de..8066eea7 100644
--- a/hugegraph-python-client/src/pyhugegraph/api/property_cas.py
+++ b/hugegraph-python-client/src/pyhugegraph/api/property_cas.py
@@ -1,7 +1,15 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
"""Narrow protocol for a future backend-enforced property CAS primitive.
diff --git a/hugegraph-python-client/src/tests/api/test_property_cas_contract.py b/hugegraph-python-client/src/tests/api/test_property_cas_contract.py
index 6e9884dd..a1bc4909 100644
--- a/hugegraph-python-client/src/tests/api/test_property_cas_contract.py
+++ b/hugegraph-python-client/src/tests/api/test_property_cas_contract.py
@@ -1,7 +1,15 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
import inspect
import json
diff --git a/hugegraph-python-client/src/tests/api/test_raw_gremlin_contract.py b/hugegraph-python-client/src/tests/api/test_raw_gremlin_contract.py
index 2af4791e..44318706 100644
--- a/hugegraph-python-client/src/tests/api/test_raw_gremlin_contract.py
+++ b/hugegraph-python-client/src/tests/api/test_raw_gremlin_contract.py
@@ -1,7 +1,15 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
import inspect
import json
diff --git a/pyproject.toml b/pyproject.toml
index a89e7639..8cfb2d37 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -183,6 +183,7 @@ ignore = [
"N802", # invalid-function-name (API compatibility)
"C901", # complexity (non-critical for now)
"RUF001", # ambiguous-unicode-character-string
+ "RUF002", # ambiguous-unicode-character-docstring (Chinese prose)
"RUF003", # ambiguous-unicode-character-comment
]
# No need to ignore E501 (line-too-long), `ruff format` will handle it automatically.