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/10/13 07:35:16 UTC

(hugegraph-ai) branch main updated: refactor(mcp): align toolsets and harden AI refresh contracts (#382)

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


The following commit(s) were added to refs/heads/main by this push:
     new 84ee9c4b refactor(mcp): align toolsets and harden AI refresh contracts (#382)
84ee9c4b is described below

commit 84ee9c4be722a34b8f9effb207a1e62130035774
Author: Yifeng Du <13...@users.noreply.github.com>
AuthorDate: Tue Oct 13 15:35:11 2026 +0800

    refactor(mcp): align toolsets and harden AI refresh contracts (#382)
    
    MCP refresh can use a different AI-service graph from the MCP target,
    while the toolset names and configuration guidance blur released and
    source behavior. In addition, login parsing differs between
    authentication and the Thin API write gate, and UI configuration reload
    can change the target during a refresh.
---
 README.md                                          |   2 +-
 .../src/hugegraph_llm/api/models/rag_requests.py   |  11 +-
 hugegraph-llm/src/hugegraph_llm/api/thin_api.py    |  49 +++-
 .../src/hugegraph_llm/config/admin_config.py       |   4 +
 .../src/hugegraph_llm/demo/rag_demo/app.py         |  46 ++--
 .../src/hugegraph_llm/models/embeddings/litellm.py |  32 ++-
 .../src/hugegraph_llm/models/embeddings/openai.py  |   8 +-
 .../index_op/gremlin_example_index_query.py        |  20 +-
 hugegraph-llm/src/tests/api/test_thin_api.py       | 272 ++++++++++++++++++++-
 .../models/embeddings/test_litellm_embedding.py    |  68 ++++++
 .../index_op/test_gremlin_example_index_query.py   | 188 +++++++++++++-
 hugegraph-mcp/README.md                            | 119 +++++++--
 hugegraph-mcp/README.zh-CN.md                      | 100 ++++++--
 hugegraph-mcp/docs/architecture.md                 |  27 +-
 hugegraph-mcp/docs/integration.md                  |  57 ++++-
 hugegraph-mcp/docs/releasing.md                    |  19 +-
 hugegraph-mcp/examples/stdio_check.py              |  89 +++++++
 hugegraph-mcp/hugegraph_mcp/config.py              |   4 +
 hugegraph-mcp/hugegraph_mcp/server.py              |  48 ++--
 hugegraph-mcp/hugegraph_mcp/tools/inspect_graph.py |   9 +-
 .../hugegraph_mcp/tools/inspect_schema.py          |   2 +-
 hugegraph-mcp/hugegraph_mcp/tools/manage_schema.py |   2 +-
 .../hugegraph_mcp/tools/mutate_graph_properties.py |   2 +-
 .../hugegraph_mcp/tools/query_graph_data.py        |   2 +-
 .../hugegraph_mcp/tools/refresh_vid_embeddings.py  |  55 ++++-
 hugegraph-mcp/hugegraph_mcp/toolsets.py            |  43 ++++
 hugegraph-mcp/tests/distribution_smoke.py          |   6 +-
 hugegraph-mcp/tests/test_config.py                 |   8 +
 hugegraph-mcp/tests/test_inspect_graph.py          |   4 +-
 hugegraph-mcp/tests/test_public_docs_contract.py   |   8 +-
 hugegraph-mcp/tests/test_readonly_mode.py          |  14 +-
 hugegraph-mcp/tests/test_refresh_vid_embeddings.py | 144 ++++++++++-
 hugegraph-mcp/tests/test_stdio_check.py            | 117 +++++++++
 hugegraph-mcp/tests/test_v1_stable_tools.py        | 150 ++++++++----
 skills/hugegraph-data-importer/SKILL.md            |   4 +-
 skills/hugegraph-operator/SKILL.md                 |   8 +-
 skills/hugegraph-operator/agents/openai.yaml       |   2 +-
 skills/hugegraph-query-analyst/SKILL.md            |   4 +-
 skills/hugegraph-regression-tester/SKILL.md        |  23 +-
 skills/hugegraph-schema-designer/SKILL.md          |  10 +-
 40 files changed, 1513 insertions(+), 267 deletions(-)

diff --git a/README.md b/README.md
index ada6289f..0a915da5 100644
--- a/README.md
+++ b/README.md
@@ -112,7 +112,7 @@ Model Context Protocol server for safe, controlled HugeGraph access:
 
 - **Stable Tool Contract**: Typed graph, schema, Gremlin, and extraction tools for MCP clients
 - **Safe Writes**: Read-only defaults plus dry-run, persistent single-use confirmation, and target revalidation
-- **Compatibility**: Default `v2_core` toolset with an opt-in `v1` compatibility mode
+- **Compatibility**: Default `core` toolset with an opt-in `legacy` compatibility mode in this checkout; published `1.7.1` uses `v2_core` / `v1`
 
 ### [hugegraph-ml](./hugegraph-ml)
 
diff --git a/hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py b/hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py
index e7739421..a06a4fa1 100644
--- a/hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py
+++ b/hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py
@@ -186,7 +186,16 @@ class GraphImportRequest(BaseModel):
 
 
 class VidEmbeddingsRefreshRequest(BaseModel):
-    pass
+    # Empty bodies remain compatible with authenticated legacy callers. Clients
+    # requesting target validation must explicitly provide both fields. Null or
+    # blank graphspace identifies DEFAULT without changing stored configuration.
+    expected_graph: Optional[str] = Field(default=None, min_length=1)
+    expected_graphspace: Optional[str] = None
+
+
+class ValidatedVidEmbeddingsRefreshRequest(VidEmbeddingsRefreshRequest):
+    expected_graph: str = Field(min_length=1)
+    expected_graphspace: Optional[str] = Field(...)
 
 
 class GraphIndexInfoRequest(BaseModel):
diff --git a/hugegraph-llm/src/hugegraph_llm/api/thin_api.py b/hugegraph-llm/src/hugegraph_llm/api/thin_api.py
index 80eea09c..bb4c3f3e 100644
--- a/hugegraph-llm/src/hugegraph_llm/api/thin_api.py
+++ b/hugegraph-llm/src/hugegraph_llm/api/thin_api.py
@@ -25,10 +25,11 @@ from fastapi import APIRouter, status
 from hugegraph_llm.api.models.rag_requests import (
     GraphExtractRequest,
     GraphImportRequest,
+    ValidatedVidEmbeddingsRefreshRequest,
     VidEmbeddingsRefreshRequest,
 )
 from hugegraph_llm.api.models.rag_response import ThinAPIResponse
-from hugegraph_llm.config import admin_settings, prompt
+from hugegraph_llm.config import admin_settings, huge_settings, prompt, runtime_config_lock
 from hugegraph_llm.flows import FlowName
 from hugegraph_llm.flows.scheduler import SchedulerSingleton
 from hugegraph_llm.utils.log import log
@@ -111,7 +112,7 @@ def _wrap_flow_call(flow_name: FlowName, *args: Any, **kwargs: Any) -> dict[str,
 
 def _thin_write_disabled() -> dict[str, Any] | None:
     """Keep legacy write endpoints fail-closed unless authenticated and enabled."""
-    login_enabled = str(admin_settings.enable_login).strip().lower() == "true"
+    login_enabled = admin_settings.is_login_enabled()
     configured_write_flag = os.getenv("HUGEGRAPH_LLM_ENABLE_THIN_WRITES", "false")
     writes_enabled = str(configured_write_flag).strip().lower() == "true"
     if login_enabled and writes_enabled:
@@ -150,11 +151,49 @@ def graph_import_api(req: GraphImportRequest):
     return _wrap_flow_call(FlowName.IMPORT_GRAPH_DATA, req.data, req.graph_schema)
 
 
-@thin_router.post("/vid-embeddings/refresh", status_code=status.HTTP_200_OK, response_model=ThinAPIResponse)
-def vid_embeddings_refresh_api(_req: VidEmbeddingsRefreshRequest):
+def _effective_graphspace(graphspace: str | None) -> str:
+    # Match the client's default-space identity without changing configuration
+    # or index paths. Non-default names retain their exact routing semantics.
+    return graphspace if graphspace and graphspace.strip() else "DEFAULT"
+
+
+def _refresh_vid_embeddings(req: VidEmbeddingsRefreshRequest):
     if blocked := _thin_write_disabled():
         return blocked
-    return _wrap_flow_call(FlowName.UPDATE_VID_EMBEDDINGS)
+    # Keep target validation and flow execution atomic with respect to runtime
+    # graph configuration updates. The flow reads the same shared settings.
+    with runtime_config_lock:
+        expected_fields = {"expected_graph", "expected_graphspace"}
+        supplied_fields = req.model_fields_set & expected_fields
+        if supplied_fields and (
+            supplied_fields != expected_fields
+            or req.expected_graph != huge_settings.graph_name
+            or _effective_graphspace(req.expected_graphspace) != _effective_graphspace(huge_settings.graph_space)
+        ):
+            return _envelope_err(
+                "VALIDATION_ERROR",
+                "Expected graph and graphspace must match the configured HugeGraph target.",
+                suggestion="Provide both expected_graph and expected_graphspace matching the AI service target.",
+            )
+        return _wrap_flow_call(FlowName.UPDATE_VID_EMBEDDINGS)
+
+
+@thin_router.post("/vid-embeddings/refresh", status_code=status.HTTP_200_OK, response_model=ThinAPIResponse)
+def vid_embeddings_refresh_api(req: VidEmbeddingsRefreshRequest):
+    """Keep empty requests compatible with authenticated legacy callers."""
+    return _refresh_vid_embeddings(req)
+
+
+@thin_router.post("/v1/vid-embeddings/refresh", status_code=status.HTTP_200_OK, response_model=ThinAPIResponse)
+def validated_vid_embeddings_refresh_api(req: ValidatedVidEmbeddingsRefreshRequest):
+    """Require target validation on a route that old replicas cannot execute."""
+    return _refresh_vid_embeddings(req)
+
+
+@thin_router.get("/vid-embeddings/refresh-capabilities", status_code=status.HTTP_200_OK, response_model=ThinAPIResponse)
+def vid_embeddings_refresh_capabilities_api():
+    """Keep discovery compatible; the versioned POST enforces validation."""
+    return _envelope_ok({"protocol_version": 1})
 
 
 @thin_router.get("/graph-index-info", status_code=status.HTTP_200_OK, response_model=ThinAPIResponse)
diff --git a/hugegraph-llm/src/hugegraph_llm/config/admin_config.py b/hugegraph-llm/src/hugegraph_llm/config/admin_config.py
index 5f6ed576..1ce50e79 100644
--- a/hugegraph-llm/src/hugegraph_llm/config/admin_config.py
+++ b/hugegraph-llm/src/hugegraph_llm/config/admin_config.py
@@ -26,3 +26,7 @@ class AdminConfig(BaseConfig):
     enable_login: Optional[str] = "False"
     user_token: Optional[str] = "4321"
     admin_token: Optional[str] = "xxxx"
+
+    def is_login_enabled(self) -> bool:
+        """Use the same login flag semantics for routing and write protection."""
+        return str(self.enable_login).strip().lower() == "true"
diff --git a/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/app.py b/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/app.py
index 526b2364..bfa37a03 100644
--- a/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/app.py
+++ b/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/app.py
@@ -28,7 +28,7 @@ from hugegraph_llm.api.admin_api import admin_http_api
 from hugegraph_llm.api.graph_extract_api import graph_extract_http_api
 from hugegraph_llm.api.rag_api import rag_http_api
 from hugegraph_llm.api.thin_api import thin_router
-from hugegraph_llm.config import admin_settings, huge_settings, prompt
+from hugegraph_llm.config import admin_settings, huge_settings, prompt, runtime_config_lock
 from hugegraph_llm.demo.rag_demo.admin_block import create_admin_block, log_stream
 from hugegraph_llm.demo.rag_demo.configs_block import (
     apply_embedding_config,
@@ -66,7 +66,7 @@ def authenticate(credentials: HTTPAuthorizationCredentials = Depends(sec)):
 
 def create_api_router() -> tuple[APIRouter, bool]:
     """Build the production API router, including its authentication boundary."""
-    auth_enabled = admin_settings.enable_login.lower() == "true"
+    auth_enabled = admin_settings.is_login_enabled()
     log.info("(Status) Authentication is %s now.", "enabled" if auth_enabled else "disabled")
     api_router = APIRouter(dependencies=[Depends(authenticate)] if auth_enabled else [])
     api_router.include_router(thin_router)
@@ -122,27 +122,27 @@ def init_rag_ui() -> gr.Interface:
             create_admin_block()
 
         def refresh_ui_config_prompt() -> tuple:
-            # we can use its __init__() for in-place reload
-            # settings.from_env()
-            huge_settings.__init__()  # type: ignore[misc] # pylint: disable=C2801
-            prompt.ensure_yaml_file_exists()
-            return (
-                huge_settings.graph_url,
-                huge_settings.graph_name,
-                huge_settings.graph_user,
-                huge_settings.graph_pwd,
-                huge_settings.graph_space,
-                prompt.doc_input_text,
-                prompt.graph_schema,
-                prompt.extract_graph_prompt,
-                prompt.default_question,
-                prompt.answer_prompt,
-                prompt.keywords_extract_prompt,
-                prompt.custom_rerank_info,
-                prompt.default_question,
-                huge_settings.graph_name,
-                prompt.gremlin_generate_prompt,
-            )
+            # Reload and read the UI snapshot under the same lock as graph writes.
+            with runtime_config_lock:
+                huge_settings.__init__()  # type: ignore[misc] # pylint: disable=C2801
+                prompt.ensure_yaml_file_exists()
+                return (
+                    huge_settings.graph_url,
+                    huge_settings.graph_name,
+                    huge_settings.graph_user,
+                    huge_settings.graph_pwd,
+                    huge_settings.graph_space,
+                    prompt.doc_input_text,
+                    prompt.graph_schema,
+                    prompt.extract_graph_prompt,
+                    prompt.default_question,
+                    prompt.answer_prompt,
+                    prompt.keywords_extract_prompt,
+                    prompt.custom_rerank_info,
+                    prompt.default_question,
+                    huge_settings.graph_name,
+                    prompt.gremlin_generate_prompt,
+                )
 
         hugegraph_llm_ui.load(  # pylint: disable=E1101
             fn=refresh_ui_config_prompt,
diff --git a/hugegraph-llm/src/hugegraph_llm/models/embeddings/litellm.py b/hugegraph-llm/src/hugegraph_llm/models/embeddings/litellm.py
index 06a590af..8301e361 100644
--- a/hugegraph-llm/src/hugegraph_llm/models/embeddings/litellm.py
+++ b/hugegraph-llm/src/hugegraph_llm/models/embeddings/litellm.py
@@ -80,18 +80,28 @@ class LiteLLMEmbedding(BaseEmbedding):
             A list of embedding vectors.
         """
         all_embeddings = []
+        for i in range(0, len(texts), batch_size):
+            batch = texts[i : i + batch_size]
+            all_embeddings.extend(self._get_batch_embeddings(batch))
+        return all_embeddings
+
+    @retry(
+        stop=stop_after_attempt(3),
+        wait=wait_exponential(multiplier=1, min=4, max=10),
+        retry=retry_if_exception_type((RateLimitError, APIConnectionError, APIError)),
+        reraise=True,
+    )
+    def _get_batch_embeddings(self, texts: List[str]) -> List[List[float]]:
+        """Retry one batch without repeating completed batches."""
         try:
-            for i in range(0, len(texts), batch_size):
-                batch = texts[i : i + batch_size]
-                response = embedding(
-                    model=self.model,
-                    input=batch,
-                    api_key=self.api_key,
-                    api_base=self.api_base,
-                )
-                log.info("Token usage: %s", response.usage)
-                all_embeddings.extend([data["embedding"] for data in response.data])
-            return all_embeddings
+            response = embedding(
+                model=self.model,
+                input=texts,
+                api_key=self.api_key,
+                api_base=self.api_base,
+            )
+            log.info("Token usage: %s", response.usage)
+            return [data["embedding"] for data in response.data]
         except (RateLimitError, APIConnectionError, APIError) as e:
             log.error("Error in LiteLLM batch embedding call: %s", e)
             raise
diff --git a/hugegraph-llm/src/hugegraph_llm/models/embeddings/openai.py b/hugegraph-llm/src/hugegraph_llm/models/embeddings/openai.py
index 0d0058cd..2af81b7c 100644
--- a/hugegraph-llm/src/hugegraph_llm/models/embeddings/openai.py
+++ b/hugegraph-llm/src/hugegraph_llm/models/embeddings/openai.py
@@ -47,7 +47,7 @@ class OpenAIEmbedding(BaseEmbedding):
         response = self.client.embeddings.create(input=text, model=self.model)
         return response.data[0].embedding
 
-    def get_texts_embeddings(self, texts: List[str], batch_size: int = 32) -> List[List[float]]:
+    def get_texts_embeddings(self, texts: List[str], batch_size: int = 10) -> List[List[float]]:
         """Get embeddings for multiple texts with automatic batch splitting.
 
         This method efficiently processes multiple texts by splitting them into
@@ -58,7 +58,7 @@ class OpenAIEmbedding(BaseEmbedding):
         texts : List[str]
             A list of text strings to be embedded.
         batch_size : int, optional
-            Maximum number of texts to process in a single API call (default: 32).
+            Maximum number of texts to process in a single API call (default: 10).
 
         Returns
         -------
@@ -73,7 +73,7 @@ class OpenAIEmbedding(BaseEmbedding):
             all_embeddings.extend([data.embedding for data in response.data])
         return all_embeddings
 
-    async def async_get_texts_embeddings(self, texts: List[str], batch_size: int = 32) -> List[List[float]]:
+    async def async_get_texts_embeddings(self, texts: List[str], batch_size: int = 10) -> List[List[float]]:
         """Get embeddings for multiple texts with automatic batch splitting (async).
 
         This method should efficiently process multiple texts at once by leveraging
@@ -85,7 +85,7 @@ class OpenAIEmbedding(BaseEmbedding):
         texts : List[str]
             A list of text strings to be embedded.
         batch_size : int, optional
-            Maximum number of texts to process in a single API call (default: 32).
+            Maximum number of texts to process in a single API call (default: 10).
 
         Returns
         -------
diff --git a/hugegraph-llm/src/hugegraph_llm/operators/index_op/gremlin_example_index_query.py b/hugegraph-llm/src/hugegraph_llm/operators/index_op/gremlin_example_index_query.py
index 345db47a..5c1e5c6c 100644
--- a/hugegraph-llm/src/hugegraph_llm/operators/index_op/gremlin_example_index_query.py
+++ b/hugegraph-llm/src/hugegraph_llm/operators/index_op/gremlin_example_index_query.py
@@ -20,7 +20,6 @@ import os
 from typing import Any, Dict, List, Optional
 
 import pandas as pd
-from tqdm import tqdm
 
 from hugegraph_llm.config import resource_path
 from hugegraph_llm.indices.vector_index.base import VectorStoreBase
@@ -56,16 +55,15 @@ class GremlinExampleIndexQuery:
 
     def _build_default_example_index(self):
         properties = pd.read_csv(os.path.join(resource_path, "demo", "text2gremlin.csv")).to_dict(orient="records")
-        from concurrent.futures import ThreadPoolExecutor
-
-        # TODO: reuse the logic in build_semantic_index.py (consider extract the batch-embedding method)
-        with ThreadPoolExecutor() as executor:
-            embeddings = list(
-                tqdm(
-                    executor.map(self.embedding.get_text_embedding, [row["query"] for row in properties]),
-                    total=len(properties),
-                )
-            )
+        # Some supported services accept at most 10 texts, regardless of the adapter default.
+        # Complete each batch sequentially and only publish the index after all batches succeed.
+        embeddings = []
+        for start in range(0, len(properties), 10):
+            texts = [row["query"] for row in properties[start : start + 10]]
+            batch_embeddings = self.embedding.get_texts_embeddings(texts)
+            if len(batch_embeddings) != len(texts):
+                raise ValueError(f"Embedding batch returned {len(batch_embeddings)} vectors for {len(texts)} texts.")
+            embeddings.extend(batch_embeddings)
         self.vector_index.add(embeddings, properties)
         self.vector_index.save_index_by_name("gremlin_examples")
 
diff --git a/hugegraph-llm/src/tests/api/test_thin_api.py b/hugegraph-llm/src/tests/api/test_thin_api.py
index 94285f0d..d9f2f2ad 100644
--- a/hugegraph-llm/src/tests/api/test_thin_api.py
+++ b/hugegraph-llm/src/tests/api/test_thin_api.py
@@ -16,7 +16,10 @@
 # under the License.
 
 import importlib
+import os
 import warnings
+from concurrent.futures import ThreadPoolExecutor
+from threading import Event
 from unittest.mock import Mock
 
 import pytest
@@ -198,11 +201,227 @@ def test_vid_embeddings_refresh_api_calls_flow(monkeypatch):
     scheduler.schedule_flow.assert_called_once_with(FlowName.UPDATE_VID_EMBEDDINGS)
 
 
+@pytest.mark.parametrize(
+    ("graphspace", "payload", "expected_ok"),
+    [
+        (None, {"expected_graph": "target", "expected_graphspace": None}, True),
+        ("space", {"expected_graph": "target", "expected_graphspace": "space"}, True),
+        (None, {"expected_graph": "other", "expected_graphspace": None}, False),
+        ("space", {"expected_graph": "target", "expected_graphspace": "other"}, False),
+        ("space", {"expected_graph": "target", "expected_graphspace": None}, False),
+        (None, {"expected_graph": "target", "expected_graphspace": "space"}, False),
+        (None, {"expected_graph": "target"}, False),
+        (None, {"expected_graphspace": None}, False),
+        (None, {"expected_graph": None, "expected_graphspace": None}, False),
+    ],
+)
+def test_vid_embeddings_refresh_validates_target_before_flow(monkeypatch, graphspace, payload, expected_ok):
+    scheduler = Mock()
+    scheduler.schedule_flow.return_value = "Refreshed."
+    monkeypatch.setattr("hugegraph_llm.api.thin_api.admin_settings.enable_login", "True")
+    monkeypatch.setattr("hugegraph_llm.api.thin_api.huge_settings.graph_name", "target")
+    monkeypatch.setattr("hugegraph_llm.api.thin_api.huge_settings.graph_space", graphspace)
+    monkeypatch.setenv("HUGEGRAPH_LLM_ENABLE_THIN_WRITES", "true")
+    client = _client(monkeypatch, scheduler)
+
+    response = client.post("/vid-embeddings/refresh", json=payload)
+
+    assert response.status_code == 200
+    _assert_envelope(response.json(), expected_ok=expected_ok)
+    if expected_ok:
+        scheduler.schedule_flow.assert_called_once_with(FlowName.UPDATE_VID_EMBEDDINGS)
+    else:
+        assert response.json()["error"]["type"] == "VALIDATION_ERROR"
+        scheduler.schedule_flow.assert_not_called()
+
+
+@pytest.mark.parametrize("path", ["/vid-embeddings/refresh", "/v1/vid-embeddings/refresh"])
+@pytest.mark.parametrize("configured_space", [None, "", "   ", "DEFAULT"])
+@pytest.mark.parametrize("expected_space", [None, "", "   ", "DEFAULT"])
+def test_refresh_normalizes_default_identity_without_mutating_settings(
+    monkeypatch, path, configured_space, expected_space
+):
+    from hugegraph_llm.api.thin_api import huge_settings
+
+    scheduler = Mock()
+    scheduler.schedule_flow.return_value = "Refreshed."
+    monkeypatch.setattr("hugegraph_llm.api.thin_api.admin_settings.enable_login", "true")
+    monkeypatch.setattr(huge_settings, "graph_name", "target")
+    monkeypatch.setattr(huge_settings, "graph_space", configured_space)
+    monkeypatch.setenv("HUGEGRAPH_LLM_ENABLE_THIN_WRITES", "true")
+    client = _client(monkeypatch, scheduler)
+
+    response = client.post(path, json={"expected_graph": "target", "expected_graphspace": expected_space})
+
+    assert response.status_code == 200
+    _assert_envelope(response.json(), expected_ok=True)
+    scheduler.schedule_flow.assert_called_once_with(FlowName.UPDATE_VID_EMBEDDINGS)
+    assert huge_settings.graph_space == configured_space
+
+
+@pytest.mark.parametrize(
+    ("configured_space", "expected_graph", "expected_space", "expected_ok"),
+    [
+        ("space", "target", "space", True),
+        ("space", "other", "space", False),
+        ("space", "target", "other", False),
+        ("space", "target", None, False),
+        (None, "target", "space", False),
+        ("space", "target", " space ", False),
+    ],
+)
+def test_validated_refresh_checks_target_before_flow(
+    monkeypatch, configured_space, expected_graph, expected_space, expected_ok
+):
+    scheduler = Mock()
+    scheduler.schedule_flow.return_value = "Refreshed."
+    monkeypatch.setattr("hugegraph_llm.api.thin_api.admin_settings.enable_login", "true")
+    monkeypatch.setattr("hugegraph_llm.api.thin_api.huge_settings.graph_name", "target")
+    monkeypatch.setattr("hugegraph_llm.api.thin_api.huge_settings.graph_space", configured_space)
+    monkeypatch.setenv("HUGEGRAPH_LLM_ENABLE_THIN_WRITES", "true")
+    client = _client(monkeypatch, scheduler)
+
+    response = client.post(
+        "/v1/vid-embeddings/refresh",
+        json={"expected_graph": expected_graph, "expected_graphspace": expected_space},
+    )
+
+    assert response.status_code == 200
+    _assert_envelope(response.json(), expected_ok=expected_ok)
+    if expected_ok:
+        scheduler.schedule_flow.assert_called_once_with(FlowName.UPDATE_VID_EMBEDDINGS)
+    else:
+        assert response.json()["error"]["type"] == "VALIDATION_ERROR"
+        scheduler.schedule_flow.assert_not_called()
+
+
+@pytest.mark.parametrize(
+    "payload",
+    [
+        {},
+        {"expected_graph": "target"},
+        {"expected_graphspace": "DEFAULT"},
+        {"expected_graph": None, "expected_graphspace": "DEFAULT"},
+        {"expected_graph": "", "expected_graphspace": "DEFAULT"},
+    ],
+)
+def test_validated_refresh_requires_target_fields(monkeypatch, payload):
+    scheduler = Mock()
+    monkeypatch.setattr("hugegraph_llm.api.thin_api.admin_settings.enable_login", "true")
+    monkeypatch.setenv("HUGEGRAPH_LLM_ENABLE_THIN_WRITES", "true")
+    client = _client(monkeypatch, scheduler)
+
+    response = client.post("/v1/vid-embeddings/refresh", json=payload)
+
+    assert response.status_code == 422
+    scheduler.schedule_flow.assert_not_called()
+
+
+def test_vid_embeddings_refresh_capabilities_has_no_flow_or_write(monkeypatch):
+    scheduler = Mock()
+    monkeypatch.delenv("HUGEGRAPH_LLM_ENABLE_THIN_WRITES", raising=False)
+    client = _client(monkeypatch, scheduler)
+
+    response = client.get("/vid-embeddings/refresh-capabilities")
+
+    assert response.status_code == 200
+    _assert_envelope(response.json(), expected_ok=True)
+    assert response.json()["data"] == {"protocol_version": 1}
+    scheduler.schedule_flow.assert_not_called()
+
+
+@pytest.mark.parametrize("path", ["/vid-embeddings/refresh", "/v1/vid-embeddings/refresh"])
+def test_ui_config_reload_waits_for_refresh_target(monkeypatch, tmp_path, path):
+    from hugegraph_llm.api import thin_api
+    from hugegraph_llm.config.models import base_config
+
+    # Exercise the callback actually registered by Gradio, not a copied function.
+    ui = rag_demo_app.init_rag_ui()
+    reload_config = next(fn.fn for fn in ui.fns.values() if fn.fn and fn.fn.__name__ == "refresh_ui_config_prompt")
+    env_path = tmp_path / "graph.env"
+    env_path.write_text("GRAPH_NAME=graph_a\nGRAPH_SPACE=DEFAULT\n", encoding="utf-8")
+    monkeypatch.setattr(base_config, "env_path", str(env_path))
+    # BaseConfig reload writes these environment variables; restore them after the test.
+    for field in type(thin_api.huge_settings).model_fields:
+        monkeypatch.setenv(field.upper(), os.environ.get(field.upper(), ""))
+    settings = type(thin_api.huge_settings)(_env_file=env_path)
+    monkeypatch.setattr(thin_api, "huge_settings", settings)
+    monkeypatch.setattr(rag_demo_app, "huge_settings", settings)
+    monkeypatch.setattr(prompt, "ensure_yaml_file_exists", Mock())
+    monkeypatch.setattr(rag_demo_app.admin_settings, "enable_login", "true")
+    monkeypatch.setattr(rag_demo_app.admin_settings, "user_token", "internal-token")
+    monkeypatch.setenv("HUGEGRAPH_LLM_ENABLE_THIN_WRITES", "true")
+    flow_entered, finish_flow = Event(), Event()
+    reload_started, reload_finished = Event(), Event()
+
+    def refresh_flow(*_args):
+        flow_entered.set()
+        assert finish_flow.wait(10)
+        return {"observed_graph": settings.graph_name}
+
+    def reload_ui():
+        reload_started.set()
+        try:
+            return reload_config()
+        finally:
+            reload_finished.set()
+
+    scheduler = Mock()
+    scheduler.schedule_flow.side_effect = refresh_flow
+    client = _production_router_client(monkeypatch, scheduler)
+    payload = {"expected_graph": "graph_a", "expected_graphspace": "DEFAULT"}
+    headers = {"Authorization": "Bearer internal-token"}
+    try:
+        with ThreadPoolExecutor(max_workers=2) as workers:
+            refresh = workers.submit(client.post, path, json=payload, headers=headers)
+            try:
+                assert flow_entered.wait(5)
+                env_path.write_text(
+                    env_path.read_text(encoding="utf-8").replace("GRAPH_NAME=graph_a", "GRAPH_NAME=graph_b"),
+                    encoding="utf-8",
+                )
+                reloaded = workers.submit(reload_ui)
+                assert reload_started.wait(5)
+                assert not reload_finished.wait(0.2), "UI reload bypassed the active refresh lock"
+                assert settings.graph_name == "graph_a"
+            finally:
+                finish_flow.set()
+            response = refresh.result(timeout=5)
+            assert response.status_code == 200
+            assert response.json()["data"] == {"observed_graph": "graph_a"}
+            assert reloaded.result(timeout=5)[1] == "graph_b"
+        # After the refresh finishes, reload takes effect and old-target requests fail.
+        assert settings.graph_name == "graph_b"
+        rejected = client.post(path, json=payload, headers=headers)
+        assert rejected.json()["error"]["type"] == "VALIDATION_ERROR"
+        scheduler.schedule_flow.assert_called_once_with(FlowName.UPDATE_VID_EMBEDDINGS)
+    finally:
+        client.close()
+        ui.close()
+
+
+def test_production_router_protects_refresh_capabilities(monkeypatch):
+    scheduler = Mock()
+    monkeypatch.setattr(rag_demo_app.admin_settings, "enable_login", "True")
+    monkeypatch.setattr(rag_demo_app.admin_settings, "user_token", "internal-token")
+    client = _production_router_client(monkeypatch, scheduler)
+
+    missing = client.get("/vid-embeddings/refresh-capabilities")
+    accepted = client.get("/vid-embeddings/refresh-capabilities", headers={"Authorization": "Bearer internal-token"})
+
+    assert missing.status_code in {401, 403}
+    assert accepted.status_code == 200
+    assert accepted.json()["data"] == {"protocol_version": 1}
+    scheduler.schedule_flow.assert_not_called()
+
+
 @pytest.mark.parametrize(
     ("path", "payload"),
     [
         ("/graph-import", {"data": "{}", "schema": None}),
         ("/vid-embeddings/refresh", {}),
+        ("/vid-embeddings/refresh", {"expected_graph": "target", "expected_graphspace": None}),
+        ("/v1/vid-embeddings/refresh", {"expected_graph": "target", "expected_graphspace": None}),
     ],
 )
 def test_thin_write_endpoints_are_disabled_by_default(monkeypatch, path, payload):
@@ -259,13 +478,23 @@ def test_thin_writes_remain_disabled_without_write_flag(monkeypatch):
             {},
             FlowName.UPDATE_VID_EMBEDDINGS,
         ),
+        (
+            "/v1/vid-embeddings/refresh",
+            {"expected_graph": "target", "expected_graphspace": "DEFAULT"},
+            FlowName.UPDATE_VID_EMBEDDINGS,
+        ),
     ],
 )
-def test_production_router_requires_bearer_for_enabled_thin_writes(monkeypatch, path, payload, expected_flow):
+@pytest.mark.parametrize("login_value", ["true", "TRUE", " true "])
+def test_production_router_requires_bearer_for_enabled_thin_writes(
+    monkeypatch, path, payload, expected_flow, login_value
+):
     scheduler = Mock()
     scheduler.schedule_flow.return_value = {"status": "ok"}
-    monkeypatch.setattr(rag_demo_app.admin_settings, "enable_login", "True")
+    monkeypatch.setattr(rag_demo_app.admin_settings, "enable_login", login_value)
     monkeypatch.setattr(rag_demo_app.admin_settings, "user_token", "internal-token")
+    monkeypatch.setattr("hugegraph_llm.api.thin_api.huge_settings.graph_name", "target")
+    monkeypatch.setattr("hugegraph_llm.api.thin_api.huge_settings.graph_space", "DEFAULT")
     monkeypatch.setenv("HUGEGRAPH_LLM_ENABLE_THIN_WRITES", "true")
     client = _production_router_client(monkeypatch, scheduler)
 
@@ -275,20 +504,51 @@ def test_production_router_requires_bearer_for_enabled_thin_writes(monkeypatch,
         json=payload,
         headers={"Authorization": "Bearer wrong-token"},
     )
+    assert missing.status_code in {401, 403}
+    assert invalid.status_code == 401
+    assert "wrong-token" not in invalid.text
+    scheduler.schedule_flow.assert_not_called()
     accepted = client.post(
         path,
         json=payload,
         headers={"Authorization": "Bearer internal-token"},
     )
-
-    assert missing.status_code in {401, 403}
-    assert invalid.status_code == 401
-    assert "wrong-token" not in invalid.text
     assert accepted.status_code == 200
     assert accepted.json()["ok"] is True
     assert scheduler.schedule_flow.call_count == 1
     assert scheduler.schedule_flow.call_args.args[0] == expected_flow
 
+    scheduler.schedule_flow.reset_mock()
+    monkeypatch.setenv("HUGEGRAPH_LLM_ENABLE_THIN_WRITES", "false")
+    disabled = client.post(path, json=payload, headers={"Authorization": "Bearer internal-token"})
+    assert disabled.status_code == 200
+    assert disabled.json()["ok"] is False
+    assert disabled.json()["error"]["type"] == "FEATURE_DISABLED"
+    scheduler.schedule_flow.assert_not_called()
+
+
+@pytest.mark.parametrize("login_value", ["false", " FALSE ", "invalid", "", None])
+@pytest.mark.parametrize("path", ["/graph-import", "/vid-embeddings/refresh", "/v1/vid-embeddings/refresh"])
+def test_production_router_blocks_thin_writes_when_login_disabled(monkeypatch, login_value, path):
+    scheduler = Mock()
+    monkeypatch.setattr(rag_demo_app.admin_settings, "enable_login", login_value)
+    monkeypatch.setenv("HUGEGRAPH_LLM_ENABLE_THIN_WRITES", "true")
+    monkeypatch.setattr("hugegraph_llm.api.thin_api.SchedulerSingleton.get_instance", Mock(return_value=scheduler))
+    router, auth_enabled = rag_demo_app.create_api_router()
+    assert auth_enabled is False
+    app = FastAPI()
+    app.include_router(router)
+    payload = (
+        {"data": "{}", "schema": None}
+        if path == "/graph-import"
+        else {"expected_graph": "target", "expected_graphspace": "DEFAULT"}
+    )
+    response = TestClient(app).post(path, json=payload)
+    assert response.status_code == 200
+    assert response.json()["ok"] is False
+    assert response.json()["error"]["type"] == "FEATURE_DISABLED"
+    scheduler.schedule_flow.assert_not_called()
+
 
 def test_graph_index_info_api_calls_flow(monkeypatch):
     scheduler = Mock()
diff --git a/hugegraph-llm/src/tests/models/embeddings/test_litellm_embedding.py b/hugegraph-llm/src/tests/models/embeddings/test_litellm_embedding.py
new file mode 100644
index 00000000..6cb97d8f
--- /dev/null
+++ b/hugegraph-llm/src/tests/models/embeddings/test_litellm_embedding.py
@@ -0,0 +1,68 @@
+# 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.
+
+from types import SimpleNamespace
+from unittest.mock import patch
+
+import pytest
+from litellm import APIConnectionError, APIError, RateLimitError
+
+from hugegraph_llm.models.embeddings.litellm import LiteLLMEmbedding
+
+pytestmark = pytest.mark.contract
+
+
+@pytest.mark.parametrize(
+    "error",
+    [
+        RateLimitError(message="temporary 429", llm_provider="hosted_vllm", model="test"),
+        APIConnectionError(message="temporary connection failure", llm_provider="hosted_vllm", model="test"),
+        APIError(status_code=500, message="temporary API failure", llm_provider="hosted_vllm", model="test"),
+    ],
+    ids=["rate_limit", "connection", "api_error"],
+)
+def test_batch_retry_preserves_completed_batches_and_order(error):
+    embedding = LiteLLMEmbedding(embedding_dimension=1, api_key="test", model_name="hosted_vllm/test")
+    batches = []
+
+    def respond(**kwargs):
+        texts = kwargs["input"]
+        batches.append(texts)
+        if len(batches) == 2:
+            raise error
+        return SimpleNamespace(data=[{"embedding": [float(text)]} for text in texts], usage=None)
+
+    with (
+        patch("hugegraph_llm.models.embeddings.litellm.embedding", side_effect=respond),
+        patch("tenacity.nap.time.sleep"),
+    ):
+        result = embedding.get_texts_embeddings(["0", "1", "2", "3", "4"], batch_size=2)
+
+    assert batches == [["0", "1"], ["2", "3"], ["2", "3"], ["4"]]
+    assert result == [[float(i)] for i in range(5)]
+
+
+def test_non_retryable_batch_error_is_not_retried():
+    embedding = LiteLLMEmbedding(embedding_dimension=1, api_key="test")
+    error = ValueError("invalid embedding input")
+
+    with patch("hugegraph_llm.models.embeddings.litellm.embedding", side_effect=error) as mock_embedding:
+        with pytest.raises(ValueError) as raised:
+            embedding.get_texts_embeddings(["test"])
+
+    assert raised.value is error
+    mock_embedding.assert_called_once()
diff --git a/hugegraph-llm/src/tests/operators/index_op/test_gremlin_example_index_query.py b/hugegraph-llm/src/tests/operators/index_op/test_gremlin_example_index_query.py
index 4ec869f8..66fabbb1 100644
--- a/hugegraph-llm/src/tests/operators/index_op/test_gremlin_example_index_query.py
+++ b/hugegraph-llm/src/tests/operators/index_op/test_gremlin_example_index_query.py
@@ -20,10 +20,14 @@
 import shutil
 import tempfile
 import unittest
+from types import SimpleNamespace
 from unittest.mock import MagicMock, Mock, patch
 
 import pandas as pd
+import pytest
 
+from hugegraph_llm.indices.vector_index.faiss_vector_store import FaissVectorIndex
+from hugegraph_llm.models.embeddings.litellm import LiteLLMEmbedding
 from hugegraph_llm.operators.index_op.gremlin_example_index_query import GremlinExampleIndexQuery
 
 
@@ -74,16 +78,14 @@ class TestGremlinExampleIndexQuery(unittest.TestCase):
 
     @patch("hugegraph_llm.operators.index_op.gremlin_example_index_query.resource_path", "/mock/path")
     @patch("pandas.read_csv")
-    @patch("concurrent.futures.ThreadPoolExecutor")
-    @patch("hugegraph_llm.operators.index_op.gremlin_example_index_query.tqdm")
     @patch("hugegraph_llm.operators.index_op.gremlin_example_index_query.log")
     @patch("os.path.join")
-    def test_init_without_existing_index(self, mock_join, mock_log, mock_tqdm, mock_thread_pool, mock_read_csv):
+    def test_init_without_existing_index(self, mock_join, mock_log, mock_read_csv):
         """Test initialization when index doesn't exist and needs to be built"""
         # Create mock embedding
         mock_embedding = Mock()
         mock_embedding.get_embedding_dim.return_value = self.embed_dim
-        mock_embedding.get_text_embedding.side_effect = lambda x: self.vectors[0] if "persons" in x else self.vectors[1]
+        mock_embedding.get_texts_embeddings.return_value = self.vectors
 
         # Create mock vector index class and instance
         mock_vector_index_class = MagicMock()
@@ -98,15 +100,12 @@ class TestGremlinExampleIndexQuery(unittest.TestCase):
         mock_df = pd.DataFrame(self.properties)
         mock_read_csv.return_value = mock_df
 
-        # Mock thread pool execution
-        mock_executor = MagicMock()
-        mock_thread_pool.return_value.__enter__.return_value = mock_executor
-        mock_executor.map.return_value = self.vectors
-        mock_tqdm.return_value = self.vectors
-
         # Create a GremlinExampleIndexQuery instance
         GremlinExampleIndexQuery(vector_index=mock_vector_index_class, embedding=mock_embedding, num_examples=1)
 
+        mock_embedding.get_texts_embeddings.assert_called_once_with([row["query"] for row in self.properties])
+        mock_embedding.get_text_embedding.assert_not_called()
+
         # Verify that the index was built
         mock_vector_index_class.exist.assert_called_once_with("gremlin_examples")
         mock_vector_index_class.from_name.assert_called_once_with(self.embed_dim, "gremlin_examples")
@@ -327,3 +326,172 @@ class TestGremlinExampleIndexQuery(unittest.TestCase):
 
         # Verify that get_texts_embeddings was called since query_embedding wasn't a list
         mock_embedding.get_texts_embeddings.assert_called_once_with(["find all persons"])
+
+
+class TestDefaultExampleBatching(unittest.TestCase):
+    def test_litellm_batch_limit_and_order(self):
+        from hugegraph_llm.models.embeddings.litellm import LiteLLMEmbedding
+
+        rows = [{"query": str(i), "gremlin": "g.V()"} for i in range(23)]
+        batches = []
+
+        def respond(**kwargs):
+            texts = kwargs["input"]
+            self.assertLessEqual(len(texts), 10)
+            batches.append(texts)
+            return SimpleNamespace(data=[{"embedding": [float(text)]} for text in texts], usage=None)
+
+        operator = object.__new__(GremlinExampleIndexQuery)
+        operator.embedding = LiteLLMEmbedding(embedding_dimension=1, api_key="test")
+        operator.vector_index = Mock()
+        with (
+            patch("pandas.read_csv", return_value=pd.DataFrame(rows)),
+            patch("hugegraph_llm.models.embeddings.litellm.embedding", side_effect=respond),
+        ):
+            operator._build_default_example_index()
+        self.assertEqual(batches, [[str(i) for i in range(start, min(start + 10, 23))] for start in (0, 10, 20)])
+        operator.vector_index.add.assert_called_once_with([[float(i)] for i in range(23)], rows)
+        operator.vector_index.save_index_by_name.assert_called_once_with("gremlin_examples")
+
+    def test_litellm_recovers_from_later_batch_rate_limit_before_saving_index(self):
+        from litellm import RateLimitError
+
+        from hugegraph_llm.models.embeddings.litellm import LiteLLMEmbedding
+
+        rows = [{"query": str(i), "gremlin": "g.V()"} for i in range(23)]
+        batches = []
+
+        def respond(**kwargs):
+            texts = kwargs["input"]
+            self.assertLessEqual(len(texts), 10)
+            batches.append(texts)
+            if len(batches) == 2:
+                raise RateLimitError(message="temporary 429", llm_provider="hosted_vllm", model="test")
+            return SimpleNamespace(data=[{"embedding": [float(text)]} for text in texts], usage=None)
+
+        operator = object.__new__(GremlinExampleIndexQuery)
+        operator.embedding = LiteLLMEmbedding(embedding_dimension=1, api_key="test")
+        operator.vector_index = Mock()
+        with (
+            patch("pandas.read_csv", return_value=pd.DataFrame(rows)),
+            patch("hugegraph_llm.models.embeddings.litellm.embedding", side_effect=respond),
+            patch("tenacity.nap.time.sleep"),
+        ):
+            operator._build_default_example_index()
+        expected_batches = [[str(i) for i in range(start, min(start + 10, 23))] for start in (0, 10, 10, 20)]
+        self.assertEqual(batches, expected_batches)
+        operator.vector_index.add.assert_called_once_with([[float(i)] for i in range(23)], rows)
+        operator.vector_index.save_index_by_name.assert_called_once_with("gremlin_examples")
+
+    def test_later_litellm_batch_exhausts_retries_without_saving_index(self):
+        from litellm import RateLimitError
+
+        from hugegraph_llm.models.embeddings.litellm import LiteLLMEmbedding
+
+        rows = [{"query": str(i), "gremlin": "g.V()"} for i in range(23)]
+        batches = []
+        error = RateLimitError(message="persistent 429", llm_provider="hosted_vllm", model="test")
+
+        def respond(**kwargs):
+            texts = kwargs["input"]
+            batches.append(texts)
+            if texts[0] == "10":
+                raise error
+            return SimpleNamespace(data=[{"embedding": [float(text)]} for text in texts], usage=None)
+
+        operator = object.__new__(GremlinExampleIndexQuery)
+        operator.embedding = LiteLLMEmbedding(embedding_dimension=1, api_key="test")
+        operator.vector_index = Mock()
+        with (
+            patch("pandas.read_csv", return_value=pd.DataFrame(rows)),
+            patch("hugegraph_llm.models.embeddings.litellm.embedding", side_effect=respond),
+            patch("tenacity.nap.time.sleep"),
+        ):
+            with self.assertRaises(RateLimitError) as raised:
+                operator._build_default_example_index()
+        self.assertIs(raised.exception, error)
+        self.assertEqual(batches, [[str(i) for i in range(10)]] + [[str(i) for i in range(10, 20)]] * 3)
+        operator.vector_index.add.assert_not_called()
+        operator.vector_index.save_index_by_name.assert_not_called()
+
+    def test_batch_limit_and_order(self):
+        from hugegraph_llm.models.embeddings.openai import OpenAIEmbedding
+
+        embedding = OpenAIEmbedding(api_key="test")
+        rows = [{"query": str(i), "gremlin": "g.V()"} for i in range(23)]
+        batches = []
+
+        def respond(**kwargs):
+            texts = kwargs["input"]
+            self.assertLessEqual(len(texts), 10)
+            batches.append(len(texts))
+            return SimpleNamespace(data=[SimpleNamespace(embedding=[float(text)]) for text in texts])
+
+        operator = object.__new__(GremlinExampleIndexQuery)
+        operator.embedding = embedding
+        operator.vector_index = Mock()
+        try:
+            with (
+                patch("pandas.read_csv", return_value=pd.DataFrame(rows)),
+                patch.object(embedding.client.embeddings, "create", side_effect=respond),
+            ):
+                operator._build_default_example_index()
+            self.assertEqual(batches, [10, 10, 3])
+            operator.vector_index.add.assert_called_once_with([[float(i)] for i in range(23)], rows)
+            operator.vector_index.save_index_by_name.assert_called_once_with("gremlin_examples")
+        finally:
+            embedding.client.close()
+
+    def test_embedding_failure_does_not_save_index(self):
+        operator = object.__new__(GremlinExampleIndexQuery)
+        operator.embedding = Mock()
+        operator.embedding.get_texts_embeddings.side_effect = RuntimeError("provider unavailable")
+        operator.vector_index = Mock()
+        with patch("pandas.read_csv", return_value=pd.DataFrame([{"query": "test"}])):
+            with self.assertRaisesRegex(RuntimeError, "provider unavailable"):
+                operator._build_default_example_index()
+        operator.vector_index.add.assert_not_called()
+        operator.vector_index.save_index_by_name.assert_not_called()
+
+
+@pytest.mark.parametrize("failed_batch", [0, 1, 2], ids=["first", "middle", "last"])
+@pytest.mark.parametrize("response_kind", ["short", "empty", "extra"])
+def test_invalid_batch_count_does_not_mutate_or_save_index(tmp_path, failed_batch, response_kind):
+    rows = [{"query": str(i), "gremlin": "g.V()"} for i in range(23)]
+    batches = []
+
+    def respond(**kwargs):
+        texts = kwargs["input"]
+        batches.append(texts)
+        data = [{"embedding": [float(text)]} for text in texts]
+        if len(batches) == failed_batch + 1:
+            if response_kind == "short":
+                data = data[:-1]
+            elif response_kind == "empty":
+                data = []
+            else:
+                data.append({"embedding": [999.0]})
+        return SimpleNamespace(data=data, usage=None)
+
+    operator = object.__new__(GremlinExampleIndexQuery)
+    operator.embedding = LiteLLMEmbedding(embedding_dimension=1, api_key="test")
+    operator.vector_index = FaissVectorIndex(1)
+    with (
+        patch("pandas.read_csv", return_value=pd.DataFrame(rows)),
+        patch("hugegraph_llm.models.embeddings.litellm.embedding", side_effect=respond),
+        patch("hugegraph_llm.indices.vector_index.faiss_vector_store.resource_path", str(tmp_path)),
+        patch.object(operator.vector_index, "add", wraps=operator.vector_index.add) as add,
+        patch.object(
+            operator.vector_index, "save_index_by_name", wraps=operator.vector_index.save_index_by_name
+        ) as save,
+    ):
+        with pytest.raises(ValueError, match="Embedding batch returned"):
+            operator._build_default_example_index()
+
+    expected_batches = [[str(i) for i in range(start, min(start + 10, 23))] for start in (0, 10, 20)]
+    assert batches == expected_batches[: failed_batch + 1]
+    add.assert_not_called()
+    save.assert_not_called()
+    assert operator.vector_index.index.ntotal == 0
+    assert operator.vector_index.properties == []
+    assert not (tmp_path / "gremlin_examples").exists()
diff --git a/hugegraph-mcp/README.md b/hugegraph-mcp/README.md
index 475ebc4c..5b88a09e 100644
--- a/hugegraph-mcp/README.md
+++ b/hugegraph-mcp/README.md
@@ -9,8 +9,10 @@ HugeGraph MCP is a safe, controlled Model Context Protocol adapter for HugeGraph
 ## Quick Start
 
 Installing a published package does not require cloning this repository. The
-commands below target `1.7.1` on PyPI; until it is published, use the
-[developer setup](#developer-notes). See the [release workflow](docs/releasing.md).
+commands below target `1.7.1` on PyPI. The new `core`/`legacy` names described
+below apply to this checkout and its next release; use the
+[developer setup](#developer-notes) to try them now. Published `1.7.1` uses
+`v2_core`/`v1` instead. See the [release workflow](docs/releasing.md).
 
 Install [uv](https://docs.astral.sh/uv/getting-started/installation/), then start the server in read-only mode:
 
@@ -24,7 +26,7 @@ export HUGEGRAPH_MCP_READONLY=true
 uvx hugegraph-mcp@1.7.1
 ```
 
-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.
+The process speaks MCP JSON-RPC on stdout. Keep `HUGEGRAPH_MCP_READONLY=true` unless controlled writes are required. Leave `HUGEGRAPH_MCP_TOOLSET` unset for the default toolset. To select the 10-tool compatibility toolset, use `legacy` with this checkout or `v1` with published `1.7.1`.
 
 For MCP clients that accept JSON server configuration:
 
@@ -85,32 +87,32 @@ After connecting, call `inspect_graph_tool(include_raw_schema=true)` and check t
 
 [Architecture and roadmap](docs/architecture.md) · [Integration checklist](docs/integration.md) · [Release order](docs/releasing.md)
 
-Structured queries cap the requested page size at `HUGEGRAPH_MCP_MAX_RESULT_ITEMS` and the tool maximum of 500. ID batches exceeding the effective limit are rejected before querying. Responses exceeding the item or byte limit are rejected without truncation or a continuation cursor; these checks do not bound backend execution or transport memory.
+Structured query `limit` defaults to `100` and must be an integer in `1–500`; values above `500` return `VALIDATION_ERROR`. Valid values are then reduced to `HUGEGRAPH_MCP_MAX_RESULT_ITEMS` when that configured ceiling is smaller (for example, `limit=100` with a ceiling of `50` uses `50`). ID batches exceeding the effective limit are rejected before querying. Responses exceeding the item or byte limit are rejected without truncation or a continuation cursor; these checks do not bound bac [...]
 
 ## Public Tool Surface
 
-The default `v2_core` contract registers 16 tools. The `v1` compatibility contract registers 10 tools and omits the six v2 additions.
+The default `core` toolset registers 16 tools. The `legacy` compatibility toolset registers 10 tools and omits the six core-only tools. Registration does not imply that every operation is enabled.
 
-| Tool | Contract | Description |
+| Tool | Toolset | 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 |
-
-Invalid `HUGEGRAPH_MCP_TOOLSET` values fail closed to `v1`. Tool registration is fixed at process startup, so restart the server after changing the toolset.
+| `inspect_graph_tool` | legacy, core | Inspect connection, schema summary, read-only state, and tool contract |
+| `generate_gremlin_tool` | legacy, core | Generate Gremlin; execution is disabled until the hard-budget contract is verified |
+| `execute_gremlin_read_tool` | legacy, core | Registered for compatibility; public raw execution currently returns `FEATURE_DISABLED` |
+| `extract_graph_data_tool` | legacy, core | Extract candidate graph data without writing |
+| `design_schema_tool` | legacy, core | Produce schema design guidance |
+| `apply_schema_tool` | legacy, core | Validate or preview one schema create; core supports confirmed apply |
+| `import_graph_data_tool` | legacy, core | Validate and preview structured vertex/edge creates; confirmation currently returns `FEATURE_DISABLED` |
+| `delete_graph_data_tool` | legacy, core | Preview exact deletion; confirmed edge deletion is supported |
+| `refresh_vid_embeddings_tool` | legacy, core | Admin write tool, disabled unless admin mode and writes are enabled |
+| `execute_gremlin_write_tool` | legacy, core | Registered for compatibility; public raw execution currently returns `FEATURE_DISABLED` |
+| `inspect_schema_tool` | core | Inspect and filter schema objects and relations |
+| `query_graph_data_tool` | core | Perform typed, bounded vertex and edge reads |
+| `mutate_graph_properties_tool` | core | Preview property changes; confirmation is disabled without atomic CAS |
+| `confirm_write_tool` | core | Confirm one persisted plan by `plan_id` |
+| `get_write_status_tool` | core | Read the durable plan and operation outcome by `plan_id` |
+| `reconcile_write_tool` | core | Reconcile `UNKNOWN` or `PARTIAL` outcomes by `plan_id` using read-only checks |
+
+Empty or unknown `HUGEGRAPH_MCP_TOOLSET` values stop startup with an error; an unset variable selects `core`. Tool registration is fixed at process startup, so restart the server after changing the toolset.
 
 ## Unified Response Envelope
 
@@ -135,6 +137,33 @@ High-level tools return:
 
 Failures set `ok=false`; `error.type` is a stable machine-readable code and `error.retryable` must be observed.
 
+## Toolset Naming and Migration
+
+`core` and `legacy` select groups of tools within one package. They are not
+package releases, HugeGraph Server versions, or MCP protocol versions. Both
+toolsets report `mcp_tool_contract_version="2.0"`; selecting a smaller toolset
+does not change that contract version.
+
+For deployments upgrading from `1.7.1`:
+
+- Replace `HUGEGRAPH_MCP_TOOLSET=v2_core` with `core`, and `v1` with `legacy`.
+  The old values remain accepted as deprecated input aliases and log a warning
+  on startup. They do not select separate implementations.
+- `inspect_graph_tool` now reports only `core` or `legacy` in both `data.toolset`
+  and `meta.toolset`, even when an old alias was supplied. Update clients that
+  compare these fields; there is no second, parallel toolset field.
+- Unset the variable for the default. Empty or unknown values (including `v2`)
+  now prevent startup instead of falling back to the compatibility toolset.
+- Restart after configuration changes. Tool registration, routing, and reported
+  toolset all use the same startup selection.
+
+Backward-compatible additions belong to `core`; adding a tool does not create a
+new numbered toolset. `legacy` retains its compatibility surface with necessary
+fixes. Breaking tool changes and eventual alias removal require an explicit
+migration notice and the project's release compatibility process; this change
+does not schedule alias removal. Neither toolset bypasses readonly, AI, admin,
+or backend capability checks.
+
 ## Write Safety Contract
 
 Canonical writes use only a server-issued `plan_id`:
@@ -170,7 +199,7 @@ Schema and graph-data dry-runs reject more than 200 operations or payloads large
 
 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.
 
-Structured reads and `generate_gremlin_tool(execute=false)` remain available.
+Structured reads remain available. `generate_gremlin_tool(execute=false)` additionally requires `HUGEGRAPH_MCP_ALLOW_AI=true` and a configured AI service.
 
 ## Configuration
 
@@ -181,13 +210,13 @@ Structured reads and `generate_gremlin_tool(execute=false)` remain available.
 | `HUGEGRAPH_GRAPHSPACE`, `HUGEGRAPH_GRAPH` | unset | Explicit values that override `HUGEGRAPH_GRAPH_PATH` |
 | `HUGEGRAPH_USER` | `admin` | HugeGraph username |
 | `HUGEGRAPH_PASSWORD` | empty | HugeGraph password |
-| `HUGEGRAPH_MCP_TOOLSET` | `v2_core` | `v1` or `v2_core`; invalid values fall back to `v1` |
+| `HUGEGRAPH_MCP_TOOLSET` | `core` | `core` or `legacy`; empty/unknown values prevent startup |
 | `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 | Optional HugeGraph-AI bearer token |
-| `HUGEGRAPH_AI_GRAPH_URL` | unset | Graph URL presented to HugeGraph-AI; falls back to `HUGEGRAPH_URL` |
+| `HUGEGRAPH_AI_GRAPH_URL` | unset | Deprecated, ignored; configure `GRAPH_URL` on the AI service instead |
 | `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` |
@@ -203,6 +232,42 @@ Boolean values accept `1/true/yes/on` and `0/false/no/off`, case-insensitively.
 
 Recommended defaults are `HUGEGRAPH_MCP_READONLY=true`, `HUGEGRAPH_MCP_ALLOW_AI=false`, and `HUGEGRAPH_MCP_ADMIN_MODE=false`.
 
+### AI Service Configuration
+
+Set `HUGEGRAPH_MCP_ALLOW_AI=true`, the reachable `HUGEGRAPH_AI_URL`, and
+`HUGEGRAPH_AI_TOKEN` when the AI service requires bearer authentication. Configure
+`GRAPH_URL`, `GRAPH_NAME`, and `GRAPH_SPACE` on the AI service itself. Its address
+must be reachable from that service (for example, `http://server:8080` inside
+Docker), and its graph/graphspace must match the MCP target. Changing MCP's
+`HUGEGRAPH_URL` does not reconfigure the AI service. `HUGEGRAPH_AI_GRAPH_URL` is
+ignored and is not a way to override the AI target.
+For the default MCP target `DEFAULT/hugegraph`, set AI `GRAPH_NAME=hugegraph`.
+An unset, null, empty, or whitespace-only AI graphspace is equivalent to
+`GRAPH_SPACE=DEFAULT` for target validation. This comparison does not change
+stored configuration or the existing vector-index path convention.
+
+Refresh requires MCP writes enabled, admin mode, AI enabled, and `confirm=true`.
+For the current AI source, also set `ENABLE_LOGIN=true` and
+`HUGEGRAPH_LLM_ENABLE_THIN_WRITES=true` on the AI service, configure a strong
+`USER_TOKEN`, and supply it through `HUGEGRAPH_AI_TOKEN`. These AI-side write gates
+apply to this implementation; older deployments may lack them. Verify the actual
+service version rather than assuming the published MCP `1.7.1` requires the same gates.
+
+The current MCP refresh path checks `GET /vid-embeddings/refresh-capabilities` for
+`protocol_version=1`, then sends both expected target fields to
+`POST /v1/vid-embeddings/refresh`. This versioned POST requires the fields and
+validates them under the same configuration lock as the refresh. Discovery alone
+cannot guarantee that the POST reaches the same replica during a rolling upgrade;
+the versioned POST is the validation boundary. If an older replica lacks that
+route, MCP reports that an AI upgrade is required and never falls back to
+`POST /vid-embeddings/refresh`. The legacy route still accepts empty requests from
+authenticated callers with writes enabled, for compatibility.
+
+Target validation checks graph names and graphspace, not whether MCP and AI
+connect to the same physical server; operators must configure and verify both
+addresses. Refresh reads graph IDs/schema and updates the VID vector index; it
+does not write graph records.
+
 ## License
 
 Apache License 2.0
diff --git a/hugegraph-mcp/README.zh-CN.md b/hugegraph-mcp/README.zh-CN.md
index 2b592266..13b7817c 100644
--- a/hugegraph-mcp/README.zh-CN.md
+++ b/hugegraph-mcp/README.zh-CN.md
@@ -8,8 +8,10 @@ HugeGraph MCP 是 HugeGraph Server 的安全、可控 Model Context Protocol 适
 
 ## 快速开始
 
-安装已发布版本不需要克隆仓库。以下命令面向 PyPI 的 `1.7.1` 包;
-发布前请使用[开发者说明](#开发者说明),发布步骤见[发布流程](docs/releasing.md)。
+安装已发布版本不需要克隆仓库。以下命令面向 PyPI 的 `1.7.1` 包。
+下文新的 `core`/`legacy` 名称适用于当前源码和下一次发行;现在体验请使用
+[开发者说明](#开发者说明)。已发布的 `1.7.1` 仍使用 `v2_core`/`v1`。
+发布步骤见[发布流程](docs/releasing.md)。
 
 安装 [uv](https://docs.astral.sh/uv/getting-started/installation/),然后以只读模式启动:
 
@@ -23,7 +25,7 @@ export HUGEGRAPH_MCP_READONLY=true
 uvx hugegraph-mcp@1.7.1
 ```
 
-进程通过 stdout 提供 MCP JSON-RPC。除非明确需要受控写入,请保持 `HUGEGRAPH_MCP_READONLY=true`。默认工具集为 `v2_core`;只有旧客户端需要 10 工具契约时才在启动前设置 `HUGEGRAPH_MCP_TOOLSET=v1`。
+进程通过 stdout 提供 MCP JSON-RPC。除非明确需要受控写入,请保持 `HUGEGRAPH_MCP_READONLY=true`。不设置 `HUGEGRAPH_MCP_TOOLSET` 即使用默认工具集。需要 10 工具的兼容工具集时,当前源码使用 `legacy`,已发布的 `1.7.1` 使用 `v1`。
 
 支持 JSON Server 配置的 MCP 客户端可使用:
 
@@ -84,32 +86,32 @@ Windows PowerShell:
 
 [架构与规划](docs/architecture.md) · [完整集成检查](docs/integration.md) · [发布顺序](docs/releasing.md)
 
-结构化查询会将分页大小限制在 `HUGEGRAPH_MCP_MAX_RESULT_ITEMS` 和工具上限 500 以内。超过有效上限的 ID 批次会在查询前被拒绝。返回结果超过条数或字节上限时整体拒绝,不截断结果或返回后续游标;这些检查不限制服务端执行量或传输期间的内存占用。
+结构化查询的 `limit` 默认是 `100`,输入必须是 `1–500` 的整数;超过 `500` 返回 `VALIDATION_ERROR`。合法值再按 `HUGEGRAPH_MCP_MAX_RESULT_ITEMS` 收紧,例如配置上限为 `50` 时,请求 `100` 实际使用 `50`。超过有效上限的 ID 批次会在查询前被拒绝。返回结果超过条数或字节上限时整体拒绝,不截断结果或返回后续游标;这些检查不限制服务端执行量或传输期间的内存占用。
 
 ## 对外工具面
 
-默认 `v2_core` 契约注册 16 个工具。`v1` 兼容契约注册 10 个工具,不包含六个 v2 新增工具。
+默认 `core` 工具集注册 16 个工具。`legacy` 兼容工具集注册 10 个工具,不包含六个 core 专属工具。工具已注册不代表其所有操作都已启用。
 
-| 工具 | 契约 | 说明 |
+| 工具 | 工具集 | 说明 |
 |------|------|------|
-| `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` 值会收口到 `v1`。工具注册在进程启动时确定,修改工具集后需要重启服务。
+| `inspect_graph_tool` | legacy、core | 查看连接、schema 摘要、只读状态和工具契约 |
+| `generate_gremlin_tool` | legacy、core | 生成 Gremlin;在硬资源预算合同验证前禁止执行 |
+| `execute_gremlin_read_tool` | legacy、core | 为兼容保留注册;公开 Raw Gremlin 执行当前返回 `FEATURE_DISABLED` |
+| `extract_graph_data_tool` | legacy、core | 抽取候选图数据,不写图 |
+| `design_schema_tool` | legacy、core | 提供 schema 设计建议 |
+| `apply_schema_tool` | legacy、core | 校验或预览一个 schema create;core 支持确认执行 |
+| `import_graph_data_tool` | legacy、core | 校验并预览结构化点边创建;确认当前返回 `FEATURE_DISABLED` |
+| `delete_graph_data_tool` | legacy、core | 预览精确删除;支持确认删除边 |
+| `refresh_vid_embeddings_tool` | legacy、core | 管理写工具,仅在 admin mode 与写入均启用时开放 |
+| `execute_gremlin_write_tool` | legacy、core | 为兼容保留注册;公开 Raw Gremlin 执行当前返回 `FEATURE_DISABLED` |
+| `inspect_schema_tool` | core | 查看并筛选 schema 对象及关系 |
+| `query_graph_data_tool` | core | 执行类型化、有界的点边读取 |
+| `mutate_graph_properties_tool` | core | 预览属性变更;缺少原子 CAS 时禁止确认 |
+| `confirm_write_tool` | core | 仅通过 `plan_id` 确认一个持久化计划 |
+| `get_write_status_tool` | core | 仅通过 `plan_id` 查询计划及操作的持久化结果 |
+| `reconcile_write_tool` | core | 仅通过 `plan_id` 和只读检查协调 `UNKNOWN` 或 `PARTIAL` 结果 |
+
+空白或未知的 `HUGEGRAPH_MCP_TOOLSET` 值会导致启动报错;未设置时使用 `core`。工具注册在进程启动时确定,修改工具集后需要重启服务。
 
 ## 统一响应格式
 
@@ -134,6 +136,26 @@ Windows PowerShell:
 
 失败响应设置 `ok=false`。调用方必须遵守稳定的 `error.type` 和 `error.retryable`。
 
+## 工具集命名与迁移
+
+`core` 和 `legacy` 选择同一个包内的工具分组,不是包发行版本、HugeGraph Server
+版本或 MCP 协议版本。两种工具集都返回 `mcp_tool_contract_version="2.0"`,
+选择较小的工具集不会改变工具契约版本。
+
+从 `1.7.1` 升级时:
+
+- 将 `HUGEGRAPH_MCP_TOOLSET=v2_core` 改成 `core`,将 `v1` 改成 `legacy`。
+  旧值仍作为已弃用的输入别名接受,启动时记录迁移警告,不维护独立实现。
+- `inspect_graph_tool` 的 `data.toolset` 和 `meta.toolset` 只返回 `core` 或
+  `legacy`,即使输入的是旧别名。比较这些字段的客户端需要更新;不新增第二个同义字段。
+- 不设置变量即使用默认值。空白或未知值(包括 `v2`)现在会阻止启动,不再自动回落到兼容工具集。
+- 修改配置后需要重启。工具注册、行为分支和状态报告使用同一个启动时选择结果。
+
+兼容性新增功能加入 `core`,不会因为增加工具而创建新的编号工具集。`legacy` 保留
+兼容接口并接受必要修复。破坏性工具变更或未来移除别名,需要明确迁移说明并遵循项目
+发行兼容流程;本次不设定别名移除时间。两种工具集都不会绕过 readonly、AI、admin
+或后端能力检查。
+
 ## 写入安全合同
 
 规范写入流程仅使用服务端签发的 `plan_id`:
@@ -169,7 +191,7 @@ Schema 和图数据 dry-run 在操作数超过 200 或 payload 超过 1 MiB 时
 
 所有公开 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)` 仍可使用。
+结构化读取仍可使用。`generate_gremlin_tool(execute=false)` 还需要 `HUGEGRAPH_MCP_ALLOW_AI=true` 和已配置的 AI 服务。
 
 ## 配置
 
@@ -180,13 +202,13 @@ Schema 和图数据 dry-run 在操作数超过 200 或 payload 超过 1 MiB 时
 | `HUGEGRAPH_GRAPHSPACE`、`HUGEGRAPH_GRAPH` | 未设置 | 显式值会覆盖 `HUGEGRAPH_GRAPH_PATH` |
 | `HUGEGRAPH_USER` | `admin` | HugeGraph 用户名 |
 | `HUGEGRAPH_PASSWORD` | 空 | HugeGraph 密码 |
-| `HUGEGRAPH_MCP_TOOLSET` | `v2_core` | `v1` 或 `v2_core`;非法值回落到 `v1` |
+| `HUGEGRAPH_MCP_TOOLSET` | `core` | `core` 或 `legacy`;空白或未知值导致启动失败 |
 | `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 URL |
 | `HUGEGRAPH_AI_TOKEN` | 未设置 | 可选的 HugeGraph-AI bearer token |
-| `HUGEGRAPH_AI_GRAPH_URL` | 未设置 | 提供给 HugeGraph-AI 的图 URL;默认使用 `HUGEGRAPH_URL` |
+| `HUGEGRAPH_AI_GRAPH_URL` | 未设置 | 已弃用且不生效;请在 AI 服务侧配置 `GRAPH_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` |
@@ -202,6 +224,30 @@ Schema 和图数据 dry-run 在操作数超过 200 或 payload 超过 1 MiB 时
 
 推荐默认配置为 `HUGEGRAPH_MCP_READONLY=true`、`HUGEGRAPH_MCP_ALLOW_AI=false`、`HUGEGRAPH_MCP_ADMIN_MODE=false`。
 
+### AI 服务配置
+
+设置 `HUGEGRAPH_MCP_ALLOW_AI=true`、可访问的 `HUGEGRAPH_AI_URL`,并在 AI 服务启用 bearer 认证时设置
+`HUGEGRAPH_AI_TOKEN`。在 AI 服务自身配置 `GRAPH_URL`、`GRAPH_NAME` 和 `GRAPH_SPACE`:地址必须能从
+AI 服务访问,例如 Docker 内的 `http://server:8080`;图名与 graphspace 必须与 MCP 目标一致。
+修改 MCP 的 `HUGEGRAPH_URL` 不会重新配置 AI 服务;`HUGEGRAPH_AI_GRAPH_URL` 不生效,不能用于覆盖 AI 目标。
+使用 MCP 默认目标 `DEFAULT/hugegraph` 时,请设置 AI 的 `GRAPH_NAME=hugegraph`。
+目标校验将 AI 侧未设置、null、空字符串或全空白的 graphspace 视为 `GRAPH_SPACE=DEFAULT`;
+该比较不修改保存的配置,也不改变现有向量索引路径约定。
+
+刷新需要 MCP 允许写入、启用管理模式、启用 AI,并在调用时设置 `confirm=true`。当前 AI 源码还要求服务侧
+`ENABLE_LOGIN=true`、`HUGEGRAPH_LLM_ENABLE_THIN_WRITES=true`,配置强 `USER_TOKEN`,并通过 MCP 的
+`HUGEGRAPH_AI_TOKEN` 传入。上述 AI 写入门禁适用于当前实现;旧部署可能没有这些门禁,必须核对实际服务版本,
+不能认为已发布的 MCP `1.7.1` 与所有 AI 部署都具有相同条件。
+
+当前 MCP 刷新路径先检查 `GET /vid-embeddings/refresh-capabilities` 返回的 `protocol_version=1`,
+再向 `POST /v1/vid-embeddings/refresh` 传入两个必填目标字段,在同一配置锁内完成目标校验与刷新。
+滚动升级时,能力探测与 POST 可能落到不同副本,因此安全边界是版本化 POST 路由。
+旧副本缺少该路由时,MCP 会提示升级 AI,绝不回退到 `POST /vid-embeddings/refresh`。
+旧路由为兼容已有调用方,仍允许已认证且开启写入的调用方传入空请求。
+
+此校验只确认图名和 graphspace,不证明 MCP 与 AI 连接的是同一台物理服务器;部署者仍需配置并核对两侧地址。
+刷新读取图 ID/schema 并更新 VID 向量索引,不写入图记录。
+
 ## License
 
 Apache License 2.0
diff --git a/hugegraph-mcp/docs/architecture.md b/hugegraph-mcp/docs/architecture.md
index 1833b288..3aa5da48 100644
--- a/hugegraph-mcp/docs/architecture.md
+++ b/hugegraph-mcp/docs/architecture.md
@@ -44,7 +44,8 @@ check; a process-local lock cannot supply that guarantee.
 
 | Operation | Current behavior | Requirement for broader support |
 |---|---|---|
-| Structured graph/schema reads | Available with bounded inputs and output guards | Backend execution budgets remain separate |
+| Structured graph reads | Available with bounded inputs and item/byte output guards | Backend execution budgets remain separate |
+| Schema inspection | Available; schema responses have no item/byte output guard | Add explicit limits if bounded schema output is required |
 | Schema creation | One property key, vertex label, or edge label per confirmed plan | Additional schema operations need explicit adapters and outcome verification |
 | Exact edge deletion | Confirmable using a persisted edge ID | Other backend profiles require verification |
 | Vertex/edge creation and graph import | Preview-only | Atomic create-if-absent and stable edge identity |
@@ -52,18 +53,27 @@ check; a process-local lock cannot supply that guarantee.
 | Isolated vertex deletion | Preview-only | Atomic enforcement of the no-incident-edge precondition |
 | Raw Gremlin execution | Disabled, including admin mode | Read-only principal, server evaluation timeout and result cap, and streaming response-byte limit |
 
-Capability evidence is specific to server version and storage backend. Unknown
-profiles stay disabled. HugeGraph 1.7.0/RocksDB does not provide verified atomic
+The current write capability profile and its evidence cover HugeGraph
+1.7.0/RocksDB. This supported deployment does not provide verified atomic
 creation or property CAS through the current client. Its isolated-delete
 concurrency probe can remove a concurrently added edge, so a successful ordered
 test is insufficient to enable that operation. See
 [`backend_capabilities.py`](../hugegraph_mcp/backend_capabilities.py) and
 [`test_real_write_path.py`](../tests/integration/test_real_write_path.py).
 
-Output item/byte checks run after response materialization. They bound MCP
+Output item/byte checks run after response materialization. For tools that implement these guards, they bound MCP
 output, not server work, network transfer, or parsing memory. Connection, read,
 write, and AI timeouts are configured separately.
 
+VID refresh uses `POST /v1/vid-embeddings/refresh`, whose required target fields
+are checked under the runtime configuration lock before executing the flow.
+The capability GET retains `protocol_version=1` for compatibility, but the POST
+route supplies the validation guarantee even when requests reach different
+replica versions. MCP never falls back to the legacy refresh route. Empty/default
+GraphSpace representations are equivalent only for comparison; configuration and
+index paths are unchanged. Operators must still verify physical-instance identity
+when the MCP graph URL and the AI graph URL differ.
+
 ## Durable Outcomes
 
 | Status | Meaning |
@@ -84,10 +94,11 @@ no public resume tool or automatic replay of `PARTIAL`/`UNKNOWN` writes.
 
 ## Next Work
 
-1. **Complete distribution validation.** Publish client and MCP `1.7.1`, verify
-   installation from the public index, and exercise a real HugeGraph service.
-   For the planned release, align package versions to `1.8.0` and raise the MCP
-   client dependency accordingly. Follow the [publishing guide](releasing.md).
+1. **Prepare the next aligned release.** The published pair is `1.7.1`; this
+   checkout currently has client `1.8.0` and MCP `1.7.1`, with MCP requiring
+   `hugegraph-python>=1.7.1,<2`. At release preparation, align both packages to
+   `1.8.0` and raise the client minimum accordingly. Validate distribution, public
+   index installation, and a real service following the [publishing guide](releasing.md).
 2. **Enable additional writes only with backend support.** Add the atomic
    primitives listed above and prove them with two-client concurrency tests for
    each supported backend. Imports must preserve operation dependencies,
diff --git a/hugegraph-mcp/docs/integration.md b/hugegraph-mcp/docs/integration.md
index 0fc6593d..c60a39ca 100644
--- a/hugegraph-mcp/docs/integration.md
+++ b/hugegraph-mcp/docs/integration.md
@@ -18,7 +18,7 @@ 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_TOOLSET=core
 export HUGEGRAPH_MCP_READONLY=false
 export HUGEGRAPH_MCP_ALLOW_AI=false
 export HUGEGRAPH_MCP_ADMIN_MODE=false
@@ -29,14 +29,27 @@ export HUGEGRAPH_CONNECT_TIMEOUT_SECONDS=0.5
 export HUGEGRAPH_READ_TIMEOUT_SECONDS=15
 export HUGEGRAPH_WRITE_TIMEOUT_SECONDS=15
 
-.venv-mcp/bin/python -m hugegraph_mcp.server
 ```
 
 The environment installs both packages from this checkout. Keep commands relative
 to the repository root. For Windows setup, see the [README](../README.md#developer-notes);
 the shell and Docker examples in this checklist use POSIX syntax.
 
-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.
+The server uses STDIO and does not listen on a network address. The MCP client
+must launch the server as its child and hold its stdin/stdout pipes. Run the
+provided client from the repository root using the environment above:
+
+```bash
+.venv-mcp/bin/python hugegraph-mcp/examples/stdio_check.py --interactive
+```
+
+The client launches `sys.executable -m hugegraph_mcp.server` with inherited
+environment, initializes the session, checks real graph availability, and lists
+tools. It then accepts one `tools/call`
+JSON object per input line. Paste each JSON block below as one line. Keep the
+client running while reviewing returned IDs; use another terminal for fixture
+seeding. 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.
 
 Clean up after validation:
 
@@ -54,7 +67,7 @@ rm -rf -- /tmp/hg-mcp-plan-store
 }
 ```
 
-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`.
+Require `ok=true`, `data.readonly=false`, `data.toolset="core"`, and `data.mcp_tool_contract_version="2.0"`. Confirm that the MCP client lists all 16 core tools, including `confirm_write_tool`, `get_write_status_tool`, and `reconcile_write_tool`.
 
 ## 3. Create Schema One Object Per Plan
 
@@ -371,11 +384,43 @@ Each call below must return `FEATURE_DISABLED`, even if admin mode is enabled:
 {"name":"generate_gremlin_tool","arguments":{"query":"list one vertex","execute":true}}
 ```
 
-`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.
+With the initial `HUGEGRAPH_MCP_ALLOW_AI=false` configuration,
+`generate_gremlin_tool(execute=false)` must also return `FEATURE_DISABLED`, while
+structured query tools remain available. The item and byte configuration values
+are post-materialization output guards and do not satisfy the raw-query hard-budget contract.
+
+### Separate AI-enabled phase
+
+Configure and start an AI service as described in the [README](../README.md#ai-service-configuration).
+Restart the STDIO client with `HUGEGRAPH_MCP_ALLOW_AI=true`, a reachable
+`HUGEGRAPH_AI_URL`, and the required token. Then require
+`generate_gremlin_tool(execute=false)` to generate Gremlin without executing it.
+`execute=true` must still return `FEATURE_DISABLED`. Record the AI service version
+and settings; success in this phase is separate from the AI-disabled phase.
+
+### Verify admin mode independently
+
+In separate processes with `HUGEGRAPH_MCP_READONLY=false` and one SQLite write
+instance, call `refresh_vid_embeddings_tool(confirm=false)`. With admin mode off,
+require `FEATURE_DISABLED` and `error.details.required_env.HUGEGRAPH_MCP_ADMIN_MODE`
+set to `true`. Restart with admin mode on and require `CONFIRM_REQUIRED`. Keep
+`confirm=false` throughout so no refresh is dispatched. Raw Gremlin rejection
+only verifies its disabled boundary; run classifier unit tests separately.
+
+### Verify the refresh target contract
+
+Run the API and MCP regression tests for default GraphSpace equivalence, required
+target fields, authentication/write gates, and configuration locking. The MCP
+mixed-version test routes discovery to a capable replica and the POST to an old
+replica: only `/v1/vid-embeddings/refresh` may be requested, and an unavailable
+route must return an upgrade error without dispatching the legacy refresh.
+These controlled tests do not replace a rolling-deployment integration run.
+For an authorized refresh against disposable fixtures, verify the configured VID
+index and both physical graph addresses; graph records must remain unchanged.
 
 ## Pass Criteria
 
-- Tool contract and count match `v2_core`.
+- Tool contract and count match `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.
diff --git a/hugegraph-mcp/docs/releasing.md b/hugegraph-mcp/docs/releasing.md
index 4a65a616..b4282e9a 100644
--- a/hugegraph-mcp/docs/releasing.md
+++ b/hugegraph-mcp/docs/releasing.md
@@ -8,7 +8,8 @@ second tag-triggered publisher in this repository.
 
 ## Package Versions
 
-The intermediate client and MCP packages use `1.7.1`. MCP requires
+The published client and MCP packages use `1.7.1`. This checkout is an intermediate
+development state: client `1.8.0`, MCP `1.7.1`. MCP currently requires
 `hugegraph-python>=1.7.1,<2`; the published `1.7.0` client does not include all
 client fixes required by MCP. Publish the client before the MCP package.
 
@@ -35,7 +36,10 @@ validation can therefore run before the PR is merged into ASF `main`.
 The workflow defaults to `publish=false`: it builds, tests, and records artifacts
 without uploading a package. In `both` mode, MCP uses the verified client artifact
 from the same run; this validates the package pair, not registry installation.
-Both packages must have matching source versions and use the same resolved commit.
+For the next joint release, align both source versions to `1.8.0` before using
+`both`, and use the same resolved commit. The current mixed-version checkout is
+not yet that release pair; these instructions do not establish that a remote
+workflow has been run successfully.
 With `publish=true`, `target` selects the `testpypi`
 or `pypi` GitHub environment and its `PYPI_API_TOKEN`. The PyPI token must have
 permission for the selected project; a token scoped only to `hugegraph-python`
@@ -58,10 +62,11 @@ does not establish compatibility with a client published only on public PyPI.
    version from the selected index before publishing MCP. If MCP fails after the
    client is published, fix the cause and use `component=mcp` to retry; do not
    re-upload an existing client version.
-4. Verify installation from outside the checkout:
+4. After `1.8.0` has been published, verify installation from outside the checkout
+   (the following command targets that future release, not the current published `1.7.1`):
 
    ```bash
-   uvx --no-cache hugegraph-mcp@1.7.1
+   uvx --no-cache hugegraph-mcp@1.8.0
    ```
 
 5. Configure the HugeGraph URL and credentials, connect an MCP client, and verify
@@ -71,18 +76,18 @@ does not establish compatibility with a client published only on public PyPI.
 The shared workflow tests wheel/sdist installation and uses
 `tests/distribution_smoke.py` to exercise MCP stdio with a local HTTP fixture.
 The script starts the installed command in a temporary directory, removes source
-import overrides, and verifies initialization, 16 v2 tools, inspection, and a
+import overrides, and verifies initialization, 16 core tools, inspection, and a
 structured query. This checks packaging and protocol wiring; it does not replace
 real-server integration tests.
 
 To repeat the distribution smoke locally, build a wheel and pass its absolute
-path (the client must already be on PyPI):
+path after aligning/building `1.8.0` (client `1.8.0` must already be on PyPI):
 
 ```bash
 uv build --no-sources hugegraph-mcp --out-dir /tmp/mcp-dist
 python hugegraph-mcp/tests/distribution_smoke.py -- \
   uvx --no-config --no-cache --index-url https://pypi.org/simple \
-  --from /tmp/mcp-dist/hugegraph_mcp-1.7.1-py3-none-any.whl hugegraph-mcp
+  --from /tmp/mcp-dist/hugegraph_mcp-1.8.0-py3-none-any.whl hugegraph-mcp
 ```
 
 PR CI separately tests wheels for both packages from the same checkout. That
diff --git a/hugegraph-mcp/examples/stdio_check.py b/hugegraph-mcp/examples/stdio_check.py
new file mode 100644
index 00000000..12f90eb0
--- /dev/null
+++ b/hugegraph-mcp/examples/stdio_check.py
@@ -0,0 +1,89 @@
+# 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.
+
+"""Launch one STDIO session, inspect it, then execute optional JSON tool requests.
+
+Usage: python hugegraph-mcp/examples/stdio_check.py [requests.json]
+requests.json is a list of {"name": ..., "arguments": ...} tools/call payloads.
+The same session stays open for all calls. Server inherits the caller environment.
+"""
+
+import argparse
+import asyncio
+import json
+import os
+import sys
+from pathlib import Path
+
+from mcp import ClientSession, StdioServerParameters
+from mcp.client.stdio import stdio_client
+
+
+async def run(requests_path: Path | None, interactive: bool):
+    requests = json.loads(requests_path.read_text(encoding="utf-8")) if requests_path else []
+    if not isinstance(requests, list):
+        raise ValueError("Requests must be a JSON list")
+    params = StdioServerParameters(command=sys.executable, args=["-m", "hugegraph_mcp.server"], env=dict(os.environ))
+    async with stdio_client(params) as (read, write), ClientSession(read, write) as session:
+        await session.initialize()
+        listed = await session.list_tools()
+        sys.stdout.write(json.dumps({"tools": [tool.name for tool in listed.tools]}) + "\n")
+        pending = [{"name": "inspect_graph_tool", "arguments": {"include_counts": False}}, *requests]
+        while True:
+            if pending:
+                request = pending.pop(0)
+            elif interactive:
+                try:
+                    line = await asyncio.to_thread(input, "tools/call JSON (EOF to finish): ")
+                except EOFError:
+                    break
+                if not line.strip():
+                    continue
+                try:
+                    request = json.loads(line)
+                    if not isinstance(request, dict):
+                        raise ValueError("Request must be a JSON object")
+                    if not isinstance(request.get("name"), str) or not request["name"].strip():
+                        raise ValueError("Tool name must be a non-empty string")
+                    if not isinstance(request.get("arguments", {}), dict):
+                        raise ValueError("Tool arguments must be a JSON object")
+                except ValueError as exc:
+                    sys.stderr.write(f"Invalid tools/call request: {exc}\n")
+                    continue
+            else:
+                break
+            result = await session.call_tool(request["name"], request.get("arguments", {}))
+            sys.stdout.write(result.model_dump_json() + "\n")
+            if result.isError:
+                raise RuntimeError(f"MCP tool failed: {request['name']}")
+            data = result.structuredContent
+            if data is None:
+                data = json.loads(result.content[0].text)
+            if request["name"] == "inspect_graph_tool" and (
+                not data.get("ok") or data["data"].get("hugegraph_server_status") != "available"
+            ):
+                raise RuntimeError("Graph inspection could not verify connection; inspect warnings")
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument("requests", nargs="?", type=Path)
+    parser.add_argument("--interactive", action="store_true", help="Read one tools/call JSON object per line")
+    args = parser.parse_args()
+    asyncio.run(run(args.requests, args.interactive))
+
+
+if __name__ == "__main__":
+    main()
diff --git a/hugegraph-mcp/hugegraph_mcp/config.py b/hugegraph-mcp/hugegraph_mcp/config.py
index bc0defc4..a7869383 100644
--- a/hugegraph-mcp/hugegraph_mcp/config.py
+++ b/hugegraph-mcp/hugegraph_mcp/config.py
@@ -119,6 +119,10 @@ class MCPConfig:
             cache_key = None
 
         warnings: list[str] = []
+        if "HUGEGRAPH_AI_GRAPH_URL" in env and env.get("HUGEGRAPH_AI_GRAPH_URL") is not None:
+            warnings.append(
+                "HUGEGRAPH_AI_GRAPH_URL is deprecated and has no effect; configure GRAPH_URL on the AI service."
+            )
 
         path_graphspace, path_graph = _parse_graph_path(env.get("HUGEGRAPH_GRAPH_PATH", "DEFAULT/hugegraph"))
         graphspace = path_graphspace
diff --git a/hugegraph-mcp/hugegraph_mcp/server.py b/hugegraph-mcp/hugegraph_mcp/server.py
index 0d494cfe..3424e592 100644
--- a/hugegraph-mcp/hugegraph_mcp/server.py
+++ b/hugegraph-mcp/hugegraph_mcp/server.py
@@ -17,7 +17,6 @@
 server.py 只负责参数校验和 mode 分发,具体业务逻辑委托给 tools/ 下的模块。
 """
 
-import logging
 import os
 import time
 from typing import Any
@@ -39,28 +38,19 @@ from hugegraph_mcp.tools.manage_schema import manage_schema
 from hugegraph_mcp.tools.mutate_graph_properties import mutate_graph_properties
 from hugegraph_mcp.tools.query_graph_data import query_graph_data
 from hugegraph_mcp.tools.refresh_vid_embeddings import refresh_vid_embeddings
+from hugegraph_mcp.toolsets import resolve_toolset
 from hugegraph_mcp.write_contract import resolve_optional_legacy_locator
 from hugegraph_mcp.write_executor import confirm_write, get_write_status
 
 READONLY = MCPConfig.from_env().is_readonly()
 MCP_TOOL_CONTRACT_VERSION = "2.0"
-DEFAULT_TOOLSET = "v2_core"
-VALID_TOOLSETS = frozenset({"v1", DEFAULT_TOOLSET})
-LOGGER = logging.getLogger("hugegraph_mcp.server")
+ACTIVE_TOOLSET = resolve_toolset(os.getenv("HUGEGRAPH_MCP_TOOLSET"))
 
 mcp = FastMCP("HugeGraph MCP")
 
 
-def _active_toolset() -> str:
-    value = os.getenv("HUGEGRAPH_MCP_TOOLSET", DEFAULT_TOOLSET).strip()
-    if value in VALID_TOOLSETS:
-        return value
-    LOGGER.error("Invalid HUGEGRAPH_MCP_TOOLSET value %r; falling back to v1", value)
-    return "v1"
-
-
-def _register_v2_core_tool(func):
-    if _active_toolset() == "v1":
+def _register_core_tool(func):
+    if ACTIVE_TOOLSET == "legacy":
         return func
     return mcp.tool()(func)
 
@@ -166,7 +156,7 @@ def _admin_gate(tool_name: str, *, requires_write: bool = False) -> dict | None:
             source=tool_name,
             details={
                 "tool": tool_name,
-                "toolset": _active_toolset(),
+                "toolset": ACTIVE_TOOLSET,
                 "enable_env": enable_env,
                 "required_env": required_env,
             },
@@ -252,19 +242,19 @@ def inspect_graph_tool(
         inspect_graph,
         include_raw_schema=include_raw_schema,
         include_counts=include_counts,
-        toolset=_active_toolset(),
+        toolset=ACTIVE_TOOLSET,
     )
     if result.get("ok") and isinstance(result.get("data"), dict):
         result["data"]["mcp_tool_contract_version"] = MCP_TOOL_CONTRACT_VERSION
-        result["data"]["toolset"] = _active_toolset()
+        result["data"]["toolset"] = ACTIVE_TOOLSET
     meta = dict(result.get("meta") or {})
     meta["mcp_tool_contract_version"] = MCP_TOOL_CONTRACT_VERSION
-    meta["toolset"] = _active_toolset()
+    meta["toolset"] = ACTIVE_TOOLSET
     result["meta"] = meta
     return result
 
 
-@_register_v2_core_tool
+@_register_core_tool
 def get_write_status_tool(plan_id: str) -> dict:
     """Return the durable plan, operation, and receipt outcome by plan_id."""
     return _call_public_tool(
@@ -274,7 +264,7 @@ def get_write_status_tool(plan_id: str) -> dict:
     )
 
 
-@_register_v2_core_tool
+@_register_core_tool
 def confirm_write_tool(plan_id: str) -> dict:
     """Confirm exactly one immutable server-persisted plan by plan_id only."""
     return _call_public_tool(
@@ -284,7 +274,7 @@ def confirm_write_tool(plan_id: str) -> dict:
     )
 
 
-@_register_v2_core_tool
+@_register_core_tool
 def reconcile_write_tool(plan_id: str) -> dict:
     """Reconcile an UNKNOWN or PARTIAL plan by plan_id using read-only checks."""
     return _call_public_tool(
@@ -294,7 +284,7 @@ def reconcile_write_tool(plan_id: str) -> dict:
     )
 
 
-@_register_v2_core_tool
+@_register_core_tool
 def inspect_schema_tool(
     include_raw_schema: bool = False,
     include_relations: bool = True,
@@ -321,7 +311,7 @@ def inspect_schema_tool(
     )
 
 
-@_register_v2_core_tool
+@_register_core_tool
 def query_graph_data_tool(
     target: str,
     operation: str,
@@ -363,7 +353,7 @@ def query_graph_data_tool(
     )
 
 
-# ========== V1 stable tools ==========
+# ========== Tools shared by core and legacy ==========
 
 
 @mcp.tool()
@@ -424,7 +414,7 @@ def extract_graph_data_tool(
     graph_schema: dict | None = None,
     example_prompt: str | None = None,
 ) -> dict:
-    """V1 稳定工具:自然语言文本 → 候选 graph_data(不写入)。
+    """共享工具:自然语言文本 → 候选 graph_data(不写入)。
 
     返回提取的顶点和边数据,供后续导入使用。
     graph_schema 可传入 HugeGraph schema;为空时从当前 HugeGraph 读取 live schema,
@@ -441,7 +431,7 @@ def extract_graph_data_tool(
 
 @mcp.tool()
 def design_schema_tool(operations: list[dict] | None = None) -> dict:
-    """V1 稳定工具:schema 设计指导。
+    """共享工具:schema 设计指导。
 
     提供 schema 设计建议和最佳实践。
     """
@@ -475,10 +465,10 @@ def apply_schema_tool(
       Rejects create_index_label, schema append/eliminate, remove/drop.
     """
     start = time.perf_counter()
-    if mode == "apply" and _active_toolset() == "v1":
+    if mode == "apply" and ACTIVE_TOOLSET == "legacy":
         return envelope_err(
             ErrorType.FEATURE_DISABLED,
-            "Schema apply is disabled in V1. Use validate or dry_run mode.",
+            "Schema apply is disabled in the legacy toolset. Use validate or dry_run mode.",
             suggestion="Use mode='validate' or mode='dry_run' to preview schema changes.",
             source="apply_schema_tool",
             details={"mode": mode, "tool": "apply_schema_tool"},
@@ -496,7 +486,7 @@ def apply_schema_tool(
     )
 
 
-@_register_v2_core_tool
+@_register_core_tool
 def mutate_graph_properties_tool(
     target: str,
     operation: str,
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/inspect_graph.py b/hugegraph-mcp/hugegraph_mcp/tools/inspect_graph.py
index 912bab28..84b7c720 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/inspect_graph.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/inspect_graph.py
@@ -26,6 +26,7 @@ from hugegraph_mcp.envelope import envelope_ok
 from hugegraph_mcp.gremlin_tools import execute_gremlin_read
 from hugegraph_mcp.hugegraph_ai_client import health_check
 from hugegraph_mcp.schema_tools import get_live_schema
+from hugegraph_mcp.toolsets import DEFAULT_TOOLSET
 
 
 def _extract_count(result: dict[str, Any]) -> int | None:
@@ -113,7 +114,7 @@ def _check_ai_status(cfg: MCPConfig) -> tuple[str, Any, list[str]]:
 def inspect_graph(
     include_raw_schema: bool = False,
     include_counts: bool = False,
-    toolset: str = "v2_core",
+    toolset: str = DEFAULT_TOOLSET,
 ) -> dict[str, Any]:
     """检视 HugeGraph 服务器状态、schema 摘要和 AI 状态。
 
@@ -189,17 +190,17 @@ def _run_count_query(query: str, label: str, warnings: list[str]) -> int | None:
         return None
 
 
-def _next_actions(data: dict[str, Any], *, toolset: str = "v2_core") -> list[str]:
+def _next_actions(data: dict[str, Any], *, toolset: str = DEFAULT_TOOLSET) -> list[str]:
     """根据当前状态给出下一步建议,引导 Agent 使用正确的后续工具。"""
     actions = [
         "Use inspect_graph_tool with include_raw_schema=true for full schema details",
         "Use inspect_graph_tool with include_counts=true to fetch vertex/edge counts",
     ]
     if data.get("hugegraph_server_status") == "available":
-        if toolset == "v2_core":
+        if toolset == "core":
             actions.append("Use query_graph_data_tool for bounded structured graph queries")
         else:
-            actions.append("Set HUGEGRAPH_MCP_TOOLSET=v2_core and restart to enable structured graph queries")
+            actions.append("Set HUGEGRAPH_MCP_TOOLSET=core and restart to enable structured graph queries")
     else:
         actions.append("Check HugeGraph Server URL, graph name, and credentials")
     if data.get("hugegraph_ai_status") == "unavailable":
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/inspect_schema.py b/hugegraph-mcp/hugegraph_mcp/tools/inspect_schema.py
index 0af349c7..05281f9d 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/inspect_schema.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/inspect_schema.py
@@ -11,7 +11,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Schema inspection tool for v2_core."""
+"""Schema inspection tool for core."""
 
 from typing import Any
 
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/manage_schema.py b/hugegraph-mcp/hugegraph_mcp/tools/manage_schema.py
index 4a39cbaf..59045932 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/manage_schema.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/manage_schema.py
@@ -13,7 +13,7 @@
 
 """Schema 管理统一入口 — design / validate / dry_run / apply 模式。
 
-v2_core 解锁最窄 schema apply:create_property_key、create_vertex_label、
+core 解锁最窄 schema apply:create_property_key、create_vertex_label、
 create_edge_label。index/rebuild、schema remove/drop、schema append/eliminate
 仍不在 P0a 范围内。
 """
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/mutate_graph_properties.py b/hugegraph-mcp/hugegraph_mcp/tools/mutate_graph_properties.py
index d7565262..8e234cfd 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/mutate_graph_properties.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/mutate_graph_properties.py
@@ -11,7 +11,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Controlled vertex/edge property append/eliminate tool for v2_core."""
+"""Controlled vertex/edge property append/eliminate tool for core."""
 
 from typing import Any
 from uuid import uuid4
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/query_graph_data.py b/hugegraph-mcp/hugegraph_mcp/tools/query_graph_data.py
index 1fa333df..da622443 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/query_graph_data.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/query_graph_data.py
@@ -11,7 +11,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Typed graph data query tool for v2_core."""
+"""Typed graph data query tool for core."""
 
 import json
 from typing import Any
diff --git a/hugegraph-mcp/hugegraph_mcp/tools/refresh_vid_embeddings.py b/hugegraph-mcp/hugegraph_mcp/tools/refresh_vid_embeddings.py
index 8956aa87..3101d47f 100644
--- a/hugegraph-mcp/hugegraph_mcp/tools/refresh_vid_embeddings.py
+++ b/hugegraph-mcp/hugegraph_mcp/tools/refresh_vid_embeddings.py
@@ -19,9 +19,10 @@
 import re
 from typing import Any
 
+from hugegraph_mcp.config import MCPConfig
 from hugegraph_mcp.envelope import ErrorType, envelope_err, envelope_ok
 from hugegraph_mcp.guard import Capability, guard
-from hugegraph_mcp.hugegraph_ai_client import post
+from hugegraph_mcp.hugegraph_ai_client import get, post
 
 
 def refresh_vid_embeddings(confirm: bool = False) -> dict[str, Any]:
@@ -41,8 +42,58 @@ def refresh_vid_embeddings(confirm: bool = False) -> dict[str, Any]:
             suggestion="Pass confirm=True to refresh VID embeddings.",
         )
 
-    ai_result = post("/vid-embeddings/refresh", json={})
+    cfg = MCPConfig.from_env()
+    capability = get("/vid-embeddings/refresh-capabilities", cfg=cfg)
+    if not capability.get("ok"):
+        error = capability.get("error") or {}
+        # Only an explicitly absent/unsupported endpoint means the protocol is
+        # unavailable. Keep transient and authentication errors retryable as
+        # reported by the client, without ever dispatching the refresh.
+        status_code = (error.get("details") or {}).get("status_code")
+        if error.get("type") in {
+            "AUTHENTICATION_FAILED",
+            "AUTHORIZATION_FAILED",
+            "FEATURE_DISABLED",
+        } or status_code not in {404, 405}:
+            return capability
+        return envelope_err(
+            ErrorType.FEATURE_DISABLED,
+            "AI refresh target-validation endpoint is unavailable; refresh was not sent.",
+            suggestion="Check the AI service URL and routing, or upgrade HugeGraph-AI to support target validation.",
+            details={"status_code": status_code},
+        )
+    support = capability.get("data")
+    if (
+        not isinstance(support, dict)
+        or type(support.get("protocol_version")) is not int
+        or support["protocol_version"] != 1
+    ):
+        return envelope_err(
+            ErrorType.FEATURE_DISABLED,
+            "AI refresh target-validation protocol is unsupported; refresh was not sent.",
+            suggestion="Upgrade HugeGraph-AI to a compatible target-validation version.",
+        )
+    # Discovery and refresh can reach different replica versions. Only this
+    # versioned route binds validation to execution; never fall back to legacy.
+    ai_result = post(
+        "/v1/vid-embeddings/refresh",
+        cfg=cfg,
+        json={"expected_graph": cfg.graph, "expected_graphspace": cfg.graphspace},
+    )
     if not ai_result.get("ok"):
+        error = ai_result.get("error") or {}
+        status_code = (error.get("details") or {}).get("status_code")
+        if status_code in {404, 405} and error.get("type") not in {
+            "AUTHENTICATION_FAILED",
+            "AUTHORIZATION_FAILED",
+            "FEATURE_DISABLED",
+        }:
+            return envelope_err(
+                ErrorType.FEATURE_DISABLED,
+                "AI validated-refresh endpoint is unavailable; legacy refresh was not sent.",
+                suggestion="Check AI routing and upgrade all replicas to support /v1/vid-embeddings/refresh.",
+                details={"status_code": status_code},
+            )
         return ai_result
 
     data = _unwrap_ai_payload(ai_result.get("data"))
diff --git a/hugegraph-mcp/hugegraph_mcp/toolsets.py b/hugegraph-mcp/hugegraph_mcp/toolsets.py
new file mode 100644
index 00000000..1d8ccc7c
--- /dev/null
+++ b/hugegraph-mcp/hugegraph_mcp/toolsets.py
@@ -0,0 +1,43 @@
+# 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.
+
+"""Toolset configuration names, independent of package and tool contract versions."""
+
+import logging
+
+DEFAULT_TOOLSET = "core"
+VALID_TOOLSETS = frozenset({DEFAULT_TOOLSET, "legacy"})
+_ALIASES = {"v2_core": "core", "v1": "legacy"}
+LOGGER = logging.getLogger(__name__)
+
+
+def resolve_toolset(value: str | None) -> str:
+    """Normalize configuration once at startup; never silently change capabilities."""
+    if value is None:
+        return DEFAULT_TOOLSET
+    value = value.strip()
+    if value in VALID_TOOLSETS:
+        return value
+    if value in _ALIASES:
+        canonical = _ALIASES[value]
+        LOGGER.warning(
+            "HUGEGRAPH_MCP_TOOLSET=%s is deprecated; use %s. The toolset reported by inspect_graph_tool is %s.",
+            value,
+            canonical,
+            canonical,
+        )
+        return canonical
+    raise ValueError(
+        f"Invalid HUGEGRAPH_MCP_TOOLSET value {value!r}; expected core or legacy "
+        "(deprecated aliases: v2_core, v1). Unset the variable to use core."
+    )
diff --git a/hugegraph-mcp/tests/distribution_smoke.py b/hugegraph-mcp/tests/distribution_smoke.py
index 2b4ea130..4000eb8e 100644
--- a/hugegraph-mcp/tests/distribution_smoke.py
+++ b/hugegraph-mcp/tests/distribution_smoke.py
@@ -103,7 +103,7 @@ def exercise(process, timeout):
     require("tools" in initialized["capabilities"], "Server does not advertise tools")
     send({"method": "notifications/initialized"})
     tools = {tool["name"] for tool in call(2, "tools/list", {})["tools"]}
-    require(len(tools) == 16, f"Expected 16 v2 tools, received {len(tools)}")
+    require(len(tools) == 16, f"Expected 16 core tools, received {len(tools)}")
     require({"inspect_graph_tool", "query_graph_data_tool"} <= tools, "Missing inspection/query tools")
 
     def tool_call(request_id, name, arguments):
@@ -117,7 +117,7 @@ def exercise(process, timeout):
 
     inspected = tool_call(3, "inspect_graph_tool", {"include_raw_schema": True})
     require(inspected["hugegraph_server_status"] == "available", "HTTP graph fixture is unavailable")
-    require(inspected["toolset"] == "v2_core", "Unexpected toolset")
+    require(inspected["toolset"] == "core", "Unexpected toolset")
     queried = tool_call(
         4, "query_graph_data_tool", {"target": "vertex", "operation": "page", "label": "person", "limit": 5}
     )
@@ -147,7 +147,7 @@ def main():
             HUGEGRAPH_PASSWORD="admin",
             HUGEGRAPH_MCP_READONLY="true",
             HUGEGRAPH_MCP_ALLOW_AI="false",
-            HUGEGRAPH_MCP_TOOLSET="v2_core",
+            HUGEGRAPH_MCP_TOOLSET="core",
             HUGEGRAPH_MCP_STATE_DIR=str(Path(scratch) / "state"),
             NO_PROXY="127.0.0.1,localhost",
             no_proxy="127.0.0.1,localhost",
diff --git a/hugegraph-mcp/tests/test_config.py b/hugegraph-mcp/tests/test_config.py
index c45eed6c..bc65e63b 100644
--- a/hugegraph-mcp/tests/test_config.py
+++ b/hugegraph-mcp/tests/test_config.py
@@ -451,3 +451,11 @@ def test_cached_config_uses_same_environment_snapshot_for_key_and_value(monkeypa
 
     assert environment.readonly_reads == 1
     assert cfg.readonly is False
+
+
+def test_deprecated_ai_graph_url_warns_without_changing_target(caplog):
+    with caplog.at_level(logging.WARNING):
+        cfg = MCPConfig.from_env({"HUGEGRAPH_AI_GRAPH_URL": "http://ignored:8080"})
+    assert cfg.url == "http://127.0.0.1:8080"
+    assert "has no effect" in caplog.text
+    assert "configure GRAPH_URL on the AI service" in caplog.text
diff --git a/hugegraph-mcp/tests/test_inspect_graph.py b/hugegraph-mcp/tests/test_inspect_graph.py
index 48873844..70666e01 100644
--- a/hugegraph-mcp/tests/test_inspect_graph.py
+++ b/hugegraph-mcp/tests/test_inspect_graph.py
@@ -392,7 +392,7 @@ def test_inspect_graph_v1_does_not_recommend_unavailable_tools(monkeypatch):
     from hugegraph_mcp.tools import inspect_graph as inspect_graph_module
 
     monkeypatch.setattr(inspect_graph_module, "get_live_schema", lambda: _schema_result())
-    result = inspect_graph_module.inspect_graph(toolset="v1")
-    assert any("HUGEGRAPH_MCP_TOOLSET=v2_core" in action for action in result["next_actions"])
+    result = inspect_graph_module.inspect_graph(toolset="legacy")
+    assert any("HUGEGRAPH_MCP_TOOLSET=core" in action for action in result["next_actions"])
     assert not any("query_graph_data_tool" in action for action in result["next_actions"])
     assert not any("execute_gremlin_read_tool" in action for action in result["next_actions"])
diff --git a/hugegraph-mcp/tests/test_public_docs_contract.py b/hugegraph-mcp/tests/test_public_docs_contract.py
index 270119cd..ff6addeb 100644
--- a/hugegraph-mcp/tests/test_public_docs_contract.py
+++ b/hugegraph-mcp/tests/test_public_docs_contract.py
@@ -98,9 +98,15 @@ def test_source_launch_contract_binds_both_checkout_packages():
         assert "uv venv --python 3.10 .venv-mcp" in section
         assert "uv --no-config pip install --python .venv-mcp" in section
         assert "-e ./hugegraph-python-client -e ./hugegraph-mcp" in section
-        assert ".venv-mcp/bin/python -m hugegraph_mcp.server" in section
         assert "/Users/" not in section
 
+    for section in (english, chinese):
+        assert ".venv-mcp/bin/python -m hugegraph_mcp.server" in section
+    # Integration calls share the pipes owned by the client, rather than
+    # trying to connect a second client to a detached STDIO server.
+    assert ".venv-mcp/bin/python hugegraph-mcp/examples/stdio_check.py --interactive" in checklist
+    assert "Use a second MCP client process" not in checklist
+
     for path in PUBLIC_DOCS:
         assert "/Users/" not in _text(path)
 
diff --git a/hugegraph-mcp/tests/test_readonly_mode.py b/hugegraph-mcp/tests/test_readonly_mode.py
index 0a98b449..0c21e8e4 100644
--- a/hugegraph-mcp/tests/test_readonly_mode.py
+++ b/hugegraph-mcp/tests/test_readonly_mode.py
@@ -67,16 +67,16 @@ def test_readonly_env_parsing(tmp_path):
 
 
 def test_write_tools_available_when_not_readonly(monkeypatch):
-    """Test that all V1 tools are registered when not in readonly mode."""
+    """Test that all legacy tools are registered when not in readonly mode."""
     monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
-    monkeypatch.setenv("HUGEGRAPH_MCP_TOOLSET", "v1")
+    monkeypatch.setenv("HUGEGRAPH_MCP_TOOLSET", "legacy")
 
     import hugegraph_mcp.server
 
     try:
         importlib.reload(hugegraph_mcp.server)
         tools = get_registered_tools()
-        # V1 stable tools
+        # legacy stable tools
         assert "inspect_graph_tool" in tools
         assert "generate_gremlin_tool" in tools
         assert "execute_gremlin_read_tool" in tools
@@ -98,9 +98,9 @@ def test_write_tools_available_when_not_readonly(monkeypatch):
 
 
 def test_write_tools_disabled_when_readonly(monkeypatch):
-    """Test that all V1 tools remain registered in readonly mode."""
+    """Test that all legacy tools remain registered in readonly mode."""
     monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "true")
-    monkeypatch.setenv("HUGEGRAPH_MCP_TOOLSET", "v1")
+    monkeypatch.setenv("HUGEGRAPH_MCP_TOOLSET", "legacy")
 
     import hugegraph_mcp.server
 
@@ -128,9 +128,9 @@ def test_write_tools_disabled_when_readonly(monkeypatch):
 
 
 def test_readonly_mode_default(monkeypatch):
-    """Test that default mode is readonly when env is not set (V1 safe default)."""
+    """Test that default mode is readonly when env is not set (legacy safe default)."""
     monkeypatch.delenv("HUGEGRAPH_MCP_READONLY", raising=False)
-    monkeypatch.setenv("HUGEGRAPH_MCP_TOOLSET", "v1")
+    monkeypatch.setenv("HUGEGRAPH_MCP_TOOLSET", "legacy")
 
     import hugegraph_mcp.server
 
diff --git a/hugegraph-mcp/tests/test_refresh_vid_embeddings.py b/hugegraph-mcp/tests/test_refresh_vid_embeddings.py
index 04067108..b23cc23d 100644
--- a/hugegraph-mcp/tests/test_refresh_vid_embeddings.py
+++ b/hugegraph-mcp/tests/test_refresh_vid_embeddings.py
@@ -13,10 +13,21 @@
 
 from unittest.mock import Mock
 
-from hugegraph_mcp.envelope import envelope_ok
+import pytest
+import requests
+
+from hugegraph_mcp.envelope import ErrorType, envelope_err, envelope_ok
+from hugegraph_mcp.hugegraph_ai_client import get as ai_get
+from hugegraph_mcp.hugegraph_ai_client import post as ai_post
 from hugegraph_mcp.tools import refresh_vid_embeddings as refresh_vid_embeddings_module
 
 
+@pytest.fixture(autouse=True)
+def supported_refresh(monkeypatch):
+    monkeypatch.setattr(refresh_vid_embeddings_module, "get", Mock(return_value=envelope_ok({"protocol_version": 1})))
+    monkeypatch.setenv("HUGEGRAPH_GRAPH_PATH", "DEFAULT/hugegraph")
+
+
 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"}}))
@@ -26,7 +37,8 @@ def test_refresh_vid_embeddings_success(monkeypatch):
 
     assert result["ok"] is True
     assert result["data"] == {"added": 3, "removed": 1, "summary": "done"}
-    post.assert_called_once_with("/vid-embeddings/refresh", json={})
+    assert post.call_args.args == ("/v1/vid-embeddings/refresh",)
+    assert post.call_args.kwargs["json"] == {"expected_graph": "hugegraph", "expected_graphspace": "DEFAULT"}
 
 
 def test_refresh_vid_embeddings_parses_remove_variants(monkeypatch):
@@ -63,3 +75,131 @@ def test_refresh_vid_embeddings_missing_confirm(monkeypatch):
     assert result["ok"] is False
     assert result["error"]["type"] == "CONFIRM_REQUIRED"
     post.assert_not_called()
+
+
+@pytest.mark.parametrize("data", [{}, {"protocol_version": True}, {"protocol_version": 2}, None])
+def test_old_ai_never_receives_refresh(monkeypatch, data):
+    monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
+    monkeypatch.setattr(refresh_vid_embeddings_module, "get", Mock(return_value=envelope_ok(data)))
+    post = Mock()
+    monkeypatch.setattr(refresh_vid_embeddings_module, "post", post)
+    result = refresh_vid_embeddings_module.refresh_vid_embeddings(confirm=True)
+    assert result["error"]["type"] == "FEATURE_DISABLED"
+    post.assert_not_called()
+
+
+@pytest.mark.parametrize(
+    "error_type", [ErrorType.HUGEGRAPH_AI_UNAVAILABLE, ErrorType.AUTHORIZATION_FAILED, ErrorType.FEATURE_DISABLED]
+)
+def test_capability_errors_preserve_envelope_and_never_post(monkeypatch, error_type):
+    monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
+    upstream = envelope_err(error_type, "probe failed", retryable=True, details={"reason": "test_failure"})
+    monkeypatch.setattr(refresh_vid_embeddings_module, "get", Mock(return_value=upstream))
+    post = Mock()
+    monkeypatch.setattr(refresh_vid_embeddings_module, "post", post)
+    assert refresh_vid_embeddings_module.refresh_vid_embeddings(confirm=True) is upstream
+    post.assert_not_called()
+
+
+@pytest.mark.parametrize("status_code", [401, 403, 404, 405, 429, 500, 502])
+def test_http_capability_failures_keep_classification_without_refresh(monkeypatch, status_code):
+    monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
+    monkeypatch.setenv("HUGEGRAPH_MCP_ALLOW_AI", "true")
+    monkeypatch.setattr(refresh_vid_embeddings_module, "get", ai_get)
+    response = requests.Response()
+    response.status_code = status_code
+    response.url = "http://ai.test/vid-embeddings/refresh-capabilities"
+    response._content = b'{"detail":"probe failed"}'
+    transport = Mock(return_value=response)
+    monkeypatch.setattr(requests, "request", transport)
+    result = refresh_vid_embeddings_module.refresh_vid_embeddings(confirm=True)
+    assert result["ok"] is False
+    expected = (
+        "FEATURE_DISABLED"
+        if status_code in {404, 405}
+        else ("AUTHORIZATION_FAILED" if status_code in {401, 403} else "HUGEGRAPH_AI_UNAVAILABLE")
+    )
+    assert result["error"]["type"] == expected
+    assert result["error"]["retryable"] is (status_code == 429 or status_code >= 500)
+    assert result["error"]["details"]["status_code"] == status_code
+    assert transport.call_count == 1
+    assert transport.call_args.args[0] == "GET"
+
+
+@pytest.mark.parametrize(
+    "failure", [requests.exceptions.Timeout("timeout"), requests.exceptions.ConnectionError("offline")]
+)
+def test_network_capability_failure_remains_retryable_without_refresh(monkeypatch, failure):
+    monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
+    monkeypatch.setenv("HUGEGRAPH_MCP_ALLOW_AI", "true")
+    monkeypatch.setattr(refresh_vid_embeddings_module, "get", ai_get)
+    transport = Mock(side_effect=failure)
+    monkeypatch.setattr(requests, "request", transport)
+    result = refresh_vid_embeddings_module.refresh_vid_embeddings(confirm=True)
+    assert result["error"]["type"] == "HUGEGRAPH_AI_UNAVAILABLE"
+    assert result["error"]["retryable"] is True
+    assert transport.call_count == 1
+    assert transport.call_args.args[0] == "GET"
+
+
+@pytest.mark.parametrize("status_code", [404, 405])
+def test_mixed_version_routing_never_dispatches_legacy_refresh(monkeypatch, status_code):
+    monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
+    monkeypatch.setenv("HUGEGRAPH_MCP_ALLOW_AI", "true")
+    monkeypatch.setenv("HUGEGRAPH_AI_URL", "http://ai.test")
+    monkeypatch.setattr(refresh_vid_embeddings_module, "get", ai_get)
+    monkeypatch.setattr(refresh_vid_embeddings_module, "post", ai_post)
+    legacy_dispatch = Mock()
+
+    def route_between_replicas(method, url, **kwargs):
+        response = requests.Response()
+        response.url = url
+        response.status_code = 200
+        if method == "GET":
+            # Discovery reaches a new replica advertising the existing protocol.
+            response._content = b'{"protocol_version":1}'
+        elif url == "http://ai.test/vid-embeddings/refresh":
+            # POST reaches an old replica: its empty request model ignores targets.
+            legacy_dispatch(kwargs["json"])
+            response._content = b'{"added":3,"removed":1}'
+        else:
+            # Old replicas cannot execute the versioned validated-refresh route.
+            response.status_code = status_code
+            response._content = b'{"detail":"Not Found"}'
+        return response
+
+    transport = Mock(side_effect=route_between_replicas)
+    monkeypatch.setattr(requests, "request", transport)
+
+    result = refresh_vid_embeddings_module.refresh_vid_embeddings(confirm=True)
+
+    assert result["ok"] is False
+    assert result["error"]["type"] == "FEATURE_DISABLED"
+    assert result["error"]["details"]["status_code"] == status_code
+    assert "upgrade" in result["error"]["suggestion"].lower()
+    legacy_dispatch.assert_not_called()
+    assert [(call.args[0], call.args[1]) for call in transport.call_args_list] == [
+        ("GET", "http://ai.test/vid-embeddings/refresh-capabilities"),
+        ("POST", "http://ai.test/v1/vid-embeddings/refresh"),
+    ]
+
+
+@pytest.mark.parametrize("status_code", [401, 403, 429, 500, 502])
+def test_validated_refresh_errors_preserve_classification_without_fallback(monkeypatch, status_code):
+    monkeypatch.setenv("HUGEGRAPH_MCP_READONLY", "false")
+    monkeypatch.setenv("HUGEGRAPH_MCP_ALLOW_AI", "true")
+    monkeypatch.setattr(refresh_vid_embeddings_module, "post", ai_post)
+    response = requests.Response()
+    response.status_code = status_code
+    response._content = b'{"detail":"refresh failed"}'
+    transport = Mock(return_value=response)
+    monkeypatch.setattr(requests, "request", transport)
+
+    result = refresh_vid_embeddings_module.refresh_vid_embeddings(confirm=True)
+
+    expected = "AUTHORIZATION_FAILED" if status_code in {401, 403} else "HUGEGRAPH_AI_UNAVAILABLE"
+    assert result["error"]["type"] == expected
+    assert result["error"]["retryable"] is (status_code == 429 or status_code >= 500)
+    assert transport.call_count == 1
+    assert transport.call_args.args[0] == "POST"
+    assert transport.call_args.args[1].endswith("/v1/vid-embeddings/refresh")
diff --git a/hugegraph-mcp/tests/test_stdio_check.py b/hugegraph-mcp/tests/test_stdio_check.py
new file mode 100644
index 00000000..8b210b03
--- /dev/null
+++ b/hugegraph-mcp/tests/test_stdio_check.py
@@ -0,0 +1,117 @@
+# 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 asyncio
+import importlib.util
+from contextlib import asynccontextmanager
+from pathlib import Path
+from types import SimpleNamespace
+from unittest.mock import AsyncMock, Mock, call
+
+import pytest
+from mcp.types import CallToolResult
+
+
+@pytest.fixture
+def stdio_example(monkeypatch):
+    path = Path(__file__).resolve().parents[1] / "examples" / "stdio_check.py"
+    spec = importlib.util.spec_from_file_location("stdio_check_example", path)
+    module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(module)
+    session = AsyncMock()
+    session.__aenter__.return_value = session
+    session.list_tools.return_value = SimpleNamespace(
+        tools=[SimpleNamespace(name="inspect_graph_tool"), SimpleNamespace(name="query_graph_data_tool")]
+    )
+    session.call_tool.return_value = CallToolResult(
+        content=[], structuredContent={"ok": True, "data": {"hugegraph_server_status": "available"}}
+    )
+
+    @asynccontextmanager
+    async def transport(_params):
+        yield object(), object()
+
+    monkeypatch.setattr(module, "stdio_client", transport)
+    monkeypatch.setattr(module, "ClientSession", Mock(return_value=session))
+    return module, session
+
+
+@pytest.mark.parametrize(
+    "invalid_request",
+    [
+        "{",
+        "[]",
+        "null",
+        "42",
+        '"tool"',
+        "{}",
+        '{"name": null}',
+        '{"name": 42}',
+        '{"name": ""}',
+        '{"name": "  "}',
+        '{"name": "query_graph_data_tool", "arguments": []}',
+        '{"name": "query_graph_data_tool", "arguments": null}',
+        '{"name": "query_graph_data_tool", "arguments": "query"}',
+    ],
+)
+def test_interactive_invalid_input_keeps_same_session_open(stdio_example, monkeypatch, capsys, invalid_request):
+    module, session = stdio_example
+    monkeypatch.setattr(
+        "builtins.input",
+        Mock(
+            side_effect=[
+                invalid_request,
+                '{"name": "query_graph_data_tool", "arguments": {"query": "g.V().count()"}}',
+                EOFError,
+            ]
+        ),
+    )
+
+    asyncio.run(module.run(None, interactive=True))
+
+    session.initialize.assert_awaited_once()
+    module.ClientSession.assert_called_once()
+    assert session.call_tool.await_args_list == [
+        call("inspect_graph_tool", {"include_counts": False}),
+        call("query_graph_data_tool", {"query": "g.V().count()"}),
+    ]
+    assert "Invalid tools/call request:" in capsys.readouterr().err
+
+
+def test_interactive_blank_input_and_omitted_arguments(stdio_example, monkeypatch, capsys):
+    module, session = stdio_example
+    monkeypatch.setattr("builtins.input", Mock(side_effect=["  ", '{"name": "query_graph_data_tool"}', EOFError]))
+
+    asyncio.run(module.run(None, interactive=True))
+
+    assert session.call_tool.await_args_list == [
+        call("inspect_graph_tool", {"include_counts": False}),
+        call("query_graph_data_tool", {}),
+    ]
+    assert not capsys.readouterr().err
+
+
+@pytest.mark.parametrize("failure", [ValueError("Tool execution failed"), CallToolResult(content=[], isError=True)])
+def test_interactive_tool_failure_still_terminates(stdio_example, monkeypatch, capsys, failure):
+    module, session = stdio_example
+    session.call_tool.side_effect = [session.call_tool.return_value, failure]
+    user_input = Mock(side_effect=['{"name": "query_graph_data_tool"}', EOFError])
+    monkeypatch.setattr("builtins.input", user_input)
+
+    error_type = ValueError if isinstance(failure, Exception) else RuntimeError
+    with pytest.raises(error_type):
+        asyncio.run(module.run(None, interactive=True))
+
+    assert session.call_tool.await_count == 2
+    user_input.assert_called_once()
+    assert not capsys.readouterr().err
diff --git a/hugegraph-mcp/tests/test_v1_stable_tools.py b/hugegraph-mcp/tests/test_v1_stable_tools.py
index c4fc4543..68e17b36 100644
--- a/hugegraph-mcp/tests/test_v1_stable_tools.py
+++ b/hugegraph-mcp/tests/test_v1_stable_tools.py
@@ -11,11 +11,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Tests for V1 stable tools and admin gate (Milestone 2)."""
+"""Tests for shared tools, toolset selection, and admin gates."""
 
 import asyncio
 import logging
 import warnings
+from pathlib import Path
 from unittest.mock import Mock
 
 import pytest
@@ -33,7 +34,7 @@ async def _list_mcp_tools():
     return await list_tools()
 
 
-def _assert_v1_envelope_shape(result):
+def _assert_envelope_shape(result):
     assert set(result) == {"ok", "data", "error", "warnings", "next_actions", "meta"}
     assert result["meta"]["request_id"].startswith("req-")
     assert "graph" in result["meta"]
@@ -42,7 +43,7 @@ def _assert_v1_envelope_shape(result):
     assert "duration_ms" in result["meta"]
 
 
-V1_TOOL_NAMES = {
+LEGACY_TOOL_NAMES = {
     "inspect_graph_tool",
     "generate_gremlin_tool",
     "execute_gremlin_read_tool",
@@ -55,7 +56,7 @@ V1_TOOL_NAMES = {
     "execute_gremlin_write_tool",
 }
 
-V2_CORE_TOOL_NAMES = V1_TOOL_NAMES | {
+CORE_TOOL_NAMES = LEGACY_TOOL_NAMES | {
     "confirm_write_tool",
     "get_write_status_tool",
     "inspect_schema_tool",
@@ -65,14 +66,14 @@ V2_CORE_TOOL_NAMES = V1_TOOL_NAMES | {
 }
 
 
-def test_public_tool_contract_lists_v2_core_tools_by_default(monkeypatch):
+def test_public_tool_contract_lists_core_tools_by_default(monkeypatch):
     monkeypatch.delenv("HUGEGRAPH_MCP_TOOLSET", raising=False)
 
     async def _tool_names():
         tools = await _list_mcp_tools()
         return {tool.name for tool in tools}
 
-    assert asyncio.run(_tool_names()) == V2_CORE_TOOL_NAMES
+    assert asyncio.run(_tool_names()) == CORE_TOOL_NAMES
 
 
 def test_import_graph_data_tool_runtime_input_schema():
@@ -101,8 +102,8 @@ def test_import_graph_data_tool_runtime_input_schema():
     assert input_schema["required"] == ["mode"]
 
 
-def test_public_tool_contract_can_reload_as_v1(monkeypatch):
-    monkeypatch.setenv("HUGEGRAPH_MCP_TOOLSET", "v1")
+def test_public_tool_contract_can_reload_as_legacy(monkeypatch):
+    monkeypatch.setenv("HUGEGRAPH_MCP_TOOLSET", "legacy")
     import importlib
 
     import hugegraph_mcp.server as server_module
@@ -117,34 +118,79 @@ def test_public_tool_contract_can_reload_as_v1(monkeypatch):
         return {tool.name for tool in tools}
 
     try:
-        assert asyncio.run(_tool_names()) == V1_TOOL_NAMES
+        assert asyncio.run(_tool_names()) == LEGACY_TOOL_NAMES
     finally:
         monkeypatch.delenv("HUGEGRAPH_MCP_TOOLSET", raising=False)
         importlib.reload(server_module)
 
 
-def test_invalid_and_empty_toolsets_fail_closed_to_v1(monkeypatch, caplog):
+@pytest.mark.parametrize("value", ["typo", "", "  ", "v2", "CORE"])
+def test_invalid_toolsets_prevent_startup(monkeypatch, value):
     import importlib
 
-    import hugegraph_mcp.server as server_module
+    try:
+        monkeypatch.setenv("HUGEGRAPH_MCP_TOOLSET", value)
+        with pytest.raises(ValueError, match="expected core or legacy"):
+            importlib.reload(server)
+    finally:
+        monkeypatch.delenv("HUGEGRAPH_MCP_TOOLSET", raising=False)
+        importlib.reload(server)
 
-    async def _tool_names():
-        list_tools = getattr(server_module.mcp, "_mcp_list_tools", None)
-        if list_tools is None:
-            list_tools = server_module.mcp._list_tools
-        tools = await list_tools()
-        return {tool.name for tool in tools}
+
+@pytest.mark.parametrize(
+    ("value", "canonical", "expected_tools"),
+    [
+        ("core", "core", CORE_TOOL_NAMES),
+        ("legacy", "legacy", LEGACY_TOOL_NAMES),
+        ("v2_core", "core", CORE_TOOL_NAMES),
+        ("v1", "legacy", LEGACY_TOOL_NAMES),
+        (" core ", "core", CORE_TOOL_NAMES),
+    ],
+)
+def test_toolset_selection_metadata_and_startup_snapshot(monkeypatch, caplog, value, canonical, expected_tools):
+    import importlib
 
     try:
-        with caplog.at_level(logging.ERROR, logger="hugegraph_mcp.server"):
-            for value in ("typo", ""):
-                monkeypatch.setenv("HUGEGRAPH_MCP_TOOLSET", value)
-                importlib.reload(server_module)
-                assert asyncio.run(_tool_names()) == V1_TOOL_NAMES
-        assert any("Invalid HUGEGRAPH_MCP_TOOLSET" in message for message in caplog.messages)
+        monkeypatch.setenv("HUGEGRAPH_MCP_TOOLSET", value)
+        with caplog.at_level(logging.WARNING, logger="hugegraph_mcp.toolsets"):
+            importlib.reload(server)
+        deprecated = [record for record in caplog.records if "is deprecated" in record.message]
+        assert len(deprecated) == int(value in {"v1", "v2_core"})
+        # A later environment edit must not change metadata or behavior without a restart.
+        monkeypatch.setenv("HUGEGRAPH_MCP_TOOLSET", "legacy" if canonical == "core" else "core")
+        monkeypatch.setattr(server, "inspect_graph", lambda **kwargs: envelope_ok({"toolset": kwargs["toolset"]}))
+        calls = []
+        monkeypatch.setattr(server, "manage_schema", lambda **kwargs: calls.append(kwargs) or envelope_ok({}))
+
+        async def check_tools():
+            async with Client(server.mcp) as client:
+                registered = {tool.name for tool in await client.list_tools()}
+                assert registered == expected_tools
+                for filename in ("README.md", "README.zh-CN.md"):
+                    content = (Path(__file__).resolve().parents[1] / filename).read_text(encoding="utf-8")
+                    documented = set()
+                    for line in content.splitlines():
+                        cells = [cell.strip() for cell in line.split("|")]
+                        if len(cells) >= 4 and cells[1].endswith("_tool`") and canonical in cells[2]:
+                            documented.add(cells[1].strip("`"))
+                    assert documented == registered, filename
+                result = await client.call_tool("inspect_graph_tool", {})
+                return result.data
+
+        result = asyncio.run(check_tools())
+        assert result["data"]["toolset"] == canonical
+        assert result["meta"]["toolset"] == canonical
+        assert result["meta"]["mcp_tool_contract_version"] == "2.0"
+        applied = server.apply_schema_tool(mode="apply", operations=[])
+        if canonical == "legacy":
+            assert applied["error"]["type"] == "FEATURE_DISABLED"
+            assert not calls
+        else:
+            assert applied["ok"]
+            assert len(calls) == 1
     finally:
         monkeypatch.delenv("HUGEGRAPH_MCP_TOOLSET", raising=False)
-        importlib.reload(server_module)
+        importlib.reload(server)
 
 
 def test_public_tool_argument_models_do_not_emit_schema_shadow_warning():
@@ -174,7 +220,7 @@ def test_generate_gremlin_tool_execute_fails_closed_even_for_admin(monkeypatch):
         output_types=["vertex"],
     )
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is False
     assert result["error"]["type"] == "FEATURE_DISABLED"
     assert "hard-budget contract is incomplete" in result["error"]["message"]
@@ -189,7 +235,7 @@ def test_generate_gremlin_tool_still_generates_without_execution(monkeypatch):
 
     result = server.generate_gremlin_tool(query="count vertices", execute=False)
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is True
     assert result["data"] == expected["data"]
     mock.assert_called_once_with(
@@ -207,15 +253,15 @@ def test_inspect_graph_tool_adds_contract_fields(monkeypatch):
 
     result = server.inspect_graph_tool()
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["data"]["mcp_tool_contract_version"] == "2.0"
-    assert result["data"]["toolset"] == "v2_core"
+    assert result["data"]["toolset"] == "core"
     assert result["meta"]["mcp_tool_contract_version"] == "2.0"
-    assert result["meta"]["toolset"] == "v2_core"
-    mock.assert_called_once_with(include_raw_schema=False, include_counts=False, toolset="v2_core")
+    assert result["meta"]["toolset"] == "core"
+    mock.assert_called_once_with(include_raw_schema=False, include_counts=False, toolset="core")
 
     server.inspect_graph_tool(include_counts=True)
-    mock.assert_called_with(include_raw_schema=False, include_counts=True, toolset="v2_core")
+    mock.assert_called_with(include_raw_schema=False, include_counts=True, toolset="core")
 
 
 def test_execute_gremlin_read_tool_fails_closed_even_for_admin(monkeypatch):
@@ -225,7 +271,7 @@ def test_execute_gremlin_read_tool_fails_closed_even_for_admin(monkeypatch):
 
     result = server.execute_gremlin_read_tool(gremlin_query="g.V().limit(3)")
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is False
     assert result["error"]["type"] == "FEATURE_DISABLED"
     capabilities = result["error"]["details"]["required_capabilities"]
@@ -246,7 +292,7 @@ def test_extract_graph_data_tool_routes_to_extract_graph_data(monkeypatch):
         example_prompt="extract people",
     )
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is True
     assert result["data"] == expected["data"]
     mock.assert_called_once_with(
@@ -263,7 +309,7 @@ def test_design_schema_tool_routes_to_manage_schema_design(monkeypatch):
 
     result = server.design_schema_tool(operations=[{"op": "add_vertex_label"}])
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is True
     assert result["data"] == expected["data"]
     mock.assert_called_once_with(mode="design", operations=[{"op": "add_vertex_label"}])
@@ -276,7 +322,7 @@ def test_apply_schema_tool_validate_routes_to_manage_schema(monkeypatch):
 
     result = server.apply_schema_tool(mode="validate", operations=[{"op": "add_vertex_label"}])
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is True
     assert result["data"] == expected["data"]
     mock.assert_called_once_with(
@@ -296,7 +342,7 @@ def test_apply_schema_tool_dry_run_routes_to_manage_schema(monkeypatch):
 
     result = server.apply_schema_tool(mode="dry_run", operations=[{"op": "add_vertex_label"}])
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is True
     assert result["data"] == expected["data"]
     mock.assert_called_once_with(
@@ -309,7 +355,7 @@ def test_apply_schema_tool_dry_run_routes_to_manage_schema(monkeypatch):
     )
 
 
-def test_apply_schema_tool_apply_routes_in_v2_core(monkeypatch):
+def test_apply_schema_tool_apply_routes_in_core(monkeypatch):
     expected = envelope_ok({"status": "planned"})
     mock = Mock(return_value=expected)
     monkeypatch.setattr(server, "manage_schema", mock)
@@ -323,7 +369,7 @@ def test_apply_schema_tool_apply_routes_in_v2_core(monkeypatch):
         expires_at=9999999999,
     )
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is True
     assert result["warnings"][0].startswith(LEGACY_DEPRECATION_CODE)
     mock.assert_called_once_with(
@@ -380,7 +426,7 @@ def test_legacy_write_wrappers_reject_partial_locator_before_business_logic(
 
     result = getattr(server, tool_name)(**arguments, plan_hash="hash-only")
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is False
     assert result["error"]["type"] == "VALIDATION_ERROR"
     assert result["error"]["source"] == tool_name
@@ -394,13 +440,13 @@ def test_canonical_confirm_wrapper_passes_only_plan_id(monkeypatch):
 
     result = server.confirm_write_tool(plan_id="wp-1")
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is True
     mock.assert_called_once_with(plan_id="wp-1")
 
 
-def test_apply_schema_tool_apply_returns_feature_disabled_in_v1(monkeypatch):
-    monkeypatch.setenv("HUGEGRAPH_MCP_TOOLSET", "v1")
+def test_apply_schema_tool_apply_returns_feature_disabled_in_legacy(monkeypatch):
+    monkeypatch.setenv("HUGEGRAPH_MCP_TOOLSET", "legacy")
     import importlib
 
     import hugegraph_mcp.server as server_module
@@ -408,7 +454,7 @@ def test_apply_schema_tool_apply_returns_feature_disabled_in_v1(monkeypatch):
     importlib.reload(server_module)
     result = server_module.apply_schema_tool(mode="apply", operations=[{"op": "test"}])
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is False
     assert result["error"]["type"] == "FEATURE_DISABLED"
     assert result["error"]["source"] == "apply_schema_tool"
@@ -433,7 +479,7 @@ def test_delete_graph_data_tool_routes_to_manage_graph_data_delete(monkeypatch):
 
     result = server.delete_graph_data_tool(change_plan=change_plan)
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is True
     assert result["data"] == expected["data"]
     mock.assert_called_once_with(
@@ -467,7 +513,7 @@ def test_generate_gremlin_tool_aligns_error_source(monkeypatch):
 
     result = server.generate_gremlin_tool(query="count vertices")
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is False
     assert result["error"]["source"] == "generate_gremlin_tool"
 
@@ -479,7 +525,7 @@ def test_execute_gremlin_read_tool_reports_hard_budget_gate_source(monkeypatch):
 
     result = server.execute_gremlin_read_tool(gremlin_query="g.addV('person')")
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is False
     assert result["error"]["source"] == "execute_gremlin_read_tool"
     assert result["error"]["type"] == "FEATURE_DISABLED"
@@ -527,7 +573,7 @@ def test_admin_gate_blocks_refresh_embeddings_by_default(monkeypatch):
         result["error"]["suggestion"] == "Set HUGEGRAPH_MCP_ADMIN_MODE=true and HUGEGRAPH_MCP_READONLY=false "
         "to enable refresh_vid_embeddings_tool."
     )
-    assert result["error"]["details"]["toolset"] == "v2_core"
+    assert result["error"]["details"]["toolset"] == "core"
     assert result["error"]["details"]["required_env"] == {
         "HUGEGRAPH_MCP_ADMIN_MODE": "true",
         "HUGEGRAPH_MCP_READONLY": "false",
@@ -545,7 +591,7 @@ def test_raw_write_fails_closed_even_when_admin_and_writes_enabled(monkeypatch):
 
     result = server.execute_gremlin_write_tool(gremlin_query="g.addV('test')")
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is False
     assert result["error"]["type"] == "FEATURE_DISABLED"
     assert "hard-budget contract is incomplete" in result["error"]["message"]
@@ -583,7 +629,7 @@ def test_admin_gate_allows_refresh_embeddings_when_enabled(monkeypatch):
 
     result = server.refresh_vid_embeddings_tool(confirm=True)
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is True
     assert result["data"] == expected["data"]
     mock.assert_called_once_with(confirm=True)
@@ -602,7 +648,7 @@ def test_admin_gate_reads_current_env_after_import(monkeypatch):
 
     result = server.execute_gremlin_write_tool(gremlin_query="g.addV('test')")
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is False
     assert result["error"]["type"] == "FEATURE_DISABLED"
     mock.assert_not_called()
@@ -616,7 +662,7 @@ def test_execute_gremlin_write_tool_does_not_reach_executor(monkeypatch):
 
     result = server.execute_gremlin_write_tool(gremlin_query="g.addV('test')")
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is False
     assert result["error"]["type"] == "FEATURE_DISABLED"
     mock.assert_not_called()
@@ -630,7 +676,7 @@ def test_refresh_vid_embeddings_tool_wraps_unexpected_exceptions(monkeypatch):
 
     result = server.refresh_vid_embeddings_tool(confirm=True)
 
-    _assert_v1_envelope_shape(result)
+    _assert_envelope_shape(result)
     assert result["ok"] is False
     assert result["error"]["type"] == "FLOW_EXECUTION_FAILED"
     assert result["error"]["source"] == "refresh_vid_embeddings_tool"
diff --git a/skills/hugegraph-data-importer/SKILL.md b/skills/hugegraph-data-importer/SKILL.md
index 8d417665..8c7b025f 100644
--- a/skills/hugegraph-data-importer/SKILL.md
+++ b/skills/hugegraph-data-importer/SKILL.md
@@ -44,4 +44,6 @@ inspect_graph_tool(include_raw_schema=true)
 -> get_write_status_tool(plan_id) -> query_graph_data_tool
 ```
 
-Structured query and plan lifecycle tools require the default `v2_core` toolset. For a `v1` deployment, enable `HUGEGRAPH_MCP_TOOLSET=v2_core` and restart before using them.
+Structured query and plan lifecycle tools require the default `core` toolset. For a `legacy` deployment, enable `HUGEGRAPH_MCP_TOOLSET=core` and restart before using them.
+
+These skills describe current source. Published `1.7.1` uses `v2_core` / `v1`; see the [migration guide](../../hugegraph-mcp/README.md#toolset-naming-and-migration).
diff --git a/skills/hugegraph-operator/SKILL.md b/skills/hugegraph-operator/SKILL.md
index b60df562..1af02502 100644
--- a/skills/hugegraph-operator/SKILL.md
+++ b/skills/hugegraph-operator/SKILL.md
@@ -1,6 +1,6 @@
 ---
 name: hugegraph-operator
-description: Route HugeGraph MCP operational checks to status, schema, permission, AI availability, and read-only verification tools. Use when the user asks to check the current graph, schema, MCP connection, readonly state, HugeGraph-AI state, counts, indexes, or readiness.
+description: Route HugeGraph MCP operational checks to status, schema, MCP read-only state, AI availability, and read-only verification tools. Use when the user asks to check the current graph, schema, MCP connection, readonly state, HugeGraph-AI state, counts, indexes, or readiness.
 ---
 
 # HugeGraph Operator
@@ -9,7 +9,7 @@ description: Route HugeGraph MCP operational checks to status, schema, permissio
 
 | Goal | Tool |
 | --- | --- |
-| Quick check connection, status, permissions | `inspect_graph_tool(include_raw_schema=false)` |
+| Quick check connection, status, MCP read-only state | `inspect_graph_tool(include_raw_schema=false)` |
 | Fetch vertex/edge counts | `inspect_graph_tool(include_counts=true)` |
 | Inspect full schema, primary keys, indexes, edge endpoints | `inspect_graph_tool(include_raw_schema=true)` |
 | Check readonly, graph, or graphspace | `inspect_graph_tool(include_raw_schema=false)` |
@@ -34,4 +34,6 @@ Data verification: inspect_graph_tool(include_counts=true) -> query_graph_data_t
 
 When AI is disabled by configuration, `hugegraph_ai_status="disabled"` is normal; no AI service check is needed. `unavailable` means an enabled AI service check failed.
 
-Structured query and plan lifecycle tools require the default `v2_core` toolset. For a `v1` deployment, enable `HUGEGRAPH_MCP_TOOLSET=v2_core` and restart before using them.
+Structured query and plan lifecycle tools require the default `core` toolset. For a `legacy` deployment, enable `HUGEGRAPH_MCP_TOOLSET=core` and restart before using them.
+
+These skills describe current source. Published `1.7.1` uses `v2_core` / `v1`; see the [migration guide](../../hugegraph-mcp/README.md#toolset-naming-and-migration).
diff --git a/skills/hugegraph-operator/agents/openai.yaml b/skills/hugegraph-operator/agents/openai.yaml
index 9e7099e6..3ac4a6de 100644
--- a/skills/hugegraph-operator/agents/openai.yaml
+++ b/skills/hugegraph-operator/agents/openai.yaml
@@ -18,4 +18,4 @@
 interface:
   display_name: "HugeGraph Operator"
   short_description: "Inspect graph status and schema"
-  default_prompt: "Use $hugegraph-operator to check the current HugeGraph MCP status, schema, permissions, and next actions."
+  default_prompt: "Use $hugegraph-operator to check the current HugeGraph MCP status, schema, MCP read-only state, and next actions."
diff --git a/skills/hugegraph-query-analyst/SKILL.md b/skills/hugegraph-query-analyst/SKILL.md
index fe68252d..b4d298b7 100644
--- a/skills/hugegraph-query-analyst/SKILL.md
+++ b/skills/hugegraph-query-analyst/SKILL.md
@@ -24,4 +24,6 @@ Natural-language query: inspect_graph_tool -> query_graph_data_tool
 Existing Gremlin: explain it; translate supported queries into query_graph_data_tool arguments
 ```
 
-Structured query and plan lifecycle tools require the default `v2_core` toolset. For a `v1` deployment, enable `HUGEGRAPH_MCP_TOOLSET=v2_core` and restart before using them.
+Structured query and plan lifecycle tools require the default `core` toolset. For a `legacy` deployment, enable `HUGEGRAPH_MCP_TOOLSET=core` and restart before using them.
+
+These skills describe current source. Published `1.7.1` uses `v2_core` / `v1`; see the [migration guide](../../hugegraph-mcp/README.md#toolset-naming-and-migration).
diff --git a/skills/hugegraph-regression-tester/SKILL.md b/skills/hugegraph-regression-tester/SKILL.md
index f9978445..d37ddb5d 100644
--- a/skills/hugegraph-regression-tester/SKILL.md
+++ b/skills/hugegraph-regression-tester/SKILL.md
@@ -13,7 +13,7 @@ description: Route HugeGraph MCP regression testing tasks to the current public
 | Structured read | `query_graph_data_tool` (bounded page or exact ID query) |
 | Natural-language Gremlin generation | `generate_gremlin_tool(query, execute=false)` |
 | Generate Gremlin without execution | `generate_gremlin_tool(query, execute=false)`; raw execution must return `FEATURE_DISABLED` |
-| Write Gremlin rejection | `execute_gremlin_read_tool(unsafe_write_query)` |
+| Raw Gremlin disabled boundary | `execute_gremlin_read_tool(unsafe_write_query)`; rejection verifies unconditional disablement, not write classification |
 | Text-to-graph extraction | `extract_graph_data_tool(text, graph_schema?, example_prompt?)` |
 | Import dry-run | `import_graph_data_tool(mode="ingest", graph_data, dry_run=true)` |
 | Import preview boundary | Assert `confirmable=false`, `preview_only=true`, no `plan_id`; confirmation returns `FEATURE_DISABLED` |
@@ -21,8 +21,8 @@ description: Route HugeGraph MCP regression testing tasks to the current public
 | Schema design | `design_schema_tool(operations?)` |
 | Schema validation | `apply_schema_tool(mode="validate", operations)` |
 | Schema dry-run | `apply_schema_tool(mode="dry_run", operations)` |
-| Admin write-tool gate check | `execute_gremlin_write_tool(gremlin_query)` |
-| VID refresh gate check | `refresh_vid_embeddings_tool(confirm=false)` |
+| Raw write Gremlin disabled boundary | `execute_gremlin_write_tool(gremlin_query)`; must remain disabled even in admin mode |
+| Admin gate check without a write | `refresh_vid_embeddings_tool(confirm=false)` in separate admin-disabled and admin-enabled processes |
 
 ## Order
 
@@ -31,4 +31,19 @@ inspect -> generate/read -> extract -> import dry-run -> assert preview-only bou
 -> verify read -> schema validate/dry-run -> safety/gate checks
 ```
 
-Structured query and plan lifecycle tools require the default `v2_core` toolset. For a `v1` deployment, enable `HUGEGRAPH_MCP_TOOLSET=v2_core` and restart before using them.
+Structured query and plan lifecycle tools require the default `core` toolset. For a `legacy` deployment, enable `HUGEGRAPH_MCP_TOOLSET=core` and restart before using them.
+
+With `HUGEGRAPH_MCP_READONLY=false`, the admin-disabled refresh call must return
+`FEATURE_DISABLED` with details requiring `HUGEGRAPH_MCP_ADMIN_MODE=true`; the
+admin-enabled call must return `CONFIRM_REQUIRED`. Keep `confirm=false` in both
+processes to avoid a refresh. Restart between configuration changes. These calls
+do not verify AI-side authentication or target validation. Validate the write
+classifier independently with unit tests; raw Gremlin rejection cannot exercise it.
+For the refresh contract, run the API/MCP regression tests covering default
+GraphSpace equivalence, required fields and write/auth gates on
+`POST /v1/vid-embeddings/refresh`, configuration locking, and mixed-version routing.
+The capability GET is only discovery; MCP must never fall back to the legacy POST
+when the versioned route is unavailable. This does not verify physical-instance
+identity or establish a live rolling-deployment test.
+Generation and extraction require `HUGEGRAPH_MCP_ALLOW_AI=true` and a configured AI service.
+These skills describe current source; published `1.7.1` uses `v2_core` / `v1`.
diff --git a/skills/hugegraph-schema-designer/SKILL.md b/skills/hugegraph-schema-designer/SKILL.md
index 12c6e06c..740bd490 100644
--- a/skills/hugegraph-schema-designer/SKILL.md
+++ b/skills/hugegraph-schema-designer/SKILL.md
@@ -13,12 +13,12 @@ description: Route HugeGraph MCP schema design, validation, and dry-run preview
 | Design schema operations | `design_schema_tool(operations?)` |
 | Validate schema operations | `apply_schema_tool(mode="validate", operations)` |
 | Dry-run schema operations | `apply_schema_tool(mode="dry_run", operations)` |
-| Execute real schema apply | `confirm_write_tool(plan_id)` in default `v2_core`, one supported schema create per plan |
+| Execute real schema apply | `confirm_write_tool(plan_id)` in default `core`, one supported schema create per plan |
 | Delete or roll back schema | No V1 stable tool |
 
-The default `v2_core` toolset permits confirmed apply only for
+The default `core` toolset permits confirmed apply only for
 `create_property_key`, `create_vertex_label`, and `create_edge_label`. In
-`HUGEGRAPH_MCP_TOOLSET=v1`, real schema apply remains disabled.
+`HUGEGRAPH_MCP_TOOLSET=legacy`, real schema apply remains disabled.
 
 Review the dry-run response before applying it. Confirm only a returned, confirmable
 `plan_id` with `confirm_write_tool(plan_id)`, then query `get_write_status_tool(plan_id)`.
@@ -42,4 +42,6 @@ inspect_graph_tool -> design_schema_tool
 -> confirm_write_tool(plan_id) -> get_write_status_tool(plan_id)
 ```
 
-Structured query and plan lifecycle tools require the default `v2_core` toolset. For a `v1` deployment, enable `HUGEGRAPH_MCP_TOOLSET=v2_core` and restart before using them.
+Structured query and plan lifecycle tools require the default `core` toolset. For a `legacy` deployment, enable `HUGEGRAPH_MCP_TOOLSET=core` and restart before using them.
+
+These skills describe current source. Published `1.7.1` uses `v2_core` / `v1`; see the [migration guide](../../hugegraph-mcp/README.md#toolset-naming-and-migration).