You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2026/08/27 21:36:37 UTC

(superset) branch master updated: fix(mcp): add open-world tool annotations (#43529)

This is an automated email from the ASF dual-hosted git repository.

aminghadersohi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 478f612f258 fix(mcp): add open-world tool annotations (#43529)
478f612f258 is described below

commit 478f612f25890cf643b2941ac928de9b611194a0
Author: Amin Ghadersohi <am...@gmail.com>
AuthorDate: Thu Aug 27 17:36:30 2026 -0400

    fix(mcp): add open-world tool annotations (#43529)
---
 superset/mcp_service/CLAUDE.md                     | 13 +++++--
 .../tool/get_annotation_layer_info.py              |  1 +
 .../tool/get_layer_annotation_info.py              |  1 +
 .../tool/list_annotation_layers.py                 |  1 +
 .../tool/list_layer_annotations.py                 |  1 +
 superset/mcp_service/chart/tool/delete_chart.py    |  2 ++
 superset/mcp_service/chart/tool/generate_chart.py  |  2 ++
 superset/mcp_service/chart/tool/get_chart_data.py  |  1 +
 superset/mcp_service/chart/tool/get_chart_info.py  |  1 +
 .../mcp_service/chart/tool/get_chart_preview.py    |  1 +
 superset/mcp_service/chart/tool/get_chart_sql.py   |  1 +
 .../chart/tool/get_chart_type_schema.py            |  1 +
 superset/mcp_service/chart/tool/list_charts.py     |  1 +
 superset/mcp_service/chart/tool/restore_chart.py   |  2 ++
 superset/mcp_service/chart/tool/update_chart.py    |  2 ++
 .../mcp_service/chart/tool/update_chart_preview.py |  2 ++
 .../tool/add_chart_to_existing_dashboard.py        |  2 ++
 .../mcp_service/dashboard/tool/delete_dashboard.py |  2 ++
 .../dashboard/tool/duplicate_dashboard.py          |  2 ++
 .../dashboard/tool/generate_dashboard.py           |  2 ++
 .../dashboard/tool/get_dashboard_datasets.py       |  1 +
 .../dashboard/tool/get_dashboard_info.py           |  1 +
 .../dashboard/tool/get_dashboard_layout.py         |  1 +
 .../mcp_service/dashboard/tool/list_dashboards.py  |  1 +
 .../tool/manage_dashboard_certification.py         |  2 ++
 .../dashboard/tool/manage_dashboard_owners.py      |  2 ++
 .../dashboard/tool/manage_dashboard_roles.py       |  2 ++
 .../dashboard/tool/manage_native_filters.py        |  2 ++
 .../dashboard/tool/remove_chart_from_dashboard.py  |  2 ++
 .../dashboard/tool/restore_dashboard.py            |  2 ++
 .../mcp_service/dashboard/tool/update_dashboard.py |  2 ++
 .../mcp_service/database/tool/get_database_info.py |  1 +
 .../mcp_service/database/tool/list_databases.py    |  1 +
 .../mcp_service/dataset/tool/create_dataset.py     |  2 ++
 .../dataset/tool/create_virtual_dataset.py         |  2 ++
 .../mcp_service/dataset/tool/get_dataset_info.py   |  1 +
 superset/mcp_service/dataset/tool/list_datasets.py |  1 +
 superset/mcp_service/dataset/tool/query_dataset.py |  1 +
 .../dataset/tool/update_dataset_metric.py          |  2 ++
 .../explore/tool/generate_explore_link.py          |  2 ++
 superset/mcp_service/query/tool/get_query_info.py  |  1 +
 superset/mcp_service/query/tool/list_queries.py    |  1 +
 .../mcp_service/report/tool/get_report_info.py     |  1 +
 superset/mcp_service/report/tool/list_reports.py   |  1 +
 .../mcp_service/rls/tool/get_rls_filter_info.py    |  1 +
 superset/mcp_service/rls/tool/list_rls_filters.py  |  1 +
 superset/mcp_service/role/tool/get_role_info.py    |  1 +
 superset/mcp_service/role/tool/list_roles.py       |  1 +
 .../saved_query/tool/get_saved_query_info.py       |  1 +
 .../saved_query/tool/list_saved_queries.py         |  1 +
 .../tool/get_compatible_dimensions.py              |  1 +
 .../semantic_layer/tool/get_compatible_metrics.py  |  1 +
 .../mcp_service/semantic_layer/tool/get_table.py   |  1 +
 .../semantic_layer/tool/list_metrics.py            |  1 +
 superset/mcp_service/sql_lab/tool/execute_sql.py   |  2 ++
 .../sql_lab/tool/open_sql_lab_with_context.py      |  1 +
 .../mcp_service/sql_lab/tool/save_sql_query.py     |  2 ++
 superset/mcp_service/system/tool/find_users.py     |  1 +
 .../mcp_service/system/tool/generate_bug_report.py |  1 +
 .../mcp_service/system/tool/get_instance_info.py   |  1 +
 superset/mcp_service/system/tool/get_schema.py     |  1 +
 superset/mcp_service/system/tool/health_check.py   |  1 +
 superset/mcp_service/tag/tool/get_tag_info.py      |  1 +
 superset/mcp_service/tag/tool/list_tags.py         |  1 +
 superset/mcp_service/task/tool/get_task_info.py    |  1 +
 superset/mcp_service/task/tool/list_tasks.py       |  1 +
 superset/mcp_service/theme/tool/create_theme.py    |  2 ++
 superset/mcp_service/theme/tool/get_theme_info.py  |  1 +
 superset/mcp_service/theme/tool/list_themes.py     |  1 +
 superset/mcp_service/user/tool/get_user_info.py    |  1 +
 superset/mcp_service/user/tool/list_users.py       |  1 +
 tests/unit_tests/mcp_service/test_mcp_e2e_smoke.py | 12 +++++++
 .../mcp_service/test_mcp_tool_registration.py      | 42 ++++++++++++++++++++++
 73 files changed, 157 insertions(+), 3 deletions(-)

diff --git a/superset/mcp_service/CLAUDE.md b/superset/mcp_service/CLAUDE.md
index 6f79a8c6802..5e092ab1356 100644
--- a/superset/mcp_service/CLAUDE.md
+++ b/superset/mcp_service/CLAUDE.md
@@ -119,6 +119,7 @@ from superset.extensions import event_logger
         title="My new tool",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def my_new_tool(request: MyRequest, ctx: Context) -> MyResponse:
@@ -139,6 +140,8 @@ async def my_new_tool(request: MyRequest, ctx: Context) -> MyResponse:
         title="Create something",
         readOnlyHint=False,
         destructiveHint=False,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 async def create_something(request: CreateRequest, ctx: Context) -> CreateResponse:
@@ -210,8 +213,10 @@ The `@tool` decorator from `superset_core.mcp.decorators` accepts:
 ```python
 annotations=ToolAnnotations(
     title="Human-readable title",
-    readOnlyHint=True,   # Whether tool only reads data
-    destructiveHint=False, # Whether tool has destructive side effects
+    readOnlyHint=False,  # Whether tool only reads data
+    destructiveHint=False,  # Whether tool has destructive side effects
+    idempotentHint=False,  # Whether repeated mutating calls have additional effects
+    openWorldHint=False,  # Whether tool interacts with external entities
 )
 ```
 
@@ -572,7 +577,9 @@ async def test_my_tool_success(mcp_server):
 
 ### 4. Missing ToolAnnotations
 **Problem**: Tool lacks MCP directory compliance metadata.
-**Solution**: Always include `annotations=ToolAnnotations(title=..., readOnlyHint=..., destructiveHint=...)`.
+**Solution**: Always include `title`, `readOnlyHint`, `destructiveHint`, and
+`openWorldHint`. Mutating tools (`readOnlyHint=False`) must also include
+`idempotentHint`.
 
 ### 5. Using `Optional` Instead of Union Syntax
 **Problem**: Old-style `Optional[T]` is not Python 3.10+ style.
diff --git a/superset/mcp_service/annotation_layer/tool/get_annotation_layer_info.py b/superset/mcp_service/annotation_layer/tool/get_annotation_layer_info.py
index ad19260a89e..4d0502df98a 100644
--- a/superset/mcp_service/annotation_layer/tool/get_annotation_layer_info.py
+++ b/superset/mcp_service/annotation_layer/tool/get_annotation_layer_info.py
@@ -42,6 +42,7 @@ logger = logging.getLogger(__name__)
         title="Get annotation layer info",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_annotation_layer_info(
diff --git a/superset/mcp_service/annotation_layer/tool/get_layer_annotation_info.py b/superset/mcp_service/annotation_layer/tool/get_layer_annotation_info.py
index 43ba7648e03..1abbf201143 100644
--- a/superset/mcp_service/annotation_layer/tool/get_layer_annotation_info.py
+++ b/superset/mcp_service/annotation_layer/tool/get_layer_annotation_info.py
@@ -41,6 +41,7 @@ logger = logging.getLogger(__name__)
         title="Get annotation info",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_layer_annotation_info(
diff --git a/superset/mcp_service/annotation_layer/tool/list_annotation_layers.py b/superset/mcp_service/annotation_layer/tool/list_annotation_layers.py
index 1bd3050fb3d..266e90688ec 100644
--- a/superset/mcp_service/annotation_layer/tool/list_annotation_layers.py
+++ b/superset/mcp_service/annotation_layer/tool/list_annotation_layers.py
@@ -49,6 +49,7 @@ _SORTABLE_LAYER_COLUMNS = ["id", "name", "changed_on", "created_on"]
         title="List annotation layers",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def list_annotation_layers(
diff --git a/superset/mcp_service/annotation_layer/tool/list_layer_annotations.py b/superset/mcp_service/annotation_layer/tool/list_layer_annotations.py
index c355c76a783..469c03daa15 100644
--- a/superset/mcp_service/annotation_layer/tool/list_layer_annotations.py
+++ b/superset/mcp_service/annotation_layer/tool/list_layer_annotations.py
@@ -56,6 +56,7 @@ _SORTABLE_ANNOTATION_COLUMNS = ["id", "short_descr", "start_dttm", "end_dttm"]
         title="List annotations in a layer",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def list_layer_annotations(
diff --git a/superset/mcp_service/chart/tool/delete_chart.py b/superset/mcp_service/chart/tool/delete_chart.py
index df19de1012e..38692468d75 100644
--- a/superset/mcp_service/chart/tool/delete_chart.py
+++ b/superset/mcp_service/chart/tool/delete_chart.py
@@ -69,6 +69,8 @@ def _routes_to_soft_delete() -> bool:
         title="Delete chart",
         readOnlyHint=False,
         destructiveHint=True,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 async def delete_chart(
diff --git a/superset/mcp_service/chart/tool/generate_chart.py b/superset/mcp_service/chart/tool/generate_chart.py
index 5ff5da726a4..a9b063a7f93 100644
--- a/superset/mcp_service/chart/tool/generate_chart.py
+++ b/superset/mcp_service/chart/tool/generate_chart.py
@@ -71,6 +71,8 @@ __all__ = ["CompileResult", "_compile_chart", "validate_and_compile", "generate_
         title="Create chart",
         readOnlyHint=False,
         destructiveHint=False,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 async def generate_chart(  # noqa: C901
diff --git a/superset/mcp_service/chart/tool/get_chart_data.py b/superset/mcp_service/chart/tool/get_chart_data.py
index a80250b7634..fade02b8d8f 100644
--- a/superset/mcp_service/chart/tool/get_chart_data.py
+++ b/superset/mcp_service/chart/tool/get_chart_data.py
@@ -316,6 +316,7 @@ def _build_query_results(
         title="Get chart data",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_chart_data(  # noqa: C901
diff --git a/superset/mcp_service/chart/tool/get_chart_info.py b/superset/mcp_service/chart/tool/get_chart_info.py
index eb7deba390b..aae922334c8 100644
--- a/superset/mcp_service/chart/tool/get_chart_info.py
+++ b/superset/mcp_service/chart/tool/get_chart_info.py
@@ -201,6 +201,7 @@ def _apply_unsaved_state_override(result: ChartInfo, form_data_key: str) -> None
         title="Get chart info",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_chart_info(
diff --git a/superset/mcp_service/chart/tool/get_chart_preview.py b/superset/mcp_service/chart/tool/get_chart_preview.py
index 64dda21f79a..37dfb5b27b7 100644
--- a/superset/mcp_service/chart/tool/get_chart_preview.py
+++ b/superset/mcp_service/chart/tool/get_chart_preview.py
@@ -1399,6 +1399,7 @@ async def _get_chart_preview_internal(  # noqa: C901
         title="Get chart preview",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_chart_preview(
diff --git a/superset/mcp_service/chart/tool/get_chart_sql.py b/superset/mcp_service/chart/tool/get_chart_sql.py
index 6223f8d49f1..3914e2672ce 100644
--- a/superset/mcp_service/chart/tool/get_chart_sql.py
+++ b/superset/mcp_service/chart/tool/get_chart_sql.py
@@ -393,6 +393,7 @@ def _extract_sql_from_result(
         title="Get chart SQL",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_chart_sql(
diff --git a/superset/mcp_service/chart/tool/get_chart_type_schema.py b/superset/mcp_service/chart/tool/get_chart_type_schema.py
index 1823b2542e6..57caa19959a 100644
--- a/superset/mcp_service/chart/tool/get_chart_type_schema.py
+++ b/superset/mcp_service/chart/tool/get_chart_type_schema.py
@@ -228,6 +228,7 @@ def _get_chart_type_schema_impl(
         title="Get chart type schema",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 def get_chart_type_schema(
diff --git a/superset/mcp_service/chart/tool/list_charts.py b/superset/mcp_service/chart/tool/list_charts.py
index f066192c3c2..09185249ba9 100644
--- a/superset/mcp_service/chart/tool/list_charts.py
+++ b/superset/mcp_service/chart/tool/list_charts.py
@@ -71,6 +71,7 @@ _DEFAULT_LIST_CHARTS_REQUEST = ListChartsRequest()
         title="List charts",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def list_charts(
diff --git a/superset/mcp_service/chart/tool/restore_chart.py b/superset/mcp_service/chart/tool/restore_chart.py
index 221ec027e52..f935f753fd6 100644
--- a/superset/mcp_service/chart/tool/restore_chart.py
+++ b/superset/mcp_service/chart/tool/restore_chart.py
@@ -76,6 +76,8 @@ def _rollback() -> None:
         title="Restore chart",
         readOnlyHint=False,
         destructiveHint=False,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 async def restore_chart(
diff --git a/superset/mcp_service/chart/tool/update_chart.py b/superset/mcp_service/chart/tool/update_chart.py
index fd22b057a7e..00c9ea8d87e 100644
--- a/superset/mcp_service/chart/tool/update_chart.py
+++ b/superset/mcp_service/chart/tool/update_chart.py
@@ -476,6 +476,8 @@ def _create_preview_url(
         title="Update chart",
         readOnlyHint=False,
         destructiveHint=True,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 async def update_chart(  # noqa: C901
diff --git a/superset/mcp_service/chart/tool/update_chart_preview.py b/superset/mcp_service/chart/tool/update_chart_preview.py
index 9ba3b59e224..b2910567add 100644
--- a/superset/mcp_service/chart/tool/update_chart_preview.py
+++ b/superset/mcp_service/chart/tool/update_chart_preview.py
@@ -157,6 +157,8 @@ def _preserve_previous_adhoc_filters(
         title="Update chart preview",
         readOnlyHint=False,
         destructiveHint=False,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 def update_chart_preview(  # noqa: C901
diff --git a/superset/mcp_service/dashboard/tool/add_chart_to_existing_dashboard.py b/superset/mcp_service/dashboard/tool/add_chart_to_existing_dashboard.py
index 107b81e7d15..e68ca63589f 100644
--- a/superset/mcp_service/dashboard/tool/add_chart_to_existing_dashboard.py
+++ b/superset/mcp_service/dashboard/tool/add_chart_to_existing_dashboard.py
@@ -427,6 +427,8 @@ def _find_and_authorize_dashboard(
         title="Add chart to dashboard",
         readOnlyHint=False,
         destructiveHint=False,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 def add_chart_to_existing_dashboard(  # noqa: C901 — complexity is structural (layout traversal + multi-step authorization), not accidental
diff --git a/superset/mcp_service/dashboard/tool/delete_dashboard.py b/superset/mcp_service/dashboard/tool/delete_dashboard.py
index 182250d88c3..b078ebe7ffa 100644
--- a/superset/mcp_service/dashboard/tool/delete_dashboard.py
+++ b/superset/mcp_service/dashboard/tool/delete_dashboard.py
@@ -93,6 +93,8 @@ def _routes_to_soft_delete() -> bool:
         title="Delete dashboard",
         readOnlyHint=False,
         destructiveHint=True,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 async def delete_dashboard(
diff --git a/superset/mcp_service/dashboard/tool/duplicate_dashboard.py b/superset/mcp_service/dashboard/tool/duplicate_dashboard.py
index 145901c4c45..eb3bea8b9a8 100644
--- a/superset/mcp_service/dashboard/tool/duplicate_dashboard.py
+++ b/superset/mcp_service/dashboard/tool/duplicate_dashboard.py
@@ -276,6 +276,8 @@ async def _resolve_source(
         title="Duplicate dashboard",
         readOnlyHint=False,
         destructiveHint=False,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 async def duplicate_dashboard(
diff --git a/superset/mcp_service/dashboard/tool/generate_dashboard.py b/superset/mcp_service/dashboard/tool/generate_dashboard.py
index 71319199279..d55b70affc3 100644
--- a/superset/mcp_service/dashboard/tool/generate_dashboard.py
+++ b/superset/mcp_service/dashboard/tool/generate_dashboard.py
@@ -191,6 +191,8 @@ def _generate_title_from_charts(chart_objects: List[Any]) -> str:
         title="Create dashboard",
         readOnlyHint=False,
         destructiveHint=False,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 def generate_dashboard(  # noqa: C901
diff --git a/superset/mcp_service/dashboard/tool/get_dashboard_datasets.py b/superset/mcp_service/dashboard/tool/get_dashboard_datasets.py
index 293b8b683e7..7741e8e7455 100644
--- a/superset/mcp_service/dashboard/tool/get_dashboard_datasets.py
+++ b/superset/mcp_service/dashboard/tool/get_dashboard_datasets.py
@@ -54,6 +54,7 @@ logger = logging.getLogger(__name__)
         title="Get dashboard datasets",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 @requires_data_model_metadata_access
diff --git a/superset/mcp_service/dashboard/tool/get_dashboard_info.py b/superset/mcp_service/dashboard/tool/get_dashboard_info.py
index 2cedcbe5d81..a53718cf18e 100644
--- a/superset/mcp_service/dashboard/tool/get_dashboard_info.py
+++ b/superset/mcp_service/dashboard/tool/get_dashboard_info.py
@@ -96,6 +96,7 @@ def _lookup_dashboard(
         title="Get dashboard info",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_dashboard_info(
diff --git a/superset/mcp_service/dashboard/tool/get_dashboard_layout.py b/superset/mcp_service/dashboard/tool/get_dashboard_layout.py
index 60b855500f2..facfcad3a76 100644
--- a/superset/mcp_service/dashboard/tool/get_dashboard_layout.py
+++ b/superset/mcp_service/dashboard/tool/get_dashboard_layout.py
@@ -53,6 +53,7 @@ logger = logging.getLogger(__name__)
         title="Get dashboard layout",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_dashboard_layout(
diff --git a/superset/mcp_service/dashboard/tool/list_dashboards.py b/superset/mcp_service/dashboard/tool/list_dashboards.py
index 84426f54c30..b9c2a2b31e9 100644
--- a/superset/mcp_service/dashboard/tool/list_dashboards.py
+++ b/superset/mcp_service/dashboard/tool/list_dashboards.py
@@ -66,6 +66,7 @@ _DEFAULT_LIST_DASHBOARDS_REQUEST = ListDashboardsRequest()
         title="List dashboards",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def list_dashboards(
diff --git a/superset/mcp_service/dashboard/tool/manage_dashboard_certification.py b/superset/mcp_service/dashboard/tool/manage_dashboard_certification.py
index 1737b9eee81..0c7ebedc6bd 100644
--- a/superset/mcp_service/dashboard/tool/manage_dashboard_certification.py
+++ b/superset/mcp_service/dashboard/tool/manage_dashboard_certification.py
@@ -51,6 +51,8 @@ logger: logging.Logger = logging.getLogger(__name__)
         title="Manage dashboard certification",
         readOnlyHint=False,
         destructiveHint=False,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 def manage_dashboard_certification(
diff --git a/superset/mcp_service/dashboard/tool/manage_dashboard_owners.py b/superset/mcp_service/dashboard/tool/manage_dashboard_owners.py
index c231da7a9de..e304105c53b 100644
--- a/superset/mcp_service/dashboard/tool/manage_dashboard_owners.py
+++ b/superset/mcp_service/dashboard/tool/manage_dashboard_owners.py
@@ -248,6 +248,8 @@ def _build_owner_warnings(
         title="Manage dashboard owners",
         readOnlyHint=False,
         destructiveHint=True,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 def manage_dashboard_owners(
diff --git a/superset/mcp_service/dashboard/tool/manage_dashboard_roles.py b/superset/mcp_service/dashboard/tool/manage_dashboard_roles.py
index 6d565fa82bf..0ae010c7497 100644
--- a/superset/mcp_service/dashboard/tool/manage_dashboard_roles.py
+++ b/superset/mcp_service/dashboard/tool/manage_dashboard_roles.py
@@ -160,6 +160,8 @@ def _resolve_role_subjects(
         title="Manage dashboard access roles",
         readOnlyHint=False,
         destructiveHint=True,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 def manage_dashboard_roles(
diff --git a/superset/mcp_service/dashboard/tool/manage_native_filters.py b/superset/mcp_service/dashboard/tool/manage_native_filters.py
index 8a7c01a8d43..709f5d30610 100644
--- a/superset/mcp_service/dashboard/tool/manage_native_filters.py
+++ b/superset/mcp_service/dashboard/tool/manage_native_filters.py
@@ -376,6 +376,8 @@ def _build_native_filters_payload(  # noqa: C901
         title="Manage dashboard native filters",
         readOnlyHint=False,
         destructiveHint=True,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 def manage_native_filters(
diff --git a/superset/mcp_service/dashboard/tool/remove_chart_from_dashboard.py b/superset/mcp_service/dashboard/tool/remove_chart_from_dashboard.py
index 1ad1f5a2c60..863ef45cfa1 100644
--- a/superset/mcp_service/dashboard/tool/remove_chart_from_dashboard.py
+++ b/superset/mcp_service/dashboard/tool/remove_chart_from_dashboard.py
@@ -278,6 +278,8 @@ def _find_and_authorize_dashboard(
         title="Remove chart from dashboard",
         readOnlyHint=False,
         destructiveHint=True,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 def remove_chart_from_dashboard(  # noqa: C901 — complexity is structural (layout traversal + multi-step authorization), not accidental
diff --git a/superset/mcp_service/dashboard/tool/restore_dashboard.py b/superset/mcp_service/dashboard/tool/restore_dashboard.py
index d28b0267a9d..4fabbfc7c9b 100644
--- a/superset/mcp_service/dashboard/tool/restore_dashboard.py
+++ b/superset/mcp_service/dashboard/tool/restore_dashboard.py
@@ -78,6 +78,8 @@ def _rollback() -> None:
         title="Restore dashboard",
         readOnlyHint=False,
         destructiveHint=False,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 async def restore_dashboard(
diff --git a/superset/mcp_service/dashboard/tool/update_dashboard.py b/superset/mcp_service/dashboard/tool/update_dashboard.py
index b468573dc83..2045936edbd 100644
--- a/superset/mcp_service/dashboard/tool/update_dashboard.py
+++ b/superset/mcp_service/dashboard/tool/update_dashboard.py
@@ -291,6 +291,8 @@ def _validate_update_request(
         title="Update dashboard layout/theme/CSS/metadata",
         readOnlyHint=False,
         destructiveHint=True,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 async def update_dashboard(
diff --git a/superset/mcp_service/database/tool/get_database_info.py b/superset/mcp_service/database/tool/get_database_info.py
index c91b4306ad8..c23f4dab6e8 100644
--- a/superset/mcp_service/database/tool/get_database_info.py
+++ b/superset/mcp_service/database/tool/get_database_info.py
@@ -52,6 +52,7 @@ logger = logging.getLogger(__name__)
         title="Get database info",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 @requires_data_model_metadata_access
diff --git a/superset/mcp_service/database/tool/list_databases.py b/superset/mcp_service/database/tool/list_databases.py
index b03949146ed..fc2fca6d908 100644
--- a/superset/mcp_service/database/tool/list_databases.py
+++ b/superset/mcp_service/database/tool/list_databases.py
@@ -59,6 +59,7 @@ _DEFAULT_LIST_DATABASES_REQUEST = ListDatabasesRequest()
         title="List databases",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 @requires_data_model_metadata_access
diff --git a/superset/mcp_service/dataset/tool/create_dataset.py b/superset/mcp_service/dataset/tool/create_dataset.py
index 16dcd682579..9a3a903973a 100644
--- a/superset/mcp_service/dataset/tool/create_dataset.py
+++ b/superset/mcp_service/dataset/tool/create_dataset.py
@@ -119,6 +119,8 @@ def _build_dataset_properties(
         title="Register physical table as dataset",
         readOnlyHint=False,
         destructiveHint=False,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 async def create_dataset(
diff --git a/superset/mcp_service/dataset/tool/create_virtual_dataset.py b/superset/mcp_service/dataset/tool/create_virtual_dataset.py
index f16e31b0f00..f231a019258 100644
--- a/superset/mcp_service/dataset/tool/create_virtual_dataset.py
+++ b/superset/mcp_service/dataset/tool/create_virtual_dataset.py
@@ -91,6 +91,8 @@ def _update_virtual_dataset(dataset_id: int, update_props: dict[str, Any]) -> An
         title="Create virtual dataset from SQL",
         readOnlyHint=False,
         destructiveHint=False,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 async def create_virtual_dataset(  # noqa: C901
diff --git a/superset/mcp_service/dataset/tool/get_dataset_info.py b/superset/mcp_service/dataset/tool/get_dataset_info.py
index f5cde391f26..d9ee6ba4814 100644
--- a/superset/mcp_service/dataset/tool/get_dataset_info.py
+++ b/superset/mcp_service/dataset/tool/get_dataset_info.py
@@ -54,6 +54,7 @@ logger = logging.getLogger(__name__)
         title="Get dataset info",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 @requires_data_model_metadata_access
diff --git a/superset/mcp_service/dataset/tool/list_datasets.py b/superset/mcp_service/dataset/tool/list_datasets.py
index 7422823b463..b0e3a73f83f 100644
--- a/superset/mcp_service/dataset/tool/list_datasets.py
+++ b/superset/mcp_service/dataset/tool/list_datasets.py
@@ -76,6 +76,7 @@ _DEFAULT_LIST_DATASETS_REQUEST = ListDatasetsRequest()
         title="List datasets",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 @requires_data_model_metadata_access
diff --git a/superset/mcp_service/dataset/tool/query_dataset.py b/superset/mcp_service/dataset/tool/query_dataset.py
index d306242696b..eb843a07e24 100644
--- a/superset/mcp_service/dataset/tool/query_dataset.py
+++ b/superset/mcp_service/dataset/tool/query_dataset.py
@@ -70,6 +70,7 @@ _NO_SAVED_METRICS_HINT = (
         title="Query dataset",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 @requires_data_model_metadata_access
diff --git a/superset/mcp_service/dataset/tool/update_dataset_metric.py b/superset/mcp_service/dataset/tool/update_dataset_metric.py
index 3b6ce766898..09254fd4062 100644
--- a/superset/mcp_service/dataset/tool/update_dataset_metric.py
+++ b/superset/mcp_service/dataset/tool/update_dataset_metric.py
@@ -102,6 +102,8 @@ def _serialize_metric(metric: Any) -> DatasetMetricDetail:
         # Overwrites an existing metric's definition and affects every chart
         # that uses it — non-additive, like update_chart.
         destructiveHint=True,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 async def update_dataset_metric(  # noqa: C901
diff --git a/superset/mcp_service/explore/tool/generate_explore_link.py b/superset/mcp_service/explore/tool/generate_explore_link.py
index 383a174c68c..178c95ea8ae 100644
--- a/superset/mcp_service/explore/tool/generate_explore_link.py
+++ b/superset/mcp_service/explore/tool/generate_explore_link.py
@@ -58,6 +58,8 @@ logger = logging.getLogger(__name__)
         title="Generate explore link",
         readOnlyHint=False,
         destructiveHint=False,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 async def generate_explore_link(
diff --git a/superset/mcp_service/query/tool/get_query_info.py b/superset/mcp_service/query/tool/get_query_info.py
index f26dac2349a..8b733328dee 100644
--- a/superset/mcp_service/query/tool/get_query_info.py
+++ b/superset/mcp_service/query/tool/get_query_info.py
@@ -47,6 +47,7 @@ logger = logging.getLogger(__name__)
         title="Get query info",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_query_info(
diff --git a/superset/mcp_service/query/tool/list_queries.py b/superset/mcp_service/query/tool/list_queries.py
index 64a97e0cb45..caafd78fbc6 100644
--- a/superset/mcp_service/query/tool/list_queries.py
+++ b/superset/mcp_service/query/tool/list_queries.py
@@ -52,6 +52,7 @@ _DEFAULT_LIST_QUERIES_REQUEST = ListQueriesRequest()
         title="List queries",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def list_queries(
diff --git a/superset/mcp_service/report/tool/get_report_info.py b/superset/mcp_service/report/tool/get_report_info.py
index e6ffef35b39..7ce3322cacc 100644
--- a/superset/mcp_service/report/tool/get_report_info.py
+++ b/superset/mcp_service/report/tool/get_report_info.py
@@ -44,6 +44,7 @@ logger = logging.getLogger(__name__)
         title="Get report info",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_report_info(
diff --git a/superset/mcp_service/report/tool/list_reports.py b/superset/mcp_service/report/tool/list_reports.py
index 1f8e58c5f43..7f5e4ddfec9 100644
--- a/superset/mcp_service/report/tool/list_reports.py
+++ b/superset/mcp_service/report/tool/list_reports.py
@@ -58,6 +58,7 @@ _DEFAULT_LIST_REPORTS_REQUEST = ListReportsRequest()
         title="List reports",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def list_reports(
diff --git a/superset/mcp_service/rls/tool/get_rls_filter_info.py b/superset/mcp_service/rls/tool/get_rls_filter_info.py
index 86a6ee718f7..1efa5ec5f49 100644
--- a/superset/mcp_service/rls/tool/get_rls_filter_info.py
+++ b/superset/mcp_service/rls/tool/get_rls_filter_info.py
@@ -44,6 +44,7 @@ logger = logging.getLogger(__name__)
         title="Get RLS filter info",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_rls_filter_info(
diff --git a/superset/mcp_service/rls/tool/list_rls_filters.py b/superset/mcp_service/rls/tool/list_rls_filters.py
index 0dd972994f7..589787928a1 100644
--- a/superset/mcp_service/rls/tool/list_rls_filters.py
+++ b/superset/mcp_service/rls/tool/list_rls_filters.py
@@ -50,6 +50,7 @@ _DEFAULT_LIST_RLS_FILTERS_REQUEST = ListRlsFiltersRequest()
         title="List RLS filters",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def list_rls_filters(
diff --git a/superset/mcp_service/role/tool/get_role_info.py b/superset/mcp_service/role/tool/get_role_info.py
index d962c0a9116..3f67d4ace7b 100644
--- a/superset/mcp_service/role/tool/get_role_info.py
+++ b/superset/mcp_service/role/tool/get_role_info.py
@@ -45,6 +45,7 @@ logger = logging.getLogger(__name__)
         title="Get role info",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_role_info(
diff --git a/superset/mcp_service/role/tool/list_roles.py b/superset/mcp_service/role/tool/list_roles.py
index 4f7482ed09b..4ce66e7154c 100644
--- a/superset/mcp_service/role/tool/list_roles.py
+++ b/superset/mcp_service/role/tool/list_roles.py
@@ -53,6 +53,7 @@ _DEFAULT_LIST_ROLES_REQUEST = ListRolesRequest()
         title="List roles",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def list_roles(
diff --git a/superset/mcp_service/saved_query/tool/get_saved_query_info.py b/superset/mcp_service/saved_query/tool/get_saved_query_info.py
index f1a4f19b3f1..5a4c9cd2773 100644
--- a/superset/mcp_service/saved_query/tool/get_saved_query_info.py
+++ b/superset/mcp_service/saved_query/tool/get_saved_query_info.py
@@ -47,6 +47,7 @@ logger = logging.getLogger(__name__)
         title="Get saved query info",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_saved_query_info(
diff --git a/superset/mcp_service/saved_query/tool/list_saved_queries.py b/superset/mcp_service/saved_query/tool/list_saved_queries.py
index d4f09bb60fc..42b6a8ca279 100644
--- a/superset/mcp_service/saved_query/tool/list_saved_queries.py
+++ b/superset/mcp_service/saved_query/tool/list_saved_queries.py
@@ -52,6 +52,7 @@ _DEFAULT_LIST_SAVED_QUERIES_REQUEST = ListSavedQueriesRequest()
         title="List saved queries",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def list_saved_queries(
diff --git a/superset/mcp_service/semantic_layer/tool/get_compatible_dimensions.py b/superset/mcp_service/semantic_layer/tool/get_compatible_dimensions.py
index 49149560db3..0b2b8bd28d9 100644
--- a/superset/mcp_service/semantic_layer/tool/get_compatible_dimensions.py
+++ b/superset/mcp_service/semantic_layer/tool/get_compatible_dimensions.py
@@ -49,6 +49,7 @@ logger = logging.getLogger(__name__)
         title="Get compatible dimensions",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 @requires_data_model_metadata_access
diff --git a/superset/mcp_service/semantic_layer/tool/get_compatible_metrics.py b/superset/mcp_service/semantic_layer/tool/get_compatible_metrics.py
index 55117c2a5c3..87a36131fe8 100644
--- a/superset/mcp_service/semantic_layer/tool/get_compatible_metrics.py
+++ b/superset/mcp_service/semantic_layer/tool/get_compatible_metrics.py
@@ -49,6 +49,7 @@ logger = logging.getLogger(__name__)
         title="Get compatible metrics",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 @requires_data_model_metadata_access
diff --git a/superset/mcp_service/semantic_layer/tool/get_table.py b/superset/mcp_service/semantic_layer/tool/get_table.py
index 7306e37e86e..39078b1116a 100644
--- a/superset/mcp_service/semantic_layer/tool/get_table.py
+++ b/superset/mcp_service/semantic_layer/tool/get_table.py
@@ -416,6 +416,7 @@ def _validate_datasource_selection(
         title="Get table",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 @requires_data_model_metadata_access
diff --git a/superset/mcp_service/semantic_layer/tool/list_metrics.py b/superset/mcp_service/semantic_layer/tool/list_metrics.py
index 54dd1669893..1b060c27deb 100644
--- a/superset/mcp_service/semantic_layer/tool/list_metrics.py
+++ b/superset/mcp_service/semantic_layer/tool/list_metrics.py
@@ -236,6 +236,7 @@ async def _collect_external_metrics(
         title="List metrics",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 @requires_data_model_metadata_access
diff --git a/superset/mcp_service/sql_lab/tool/execute_sql.py b/superset/mcp_service/sql_lab/tool/execute_sql.py
index 6ed79c87eaa..e79fbe8e930 100644
--- a/superset/mcp_service/sql_lab/tool/execute_sql.py
+++ b/superset/mcp_service/sql_lab/tool/execute_sql.py
@@ -114,6 +114,8 @@ async def _validate_non_destructive_sql(
         title="Execute SQL query",
         readOnlyHint=False,
         destructiveHint=True,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 async def execute_sql(request: ExecuteSqlRequest, ctx: Context) -> ExecuteSqlResponse:
diff --git a/superset/mcp_service/sql_lab/tool/open_sql_lab_with_context.py b/superset/mcp_service/sql_lab/tool/open_sql_lab_with_context.py
index 76add8f6a5d..78b5907df00 100644
--- a/superset/mcp_service/sql_lab/tool/open_sql_lab_with_context.py
+++ b/superset/mcp_service/sql_lab/tool/open_sql_lab_with_context.py
@@ -45,6 +45,7 @@ logger = logging.getLogger(__name__)
         title="Open SQL Lab with context",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 def open_sql_lab_with_context(
diff --git a/superset/mcp_service/sql_lab/tool/save_sql_query.py b/superset/mcp_service/sql_lab/tool/save_sql_query.py
index 8126bf19efb..56a534164c7 100644
--- a/superset/mcp_service/sql_lab/tool/save_sql_query.py
+++ b/superset/mcp_service/sql_lab/tool/save_sql_query.py
@@ -48,6 +48,8 @@ logger = logging.getLogger(__name__)
         title="Save SQL query",
         readOnlyHint=False,
         destructiveHint=False,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 async def save_sql_query(
diff --git a/superset/mcp_service/system/tool/find_users.py b/superset/mcp_service/system/tool/find_users.py
index 6e46585a48b..e56c8136e78 100755
--- a/superset/mcp_service/system/tool/find_users.py
+++ b/superset/mcp_service/system/tool/find_users.py
@@ -40,6 +40,7 @@ logger = logging.getLogger(__name__)
         title="Find users",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def find_users(request: FindUsersRequest, ctx: Context) -> FindUsersResponse:
diff --git a/superset/mcp_service/system/tool/generate_bug_report.py b/superset/mcp_service/system/tool/generate_bug_report.py
index 0a4cff79a94..baf3fa2a8a6 100644
--- a/superset/mcp_service/system/tool/generate_bug_report.py
+++ b/superset/mcp_service/system/tool/generate_bug_report.py
@@ -261,6 +261,7 @@ def _format_report(
         title="Generate bug report",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def generate_bug_report(
diff --git a/superset/mcp_service/system/tool/get_instance_info.py b/superset/mcp_service/system/tool/get_instance_info.py
index 2aaf4692ebd..bf6ff243036 100644
--- a/superset/mcp_service/system/tool/get_instance_info.py
+++ b/superset/mcp_service/system/tool/get_instance_info.py
@@ -95,6 +95,7 @@ def _redact_data_model_metadata(result: InstanceInfo) -> InstanceInfo:
         title="Get instance info",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 def get_instance_info(
diff --git a/superset/mcp_service/system/tool/get_schema.py b/superset/mcp_service/system/tool/get_schema.py
index ca3cff2ad83..d80b64478d8 100644
--- a/superset/mcp_service/system/tool/get_schema.py
+++ b/superset/mcp_service/system/tool/get_schema.py
@@ -201,6 +201,7 @@ _MODEL_TYPE_CLASS_PERMISSION: dict[ModelType, str] = {
         title="Get schema",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_schema(
diff --git a/superset/mcp_service/system/tool/health_check.py b/superset/mcp_service/system/tool/health_check.py
index e41f673ac8b..35145038cd8 100644
--- a/superset/mcp_service/system/tool/health_check.py
+++ b/superset/mcp_service/system/tool/health_check.py
@@ -40,6 +40,7 @@ _start_time = time.monotonic()
         title="Health check",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def health_check() -> HealthCheckResponse:
diff --git a/superset/mcp_service/tag/tool/get_tag_info.py b/superset/mcp_service/tag/tool/get_tag_info.py
index ab4cde6a929..547f52270de 100644
--- a/superset/mcp_service/tag/tool/get_tag_info.py
+++ b/superset/mcp_service/tag/tool/get_tag_info.py
@@ -47,6 +47,7 @@ logger = logging.getLogger(__name__)
         title="Get tag info",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_tag_info(request: GetTagInfoRequest, ctx: Context) -> TagInfo | TagError:
diff --git a/superset/mcp_service/tag/tool/list_tags.py b/superset/mcp_service/tag/tool/list_tags.py
index f248dad6c3e..444d19482a3 100644
--- a/superset/mcp_service/tag/tool/list_tags.py
+++ b/superset/mcp_service/tag/tool/list_tags.py
@@ -63,6 +63,7 @@ _DEFAULT_LIST_TAGS_REQUEST = ListTagsRequest()
         title="List tags",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def list_tags(
diff --git a/superset/mcp_service/task/tool/get_task_info.py b/superset/mcp_service/task/tool/get_task_info.py
index 6927eebeb07..a051bfe4833 100644
--- a/superset/mcp_service/task/tool/get_task_info.py
+++ b/superset/mcp_service/task/tool/get_task_info.py
@@ -43,6 +43,7 @@ logger = logging.getLogger(__name__)
         title="Get task info",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_task_info(
diff --git a/superset/mcp_service/task/tool/list_tasks.py b/superset/mcp_service/task/tool/list_tasks.py
index 609dcf9becc..607e14b2281 100644
--- a/superset/mcp_service/task/tool/list_tasks.py
+++ b/superset/mcp_service/task/tool/list_tasks.py
@@ -49,6 +49,7 @@ _DEFAULT_LIST_TASKS_REQUEST = ListTasksRequest()
         title="List tasks",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def list_tasks(
diff --git a/superset/mcp_service/theme/tool/create_theme.py b/superset/mcp_service/theme/tool/create_theme.py
index 4a32911e34d..25b41cc5734 100644
--- a/superset/mcp_service/theme/tool/create_theme.py
+++ b/superset/mcp_service/theme/tool/create_theme.py
@@ -47,6 +47,8 @@ logger = logging.getLogger(__name__)
         title="Create theme",
         readOnlyHint=False,
         destructiveHint=False,
+        idempotentHint=False,
+        openWorldHint=False,
     ),
 )
 async def create_theme(
diff --git a/superset/mcp_service/theme/tool/get_theme_info.py b/superset/mcp_service/theme/tool/get_theme_info.py
index 9af4ca19c8f..b449f00b6a2 100644
--- a/superset/mcp_service/theme/tool/get_theme_info.py
+++ b/superset/mcp_service/theme/tool/get_theme_info.py
@@ -47,6 +47,7 @@ logger = logging.getLogger(__name__)
         title="Get theme info",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_theme_info(
diff --git a/superset/mcp_service/theme/tool/list_themes.py b/superset/mcp_service/theme/tool/list_themes.py
index 362d5c7e740..5782ffd04e7 100644
--- a/superset/mcp_service/theme/tool/list_themes.py
+++ b/superset/mcp_service/theme/tool/list_themes.py
@@ -71,6 +71,7 @@ _DEFAULT_LIST_THEMES_REQUEST = ListThemesRequest()
         title="List themes",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def list_themes(
diff --git a/superset/mcp_service/user/tool/get_user_info.py b/superset/mcp_service/user/tool/get_user_info.py
index 156db4c80b5..bedf2c517b4 100644
--- a/superset/mcp_service/user/tool/get_user_info.py
+++ b/superset/mcp_service/user/tool/get_user_info.py
@@ -46,6 +46,7 @@ logger = logging.getLogger(__name__)
         title="Get user info",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def get_user_info(
diff --git a/superset/mcp_service/user/tool/list_users.py b/superset/mcp_service/user/tool/list_users.py
index f9d658a88b3..66466426092 100644
--- a/superset/mcp_service/user/tool/list_users.py
+++ b/superset/mcp_service/user/tool/list_users.py
@@ -54,6 +54,7 @@ _DEFAULT_LIST_USERS_REQUEST = ListUsersRequest()
         title="List users",
         readOnlyHint=True,
         destructiveHint=False,
+        openWorldHint=False,
     ),
 )
 async def list_users(
diff --git a/tests/unit_tests/mcp_service/test_mcp_e2e_smoke.py b/tests/unit_tests/mcp_service/test_mcp_e2e_smoke.py
index a2930ed4032..dde4cb37be9 100644
--- a/tests/unit_tests/mcp_service/test_mcp_e2e_smoke.py
+++ b/tests/unit_tests/mcp_service/test_mcp_e2e_smoke.py
@@ -212,6 +212,18 @@ async def test_tools_list_over_real_asgi_transport() -> None:
     assert len(tools) > 0
     tool_names = {tool.name for tool in tools}
     assert "health_check" in tool_names
+    assert not {
+        tool.name
+        for tool in tools
+        if tool.annotations is None or tool.annotations.openWorldHint is not False
+    }
+    assert not {
+        tool.name
+        for tool in tools
+        if tool.annotations is not None
+        and tool.annotations.readOnlyHint is False
+        and tool.annotations.idempotentHint is not False
+    }
 
 
 @pytest.mark.asyncio
diff --git a/tests/unit_tests/mcp_service/test_mcp_tool_registration.py b/tests/unit_tests/mcp_service/test_mcp_tool_registration.py
index 3ebccf3f194..46db53a8e13 100644
--- a/tests/unit_tests/mcp_service/test_mcp_tool_registration.py
+++ b/tests/unit_tests/mcp_service/test_mcp_tool_registration.py
@@ -57,6 +57,48 @@ def test_mcp_app_imports_successfully():
     assert "list_charts" in tool_names
 
 
+def test_all_registered_tools_have_complete_annotations():
+    """Every advertised tool declares all MCP safety annotations."""
+    required_fields = (
+        "title",
+        "readOnlyHint",
+        "destructiveHint",
+        "openWorldHint",
+    )
+    missing_by_tool = {}
+    open_world_tools = []
+    idempotent_mutating_tools = []
+
+    for registered_tool in _run(mcp.list_tools()):
+        annotations = registered_tool.annotations
+        missing = [
+            field
+            for field in required_fields
+            if annotations is None or getattr(annotations, field, None) is None
+        ]
+        if annotations is not None and annotations.readOnlyHint is False:
+            if annotations.idempotentHint is None:
+                missing.append("idempotentHint")
+            elif annotations.idempotentHint is not False:
+                idempotent_mutating_tools.append(registered_tool.name)
+        if missing:
+            missing_by_tool[registered_tool.name] = missing
+        elif annotations.openWorldHint is not False:
+            open_world_tools.append(registered_tool.name)
+
+    assert not missing_by_tool, (
+        f"Registered MCP tools have incomplete annotations: {missing_by_tool}"
+    )
+    assert not open_world_tools, (
+        "Tools must be explicitly closed-world (openWorldHint=False): "
+        f"{open_world_tools}"
+    )
+    assert not idempotent_mutating_tools, (
+        "Mutating tools must not claim idempotency (idempotentHint=False): "
+        f"{idempotent_mutating_tools}"
+    )
+
+
 def test_mcp_prompts_registered():
     """Test that MCP prompts are registered."""
     prompts = _run(mcp.list_prompts())