Set read-only annotation hints on each tool for security

This commit is contained in:
Fahad
2025-06-26 13:16:00 +04:00
parent 2bed3a0774
commit 0237fb3419
7 changed files with 67 additions and 12 deletions

View File

@@ -14,9 +14,9 @@ import os
# These values are used in server responses and for tracking releases
# IMPORTANT: This is the single source of truth for version and author info
# Semantic versioning: MAJOR.MINOR.PATCH
__version__ = "5.7.0"
__version__ = "5.7.1"
# Last update date in ISO format
__updated__ = "2025-06-23"
__updated__ = "2025-06-26"
# Primary maintainer
__author__ = "Fahad Gilani"

View File

@@ -47,6 +47,7 @@ from mcp.types import ( # noqa: E402
ServerCapabilities,
TextContent,
Tool,
ToolAnnotations,
ToolsCapability,
)
@@ -559,11 +560,16 @@ async def handle_list_tools() -> list[Tool]:
# Add all registered AI-powered tools from the TOOLS registry
for tool in TOOLS.values():
# Get optional annotations from the tool
annotations = tool.get_annotations()
tool_annotations = ToolAnnotations(**annotations) if annotations else None
tools.append(
Tool(
name=tool.name,
description=tool.description,
inputSchema=tool.get_input_schema(),
annotations=tool_annotations,
)
)

View File

@@ -43,11 +43,11 @@ class ListModelsTool(BaseTool):
def get_input_schema(self) -> dict[str, Any]:
"""Return the JSON schema for the tool's input"""
return {
"type": "object",
"properties": {"model": {"type": "string", "description": "Model to use (ignored by listmodels tool)"}},
"required": [],
}
return {"type": "object", "properties": {}, "required": []}
def get_annotations(self) -> Optional[dict[str, Any]]:
"""Return tool annotations indicating this is a read-only tool"""
return {"readOnlyHint": True}
def get_system_prompt(self) -> str:
"""No AI model needed for this tool"""
@@ -57,6 +57,9 @@ class ListModelsTool(BaseTool):
"""Return the Pydantic model for request validation."""
return ToolRequest
def requires_model(self) -> bool:
return False
async def prepare_prompt(self, request: ToolRequest) -> str:
"""Not used for this utility tool"""
return ""

View File

@@ -153,6 +153,19 @@ class BaseTool(ABC):
"""
pass
def get_annotations(self) -> Optional[dict[str, Any]]:
"""
Return optional annotations for this tool.
Annotations provide hints about tool behavior without being security-critical.
They help MCP clients make better decisions about tool usage.
Returns:
Optional[dict]: Dictionary with annotation fields like readOnlyHint, destructiveHint, etc.
Returns None if no annotations are needed.
"""
return None
def requires_model(self) -> bool:
"""
Return whether this tool requires AI model access.

View File

@@ -100,6 +100,21 @@ class SimpleTool(BaseTool):
"""
return []
def get_annotations(self) -> Optional[dict[str, Any]]:
"""
Return tool annotations. Simple tools are read-only by default.
All simple tools perform operations without modifying the environment.
They may call external AI models for analysis or conversation, but they
don't write files or make system changes.
Override this method if your simple tool needs different annotations.
Returns:
Dictionary with readOnlyHint set to True
"""
return {"readOnlyHint": True}
def format_response(self, response: str, request, model_info: Optional[dict] = None) -> str:
"""
Format the AI response before returning to the client.

View File

@@ -147,11 +147,11 @@ class VersionTool(BaseTool):
def get_input_schema(self) -> dict[str, Any]:
"""Return the JSON schema for the tool's input"""
return {
"type": "object",
"properties": {"model": {"type": "string", "description": "Model to use (ignored by version tool)"}},
"required": [],
}
return {"type": "object", "properties": {}, "required": []}
def get_annotations(self) -> Optional[dict[str, Any]]:
"""Return tool annotations indicating this is a read-only tool"""
return {"readOnlyHint": True}
def get_system_prompt(self) -> str:
"""No AI model needed for this tool"""
@@ -161,6 +161,9 @@ class VersionTool(BaseTool):
"""Return the Pydantic model for request validation."""
return ToolRequest
def requires_model(self) -> bool:
return False
async def prepare_prompt(self, request: ToolRequest) -> str:
"""Not used for this utility tool"""
return ""

View File

@@ -110,6 +110,21 @@ class WorkflowTool(BaseTool, BaseWorkflowMixin):
"""
return []
def get_annotations(self) -> Optional[dict[str, Any]]:
"""
Return tool annotations. Workflow tools are read-only by default.
All workflow tools perform analysis and investigation without modifying
the environment. They may call external AI models for expert analysis,
but they don't write files or make system changes.
Override this method if your workflow tool needs different annotations.
Returns:
Dictionary with readOnlyHint set to True
"""
return {"readOnlyHint": True}
def get_input_schema(self) -> dict[str, Any]:
"""
Generate the complete input schema using SchemaBuilder.