Set read-only annotation hints on each tool for security
This commit is contained in:
@@ -14,9 +14,9 @@ import os
|
|||||||
# These values are used in server responses and for tracking releases
|
# These values are used in server responses and for tracking releases
|
||||||
# IMPORTANT: This is the single source of truth for version and author info
|
# IMPORTANT: This is the single source of truth for version and author info
|
||||||
# Semantic versioning: MAJOR.MINOR.PATCH
|
# Semantic versioning: MAJOR.MINOR.PATCH
|
||||||
__version__ = "5.7.0"
|
__version__ = "5.7.1"
|
||||||
# Last update date in ISO format
|
# Last update date in ISO format
|
||||||
__updated__ = "2025-06-23"
|
__updated__ = "2025-06-26"
|
||||||
# Primary maintainer
|
# Primary maintainer
|
||||||
__author__ = "Fahad Gilani"
|
__author__ = "Fahad Gilani"
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ from mcp.types import ( # noqa: E402
|
|||||||
ServerCapabilities,
|
ServerCapabilities,
|
||||||
TextContent,
|
TextContent,
|
||||||
Tool,
|
Tool,
|
||||||
|
ToolAnnotations,
|
||||||
ToolsCapability,
|
ToolsCapability,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -559,11 +560,16 @@ async def handle_list_tools() -> list[Tool]:
|
|||||||
|
|
||||||
# Add all registered AI-powered tools from the TOOLS registry
|
# Add all registered AI-powered tools from the TOOLS registry
|
||||||
for tool in TOOLS.values():
|
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(
|
tools.append(
|
||||||
Tool(
|
Tool(
|
||||||
name=tool.name,
|
name=tool.name,
|
||||||
description=tool.description,
|
description=tool.description,
|
||||||
inputSchema=tool.get_input_schema(),
|
inputSchema=tool.get_input_schema(),
|
||||||
|
annotations=tool_annotations,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -43,11 +43,11 @@ class ListModelsTool(BaseTool):
|
|||||||
|
|
||||||
def get_input_schema(self) -> dict[str, Any]:
|
def get_input_schema(self) -> dict[str, Any]:
|
||||||
"""Return the JSON schema for the tool's input"""
|
"""Return the JSON schema for the tool's input"""
|
||||||
return {
|
return {"type": "object", "properties": {}, "required": []}
|
||||||
"type": "object",
|
|
||||||
"properties": {"model": {"type": "string", "description": "Model to use (ignored by listmodels tool)"}},
|
def get_annotations(self) -> Optional[dict[str, Any]]:
|
||||||
"required": [],
|
"""Return tool annotations indicating this is a read-only tool"""
|
||||||
}
|
return {"readOnlyHint": True}
|
||||||
|
|
||||||
def get_system_prompt(self) -> str:
|
def get_system_prompt(self) -> str:
|
||||||
"""No AI model needed for this tool"""
|
"""No AI model needed for this tool"""
|
||||||
@@ -57,6 +57,9 @@ class ListModelsTool(BaseTool):
|
|||||||
"""Return the Pydantic model for request validation."""
|
"""Return the Pydantic model for request validation."""
|
||||||
return ToolRequest
|
return ToolRequest
|
||||||
|
|
||||||
|
def requires_model(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
async def prepare_prompt(self, request: ToolRequest) -> str:
|
async def prepare_prompt(self, request: ToolRequest) -> str:
|
||||||
"""Not used for this utility tool"""
|
"""Not used for this utility tool"""
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -153,6 +153,19 @@ class BaseTool(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
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:
|
def requires_model(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Return whether this tool requires AI model access.
|
Return whether this tool requires AI model access.
|
||||||
|
|||||||
@@ -100,6 +100,21 @@ class SimpleTool(BaseTool):
|
|||||||
"""
|
"""
|
||||||
return []
|
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:
|
def format_response(self, response: str, request, model_info: Optional[dict] = None) -> str:
|
||||||
"""
|
"""
|
||||||
Format the AI response before returning to the client.
|
Format the AI response before returning to the client.
|
||||||
|
|||||||
@@ -147,11 +147,11 @@ class VersionTool(BaseTool):
|
|||||||
|
|
||||||
def get_input_schema(self) -> dict[str, Any]:
|
def get_input_schema(self) -> dict[str, Any]:
|
||||||
"""Return the JSON schema for the tool's input"""
|
"""Return the JSON schema for the tool's input"""
|
||||||
return {
|
return {"type": "object", "properties": {}, "required": []}
|
||||||
"type": "object",
|
|
||||||
"properties": {"model": {"type": "string", "description": "Model to use (ignored by version tool)"}},
|
def get_annotations(self) -> Optional[dict[str, Any]]:
|
||||||
"required": [],
|
"""Return tool annotations indicating this is a read-only tool"""
|
||||||
}
|
return {"readOnlyHint": True}
|
||||||
|
|
||||||
def get_system_prompt(self) -> str:
|
def get_system_prompt(self) -> str:
|
||||||
"""No AI model needed for this tool"""
|
"""No AI model needed for this tool"""
|
||||||
@@ -161,6 +161,9 @@ class VersionTool(BaseTool):
|
|||||||
"""Return the Pydantic model for request validation."""
|
"""Return the Pydantic model for request validation."""
|
||||||
return ToolRequest
|
return ToolRequest
|
||||||
|
|
||||||
|
def requires_model(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
async def prepare_prompt(self, request: ToolRequest) -> str:
|
async def prepare_prompt(self, request: ToolRequest) -> str:
|
||||||
"""Not used for this utility tool"""
|
"""Not used for this utility tool"""
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -110,6 +110,21 @@ class WorkflowTool(BaseTool, BaseWorkflowMixin):
|
|||||||
"""
|
"""
|
||||||
return []
|
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]:
|
def get_input_schema(self) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Generate the complete input schema using SchemaBuilder.
|
Generate the complete input schema using SchemaBuilder.
|
||||||
|
|||||||
Reference in New Issue
Block a user