Skip to content
Merged
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
1 change: 1 addition & 0 deletions mcpgateway/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ class Tool(Base):
request_type: Mapped[str] = mapped_column(default="SSE")
headers: Mapped[Optional[Dict[str, str]]] = mapped_column(JSON)
input_schema: Mapped[Dict[str, Any]] = mapped_column(JSON)
annotations: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, default=lambda: {})
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
is_active: Mapped[bool] = mapped_column(default=True)
Expand Down
6 changes: 6 additions & 0 deletions mcpgateway/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,10 @@ class ToolCreate(BaseModelWithConfig):
default_factory=lambda: {"type": "object", "properties": {}},
description="JSON Schema for validating tool parameters",
)
annotations: Optional[Dict[str, Any]] = Field(
default_factory=dict,
description="Tool annotations for behavior hints (title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint)",
)
jsonpath_filter: Optional[str] = Field(default="", description="JSON modification filter")
auth: Optional[AuthenticationValues] = Field(None, description="Authentication credentials (Basic or Bearer Token or custom headers) if required")
gateway_id: Optional[int] = Field(None, description="id of gateway for the tool")
Expand Down Expand Up @@ -344,6 +348,7 @@ class ToolUpdate(BaseModelWithConfig):
integration_type: Optional[Literal["MCP", "REST"]] = Field(None, description="Tool integration type")
headers: Optional[Dict[str, str]] = Field(None, description="Additional headers to send when invoking the tool")
input_schema: Optional[Dict[str, Any]] = Field(None, description="JSON Schema for validating tool parameters")
annotations: Optional[Dict[str, Any]] = Field(None, description="Tool annotations for behavior hints")
jsonpath_filter: Optional[str] = Field(None, description="JSON path filter for rpc tool calls")
auth: Optional[AuthenticationValues] = Field(None, description="Authentication credentials (Basic or Bearer Token or custom headers) if required")
gateway_id: Optional[int] = Field(None, description="id of gateway for the tool")
Expand Down Expand Up @@ -411,6 +416,7 @@ class ToolRead(BaseModelWithConfig):
integration_type: str
headers: Optional[Dict[str, str]]
input_schema: Dict[str, Any]
annotations: Optional[Dict[str, Any]]
jsonpath_filter: Optional[str]
auth: Optional[AuthenticationValues]
created_at: datetime
Expand Down
1 change: 1 addition & 0 deletions mcpgateway/services/gateway_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ async def register_gateway(self, db: Session, gateway: GatewayCreate) -> Gateway
request_type=tool.request_type,
headers=tool.headers,
input_schema=tool.input_schema,
annotations=tool.annotations,
jsonpath_filter=tool.jsonpath_filter,
auth_type=auth_type,
auth_value=auth_value,
Expand Down
4 changes: 4 additions & 0 deletions mcpgateway/services/tool_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def _convert_tool_to_read(self, tool: DbTool) -> ToolRead:
tool_dict["execution_count"] = tool.execution_count
tool_dict["metrics"] = tool.metrics_summary
tool_dict["request_type"] = tool.request_type
tool_dict["annotations"] = tool.annotations or {}

decoded_auth_value = decode_auth(tool.auth_value)
if tool.auth_type == "basic":
Expand Down Expand Up @@ -213,6 +214,7 @@ async def register_tool(self, db: Session, tool: ToolCreate) -> ToolRead:
request_type=tool.request_type,
headers=tool.headers,
input_schema=tool.input_schema,
annotations=tool.annotations,
jsonpath_filter=tool.jsonpath_filter,
auth_type=auth_type,
auth_value=auth_value,
Expand Down Expand Up @@ -644,6 +646,8 @@ async def update_tool(self, db: Session, tool_id: int, tool_update: ToolUpdate)
tool.headers = tool_update.headers
if tool_update.input_schema is not None:
tool.input_schema = tool_update.input_schema
if tool_update.annotations is not None:
tool.annotations = tool_update.annotations
if tool_update.jsonpath_filter is not None:
tool.jsonpath_filter = tool_update.jsonpath_filter

Expand Down
51 changes: 51 additions & 0 deletions mcpgateway/static/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,54 @@ async function viewTool(toolId) {
authHTML = `<p><strong>Authentication Type:</strong> None</p>`;
}

// Helper function to create annotation badges
const renderAnnotations = (annotations) => {
if (!annotations || Object.keys(annotations).length === 0) {
return '<p><strong>Annotations:</strong> <span class="text-gray-500">None</span></p>';
}

const badges = [];

// Show title if present
if (annotations.title) {
badges.push(`<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 mr-1 mb-1">${annotations.title}</span>`);
}

// Show behavior hints with appropriate colors
if (annotations.readOnlyHint === true) {
badges.push(`<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 mr-1 mb-1">📖 Read-Only</span>`);
}

if (annotations.destructiveHint === true) {
badges.push(`<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 mr-1 mb-1">⚠️ Destructive</span>`);
}

if (annotations.idempotentHint === true) {
badges.push(`<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 mr-1 mb-1">🔄 Idempotent</span>`);
}

if (annotations.openWorldHint === true) {
badges.push(`<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 mr-1 mb-1">🌐 External Access</span>`);
}

// Show any other custom annotations
Object.keys(annotations).forEach(key => {
if (!['title', 'readOnlyHint', 'destructiveHint', 'idempotentHint', 'openWorldHint'].includes(key)) {
const value = annotations[key];
badges.push(`<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 mr-1 mb-1">${key}: ${value}</span>`);
}
});

return `
<div>
<strong>Annotations:</strong>
<div class="mt-1 flex flex-wrap">
${badges.join('')}
</div>
</div>
`;
};

document.getElementById("tool-details").innerHTML = `
<div class="space-y-2 dark:bg-gray-900 dark:text-gray-100">
<p><strong>Name:</strong> ${tool.name}</p>
Expand All @@ -576,6 +624,7 @@ async function viewTool(toolId) {
<p><strong>Description:</strong> ${tool.description || "N/A"}</p>
<p><strong>Request Type:</strong> ${tool.requestType || "N/A"}</p>
${authHTML}
${renderAnnotations(tool.annotations)}
<div>
<strong>Headers:</strong>
<pre class="mt-1 bg-gray-100 p-2 rounded overflow-auto dark:bg-gray-900 dark:text-gray-300">${JSON.stringify(tool.headers || {}, null, 2)}</pre>
Expand Down Expand Up @@ -664,10 +713,12 @@ async function editTool(toolId) {

const headersJson = JSON.stringify(tool.headers || {}, null, 2);
const schemaJson = JSON.stringify(tool.inputSchema || {}, null, 2);
const annotationsJson = JSON.stringify(tool.annotations || {}, null, 2);

// Update the code editor textareas.
document.getElementById("edit-tool-headers").value = headersJson;
document.getElementById("edit-tool-schema").value = schemaJson;
document.getElementById("edit-tool-annotations").value = annotationsJson;
if (window.editToolHeadersEditor) {
window.editToolHeadersEditor.setValue(headersJson);
window.editToolHeadersEditor.refresh();
Expand Down
52 changes: 50 additions & 2 deletions mcpgateway/templates/admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,17 @@ <h3 class="text-lg font-bold mb-4 dark:text-gray-200">Add New Server</h3>
<!-- Tools Panel -->
<div id="tools-panel" class="tab-panel hidden">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold dark:text-gray-200">Registered Tools</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">This is the global catalog of Tools available. Create a Virtual Server using one of these tools.</p>
<div>
<h2 class="text-2xl font-bold dark:text-gray-200">Registered Tools</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">This is the global catalog of Tools available. Create a Virtual Server using one of these tools.</p>
<div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
<strong>Annotation badges:</strong>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 ml-1">📖 Read-Only</span>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 ml-1">⚠️ Destructive</span>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800 ml-1">🔄 Idempotent</span>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800 ml-1">🌐 External Access</span>
</div>
</div>
<div class="flex items-center">
<input
type="checkbox"
Expand Down Expand Up @@ -452,6 +461,11 @@ <h2 class="text-2xl font-bold dark:text-gray-200">Registered Tools</h2>
>
Description
</th>
<th
class="px-2 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-24"
>
Annotations
</th>
<th
class="px-2 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-12"
>
Expand Down Expand Up @@ -497,6 +511,27 @@ <h2 class="text-2xl font-bold dark:text-gray-200">Registered Tools</h2>
>
{{ tool.description }}
</td>
<td class="px-2 py-4 whitespace-nowrap">
{% if tool.annotations %}
{% if tool.annotations.title %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 mr-1 mb-1">{{ tool.annotations.title }}</span>
{% endif %}
{% if tool.annotations.readOnlyHint %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 mr-1 mb-1">📖</span>
{% endif %}
{% if tool.annotations.destructiveHint %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 mr-1 mb-1">⚠️</span>
{% endif %}
{% if tool.annotations.idempotentHint %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800 mr-1 mb-1">🔄</span>
{% endif %}
{% if tool.annotations.openWorldHint %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800 mr-1 mb-1">🌐</span>
{% endif %}
{% else %}
<span class="text-gray-400 text-xs">None</span>
{% endif %}
</td>
<td class="px-3 py-4 whitespace-nowrap">
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {% if tool.isActive %}bg-green-100 text-green-800{% else %}bg-red-100 text-red-800{% endif %}"
Expand Down Expand Up @@ -1864,6 +1899,19 @@ <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Edit Tool</h3>
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-900 dark:placeholder-gray-300 dark:text-gray-300"
></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700"
>Annotations (JSON) - Read Only</label
>
<textarea
name="annotations"
id="edit-tool-annotations"
readonly
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm bg-gray-50 text-gray-600"
placeholder="Annotations are automatically provided by MCP servers"
></textarea>
<p class="mt-1 text-xs text-gray-500">Annotations like readOnlyHint, destructiveHint are provided by the MCP server and cannot be manually edited.</p>
</div>
<!-- Authentication Section -->
<div>
<label class="block text-sm font-medium text-gray-700"
Expand Down
4 changes: 2 additions & 2 deletions mcpgateway/transports/streamablehttp_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,15 @@ async def list_tools() -> List[types.Tool]:
try:
async with get_db() as db:
tools = await tool_service.list_server_tools(db, server_id)
return [types.Tool(name=tool.name, description=tool.description, inputSchema=tool.input_schema) for tool in tools]
return [types.Tool(name=tool.name, description=tool.description, inputSchema=tool.input_schema, annotations=tool.annotations) for tool in tools]
except Exception as e:
logger.exception(f"Error listing tools:{e}")
return []
else:
try:
async with get_db() as db:
tools = await tool_service.list_tools(db)
return [types.Tool(name=tool.name, description=tool.description, inputSchema=tool.input_schema) for tool in tools]
return [types.Tool(name=tool.name, description=tool.description, inputSchema=tool.input_schema, annotations=tool.annotations) for tool in tools]
except Exception as e:
logger.exception(f"Error listing tools:{e}")
return []
Expand Down
2 changes: 2 additions & 0 deletions mcpgateway/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ class Tool(BaseModel):
requestType (str): The HTTP method used to invoke the tool (GET, POST, PUT, DELETE, SSE, STDIO).
headers (Dict[str, Any]): A JSON object representing HTTP headers.
input_schema (Dict[str, Any]): A JSON Schema for validating the tool's input.
annotations (Optional[Dict[str, Any]]): Tool annotations for behavior hints.
auth_type (Optional[str]): The type of authentication used ("basic", "bearer", or None).
auth_username (Optional[str]): The username for basic authentication.
auth_password (Optional[str]): The password for basic authentication.
Expand All @@ -402,6 +403,7 @@ class Tool(BaseModel):
requestType: str = "SSE"
headers: Dict[str, Any] = Field(default_factory=dict)
input_schema: Dict[str, Any] = Field(default_factory=lambda: {"type": "object", "properties": {}})
annotations: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Tool annotations for behavior hints")
auth_type: Optional[str] = None
auth_username: Optional[str] = None
auth_password: Optional[str] = None
Expand Down
1 change: 1 addition & 0 deletions mcpgateway/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ async def handle_list_tools() -> List[types.Tool]:
name=str(tool_name),
description=tool.get("description", ""),
inputSchema=tool.get("inputSchema", {}),
annotations=tool.get("annotations", {}),
)
)
return tools
Expand Down
69 changes: 69 additions & 0 deletions migration_add_annotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
"""
Migration script to add the annotations column to the tools table.

This migration adds support for MCP tool annotations like readOnlyHint, destructiveHint, etc.
"""

import os
import sys

from sqlalchemy import text

# Add the project root to the path so we can import mcpgateway modules
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from mcpgateway.db import engine, get_db


def migrate_up():
"""Add annotations column to tools table."""
print("Adding annotations column to tools table...")

# Check if column already exists
with engine.connect() as conn:
# Try to describe the table first
try:
result = conn.execute(text("PRAGMA table_info(tools)"))
columns = [row[1] for row in result]

if 'annotations' in columns:
print("Annotations column already exists, skipping migration.")
return
except Exception:
# For non-SQLite databases, use a different approach
try:
conn.execute(text("SELECT annotations FROM tools LIMIT 1"))
print("Annotations column already exists, skipping migration.")
return
except Exception:
pass # Column doesn't exist, continue with migration

# Add the annotations column
try:
conn.execute(text("ALTER TABLE tools ADD COLUMN annotations JSON DEFAULT '{}'"))
conn.commit()
print("Successfully added annotations column to tools table.")
except Exception as e:
print(f"Error adding annotations column: {e}")
conn.rollback()
raise

def migrate_down():
"""Remove annotations column from tools table."""
print("Removing annotations column from tools table...")

with engine.connect() as conn:
try:
# Note: SQLite doesn't support DROP COLUMN, so this would require table recreation
# For now, we'll just print a warning
print("Warning: SQLite doesn't support DROP COLUMN. Manual intervention required to remove annotations column.")
except Exception as e:
print(f"Error removing annotations column: {e}")
raise

if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "down":
migrate_down()
else:
migrate_up()
5 changes: 5 additions & 0 deletions tests/unit/mcpgateway/services/test_tool_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ async def test_register_tool(self, tool_service, mock_tool, test_db):
gateway_id=None,
execution_count=0,
auth=None, # Add auth field
annotations={}, # Add annotations field
metrics={
"total_executions": 0,
"successful_executions": 0,
Expand Down Expand Up @@ -231,6 +232,7 @@ async def test_list_tools(self, tool_service, mock_tool, test_db):
gateway_id=None,
execution_count=0,
auth=None, # Add auth field
annotations={}, # Add annotations field
metrics={
"total_executions": 0,
"successful_executions": 0,
Expand Down Expand Up @@ -278,6 +280,7 @@ async def test_get_tool(self, tool_service, mock_tool, test_db):
gateway_id=None,
execution_count=0,
auth=None, # Add auth field
annotations={}, # Add annotations field
metrics={
"total_executions": 0,
"successful_executions": 0,
Expand Down Expand Up @@ -376,6 +379,7 @@ async def test_toggle_tool_status(self, tool_service, mock_tool, test_db):
gateway_id=None,
execution_count=0,
auth=None, # Add auth field
annotations={}, # Add annotations field
metrics={
"total_executions": 0,
"successful_executions": 0,
Expand Down Expand Up @@ -441,6 +445,7 @@ async def test_update_tool(self, tool_service, mock_tool, test_db):
gateway_id=None,
execution_count=0,
auth=None, # Add auth field
annotations={}, # Add annotations field
metrics={
"total_executions": 0,
"successful_executions": 0,
Expand Down
Loading
Loading