Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/cb_mcp/tool_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import logging
from collections.abc import Callable

from .tools import KV_WRITE_TOOLS, get_tools
from .tools import ADMIN_WRITE_TOOLS, KV_WRITE_TOOLS, get_tools
from .utils.config import parse_tool_names
from .utils.constants import MCP_SERVER_NAME
from .utils.elicitation import wrap_with_confirmation
Expand All @@ -23,6 +23,7 @@ def prepare_tools_for_registration(
disabled_tools: str | None,
confirmation_required_tools: str | None,
enforce_scopes: bool = False,
admin_write_mode: bool = False,
) -> tuple[list[Callable], set[str], set[str]]:
"""Prepare final tool list and confirmation configuration for registration.

Expand All @@ -36,8 +37,9 @@ def prepare_tools_for_registration(
(stdio / unauthenticated), so ``enforce_scopes`` only affects whether
the wrapper is installed — not whether it does work per call.
"""
# When read_only_mode is True, KV write tools are not loaded.
tools = get_tools(read_only_mode=read_only_mode)
# When read_only_mode is True, KV write tools are not loaded. Admin write
# tools (index DDL, GSI settings) additionally require admin_write_mode.
tools = get_tools(read_only_mode=read_only_mode, admin_write_mode=admin_write_mode)

loaded_tool_names = {tool.__name__ for tool in tools}
disabled_tool_names = parse_tool_names(disabled_tools, loaded_tool_names)
Expand Down Expand Up @@ -74,7 +76,7 @@ def prepare_tools_for_registration(
f"{sorted(skipped_confirmation_tool_names)}"
)

write_tool_names = {fn.__name__ for fn in KV_WRITE_TOOLS}
write_tool_names = {fn.__name__ for fn in (*KV_WRITE_TOOLS, *ADMIN_WRITE_TOOLS)}

final_tools: list[Callable] = []
for tool in enabled_tools:
Expand Down
84 changes: 81 additions & 3 deletions src/cb_mcp/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,28 @@

from mcp.types import ToolAnnotations

# Scope and collection management tools (write) + get_collection_settings (read)
from .collections_admin import (
create_collection,
create_scope,
drop_collection,
drop_scope,
get_collection_settings,
update_collection,
)

# Index tools
from .index import get_index_advisor_recommendations, list_indexes

# Index management tools (DDL + GSI settings, write)
from .index_admin import (
admin_index_settings_get,
admin_index_settings_set,
build_deferred_indexes,
create_index,
drop_index,
)

# Key-Value tools
from .kv import (
delete_document_by_id,
Expand Down Expand Up @@ -68,6 +87,10 @@
# Index tools
get_index_advisor_recommendations,
list_indexes,
# Index settings (read)
admin_index_settings_get,
# Collection settings (read)
get_collection_settings,
# Query performance analysis tools
get_queries_not_selective,
get_queries_not_using_covering_index,
Expand All @@ -86,6 +109,22 @@
delete_document_by_id,
]

# Admin write tools - loaded only when read_only_mode is False AND
# admin_write_mode is True. These mutate cluster structure (index DDL) or
# cluster-wide index settings, a strictly higher privilege than data writes,
# so they gate behind a second, independent flag.
ADMIN_WRITE_TOOLS = [
create_index,
drop_index,
build_deferred_indexes,
admin_index_settings_set,
create_scope,
drop_scope,
create_collection,
drop_collection,
update_collection,
]

# List of all tools for easy registration (kept for backward compatibility)
ALL_TOOLS = READ_ONLY_TOOLS + KV_WRITE_TOOLS

Expand Down Expand Up @@ -121,20 +160,47 @@
"insert_document_by_id": ToolAnnotations(idempotentHint=True),
"replace_document_by_id": ToolAnnotations(idempotentHint=True),
"delete_document_by_id": ToolAnnotations(destructiveHint=True, idempotentHint=True),
# Index management tools (write)
"create_index": ToolAnnotations(idempotentHint=False),
"drop_index": ToolAnnotations(destructiveHint=True, idempotentHint=True),
"build_deferred_indexes": ToolAnnotations(idempotentHint=True),
# Index settings
"admin_index_settings_get": ToolAnnotations(readOnlyHint=True),
"admin_index_settings_set": ToolAnnotations(
destructiveHint=False, idempotentHint=True
),
# Scope/collection management (write)
"create_scope": ToolAnnotations(idempotentHint=False),
"drop_scope": ToolAnnotations(destructiveHint=True, idempotentHint=True),
"create_collection": ToolAnnotations(idempotentHint=False),
"drop_collection": ToolAnnotations(destructiveHint=True, idempotentHint=True),
"update_collection": ToolAnnotations(destructiveHint=False, idempotentHint=True),
# Collection settings (read)
"get_collection_settings": ToolAnnotations(readOnlyHint=True),
}


def get_tools(read_only_mode: bool = True) -> list[Callable]:
def get_tools(
read_only_mode: bool = True,
admin_write_mode: bool = False,
) -> list[Callable]:
"""Get the list of tools based on the mode settings.

This function determines which tools should be loaded based on the
READ_ONLY_MODE setting. When read_only_mode is True, write tools are excluded.
- READ_ONLY_TOOLS are always loaded.
- KV_WRITE_TOOLS load when read_only_mode is False.
- ADMIN_WRITE_TOOLS load only when read_only_mode is False AND
admin_write_mode is True. Cluster-structure mutation (index DDL) and
cluster-wide index settings are a higher privilege than data writes, so
disabling read-only alone does not expose them; an operator must also
opt in to admin writes.
"""
tools = list(READ_ONLY_TOOLS)

if not read_only_mode:
# KV write tools are only loaded when READ_ONLY_MODE is False
tools.extend(KV_WRITE_TOOLS)
if admin_write_mode:
tools.extend(ADMIN_WRITE_TOOLS)

return tools

Expand All @@ -157,6 +223,17 @@ def get_tools(read_only_mode: bool = True) -> list[Callable]:
"explain_sql_plus_plus_query",
"get_index_advisor_recommendations",
"list_indexes",
"create_index",
"drop_index",
"build_deferred_indexes",
"admin_index_settings_get",
"admin_index_settings_set",
"create_scope",
"drop_scope",
"create_collection",
"drop_collection",
"update_collection",
"get_collection_settings",
"get_cluster_health_and_services",
"get_queries_not_selective",
"get_queries_not_using_covering_index",
Expand All @@ -168,6 +245,7 @@ def get_tools(read_only_mode: bool = True) -> list[Callable]:
# Tool categories
"READ_ONLY_TOOLS",
"KV_WRITE_TOOLS",
"ADMIN_WRITE_TOOLS",
# Tool annotations
"TOOL_ANNOTATIONS",
# Convenience
Expand Down
Loading