Skip to content

Commit 18b754a

Browse files
committed
Include tool annotations
Signed-off-by: onmete <[email protected]>
1 parent 4315bf5 commit 18b754a

File tree

13 files changed

+205
-14
lines changed

13 files changed

+205
-14
lines changed

mcpgateway/db.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ class Tool(Base):
302302
request_type: Mapped[str] = mapped_column(default="SSE")
303303
headers: Mapped[Optional[Dict[str, str]]] = mapped_column(JSON)
304304
input_schema: Mapped[Dict[str, Any]] = mapped_column(JSON)
305+
annotations: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, default=lambda: {})
305306
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
306307
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
307308
is_active: Mapped[bool] = mapped_column(default=True)

mcpgateway/schemas.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,10 @@ class ToolCreate(BaseModelWithConfig):
286286
default_factory=lambda: {"type": "object", "properties": {}},
287287
description="JSON Schema for validating tool parameters",
288288
)
289+
annotations: Optional[Dict[str, Any]] = Field(
290+
default_factory=dict,
291+
description="Tool annotations for behavior hints (title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint)",
292+
)
289293
jsonpath_filter: Optional[str] = Field(default="", description="JSON modification filter")
290294
auth: Optional[AuthenticationValues] = Field(None, description="Authentication credentials (Basic or Bearer Token or custom headers) if required")
291295
gateway_id: Optional[int] = Field(None, description="id of gateway for the tool")
@@ -344,6 +348,7 @@ class ToolUpdate(BaseModelWithConfig):
344348
integration_type: Optional[Literal["MCP", "REST"]] = Field(None, description="Tool integration type")
345349
headers: Optional[Dict[str, str]] = Field(None, description="Additional headers to send when invoking the tool")
346350
input_schema: Optional[Dict[str, Any]] = Field(None, description="JSON Schema for validating tool parameters")
351+
annotations: Optional[Dict[str, Any]] = Field(None, description="Tool annotations for behavior hints")
347352
jsonpath_filter: Optional[str] = Field(None, description="JSON path filter for rpc tool calls")
348353
auth: Optional[AuthenticationValues] = Field(None, description="Authentication credentials (Basic or Bearer Token or custom headers) if required")
349354
gateway_id: Optional[int] = Field(None, description="id of gateway for the tool")
@@ -411,6 +416,7 @@ class ToolRead(BaseModelWithConfig):
411416
integration_type: str
412417
headers: Optional[Dict[str, str]]
413418
input_schema: Dict[str, Any]
419+
annotations: Optional[Dict[str, Any]]
414420
jsonpath_filter: Optional[str]
415421
auth: Optional[AuthenticationValues]
416422
created_at: datetime

mcpgateway/services/gateway_service.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ async def register_gateway(self, db: Session, gateway: GatewayCreate) -> Gateway
203203
request_type=tool.request_type,
204204
headers=tool.headers,
205205
input_schema=tool.input_schema,
206+
annotations=tool.annotations,
206207
jsonpath_filter=tool.jsonpath_filter,
207208
auth_type=auth_type,
208209
auth_value=auth_value,

mcpgateway/services/tool_service.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def _convert_tool_to_read(self, tool: DbTool) -> ToolRead:
124124
tool_dict["execution_count"] = tool.execution_count
125125
tool_dict["metrics"] = tool.metrics_summary
126126
tool_dict["request_type"] = tool.request_type
127+
tool_dict["annotations"] = tool.annotations or {}
127128

128129
decoded_auth_value = decode_auth(tool.auth_value)
129130
if tool.auth_type == "basic":
@@ -213,6 +214,7 @@ async def register_tool(self, db: Session, tool: ToolCreate) -> ToolRead:
213214
request_type=tool.request_type,
214215
headers=tool.headers,
215216
input_schema=tool.input_schema,
217+
annotations=tool.annotations,
216218
jsonpath_filter=tool.jsonpath_filter,
217219
auth_type=auth_type,
218220
auth_value=auth_value,
@@ -644,6 +646,8 @@ async def update_tool(self, db: Session, tool_id: int, tool_update: ToolUpdate)
644646
tool.headers = tool_update.headers
645647
if tool_update.input_schema is not None:
646648
tool.input_schema = tool_update.input_schema
649+
if tool_update.annotations is not None:
650+
tool.annotations = tool_update.annotations
647651
if tool_update.jsonpath_filter is not None:
648652
tool.jsonpath_filter = tool_update.jsonpath_filter
649653

mcpgateway/static/admin.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,54 @@ async function viewTool(toolId) {
568568
authHTML = `<p><strong>Authentication Type:</strong> None</p>`;
569569
}
570570

571+
// Helper function to create annotation badges
572+
const renderAnnotations = (annotations) => {
573+
if (!annotations || Object.keys(annotations).length === 0) {
574+
return '<p><strong>Annotations:</strong> <span class="text-gray-500">None</span></p>';
575+
}
576+
577+
const badges = [];
578+
579+
// Show title if present
580+
if (annotations.title) {
581+
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>`);
582+
}
583+
584+
// Show behavior hints with appropriate colors
585+
if (annotations.readOnlyHint === true) {
586+
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>`);
587+
}
588+
589+
if (annotations.destructiveHint === true) {
590+
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>`);
591+
}
592+
593+
if (annotations.idempotentHint === true) {
594+
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>`);
595+
}
596+
597+
if (annotations.openWorldHint === true) {
598+
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>`);
599+
}
600+
601+
// Show any other custom annotations
602+
Object.keys(annotations).forEach(key => {
603+
if (!['title', 'readOnlyHint', 'destructiveHint', 'idempotentHint', 'openWorldHint'].includes(key)) {
604+
const value = annotations[key];
605+
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>`);
606+
}
607+
});
608+
609+
return `
610+
<div>
611+
<strong>Annotations:</strong>
612+
<div class="mt-1 flex flex-wrap">
613+
${badges.join('')}
614+
</div>
615+
</div>
616+
`;
617+
};
618+
571619
document.getElementById("tool-details").innerHTML = `
572620
<div class="space-y-2 dark:bg-gray-900 dark:text-gray-100">
573621
<p><strong>Name:</strong> ${tool.name}</p>
@@ -576,6 +624,7 @@ async function viewTool(toolId) {
576624
<p><strong>Description:</strong> ${tool.description || "N/A"}</p>
577625
<p><strong>Request Type:</strong> ${tool.requestType || "N/A"}</p>
578626
${authHTML}
627+
${renderAnnotations(tool.annotations)}
579628
<div>
580629
<strong>Headers:</strong>
581630
<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>
@@ -664,10 +713,12 @@ async function editTool(toolId) {
664713

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

668718
// Update the code editor textareas.
669719
document.getElementById("edit-tool-headers").value = headersJson;
670720
document.getElementById("edit-tool-schema").value = schemaJson;
721+
document.getElementById("edit-tool-annotations").value = annotationsJson;
671722
if (window.editToolHeadersEditor) {
672723
window.editToolHeadersEditor.setValue(headersJson);
673724
window.editToolHeadersEditor.refresh();

mcpgateway/templates/admin.html

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,8 +401,17 @@ <h3 class="text-lg font-bold mb-4 dark:text-gray-200">Add New Server</h3>
401401
<!-- Tools Panel -->
402402
<div id="tools-panel" class="tab-panel hidden">
403403
<div class="flex justify-between items-center mb-4">
404-
<h2 class="text-2xl font-bold dark:text-gray-200">Registered Tools</h2>
405-
<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>
404+
<div>
405+
<h2 class="text-2xl font-bold dark:text-gray-200">Registered Tools</h2>
406+
<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>
407+
<div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
408+
<strong>Annotation badges:</strong>
409+
<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>
410+
<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>
411+
<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>
412+
<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>
413+
</div>
414+
</div>
406415
<div class="flex items-center">
407416
<input
408417
type="checkbox"
@@ -452,6 +461,11 @@ <h2 class="text-2xl font-bold dark:text-gray-200">Registered Tools</h2>
452461
>
453462
Description
454463
</th>
464+
<th
465+
class="px-2 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-24"
466+
>
467+
Annotations
468+
</th>
455469
<th
456470
class="px-2 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-12"
457471
>
@@ -497,6 +511,27 @@ <h2 class="text-2xl font-bold dark:text-gray-200">Registered Tools</h2>
497511
>
498512
{{ tool.description }}
499513
</td>
514+
<td class="px-2 py-4 whitespace-nowrap">
515+
{% if tool.annotations %}
516+
{% if tool.annotations.title %}
517+
<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>
518+
{% endif %}
519+
{% if tool.annotations.readOnlyHint %}
520+
<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>
521+
{% endif %}
522+
{% if tool.annotations.destructiveHint %}
523+
<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>
524+
{% endif %}
525+
{% if tool.annotations.idempotentHint %}
526+
<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>
527+
{% endif %}
528+
{% if tool.annotations.openWorldHint %}
529+
<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>
530+
{% endif %}
531+
{% else %}
532+
<span class="text-gray-400 text-xs">None</span>
533+
{% endif %}
534+
</td>
500535
<td class="px-3 py-4 whitespace-nowrap">
501536
<span
502537
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 %}"
@@ -1864,6 +1899,19 @@ <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Edit Tool</h3>
18641899
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"
18651900
></textarea>
18661901
</div>
1902+
<div>
1903+
<label class="block text-sm font-medium text-gray-700"
1904+
>Annotations (JSON) - Read Only</label
1905+
>
1906+
<textarea
1907+
name="annotations"
1908+
id="edit-tool-annotations"
1909+
readonly
1910+
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm bg-gray-50 text-gray-600"
1911+
placeholder="Annotations are automatically provided by MCP servers"
1912+
></textarea>
1913+
<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>
1914+
</div>
18671915
<!-- Authentication Section -->
18681916
<div>
18691917
<label class="block text-sm font-medium text-gray-700"

mcpgateway/transports/streamablehttp_transport.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,15 +216,15 @@ async def list_tools() -> List[types.Tool]:
216216
try:
217217
async with get_db() as db:
218218
tools = await tool_service.list_server_tools(db, server_id)
219-
return [types.Tool(name=tool.name, description=tool.description, inputSchema=tool.input_schema) for tool in tools]
219+
return [types.Tool(name=tool.name, description=tool.description, inputSchema=tool.input_schema, annotations=tool.annotations) for tool in tools]
220220
except Exception as e:
221221
logger.exception(f"Error listing tools:{e}")
222222
return []
223223
else:
224224
try:
225225
async with get_db() as db:
226226
tools = await tool_service.list_tools(db)
227-
return [types.Tool(name=tool.name, description=tool.description, inputSchema=tool.input_schema) for tool in tools]
227+
return [types.Tool(name=tool.name, description=tool.description, inputSchema=tool.input_schema, annotations=tool.annotations) for tool in tools]
228228
except Exception as e:
229229
logger.exception(f"Error listing tools:{e}")
230230
return []

mcpgateway/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ class Tool(BaseModel):
389389
requestType (str): The HTTP method used to invoke the tool (GET, POST, PUT, DELETE, SSE, STDIO).
390390
headers (Dict[str, Any]): A JSON object representing HTTP headers.
391391
input_schema (Dict[str, Any]): A JSON Schema for validating the tool's input.
392+
annotations (Optional[Dict[str, Any]]): Tool annotations for behavior hints.
392393
auth_type (Optional[str]): The type of authentication used ("basic", "bearer", or None).
393394
auth_username (Optional[str]): The username for basic authentication.
394395
auth_password (Optional[str]): The password for basic authentication.
@@ -402,6 +403,7 @@ class Tool(BaseModel):
402403
requestType: str = "SSE"
403404
headers: Dict[str, Any] = Field(default_factory=dict)
404405
input_schema: Dict[str, Any] = Field(default_factory=lambda: {"type": "object", "properties": {}})
406+
annotations: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Tool annotations for behavior hints")
405407
auth_type: Optional[str] = None
406408
auth_username: Optional[str] = None
407409
auth_password: Optional[str] = None

mcpgateway/wrapper.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ async def handle_list_tools() -> List[types.Tool]:
330330
name=str(tool_name),
331331
description=tool.get("description", ""),
332332
inputSchema=tool.get("inputSchema", {}),
333+
annotations=tool.get("annotations", {}),
333334
)
334335
)
335336
return tools

migration_add_annotations.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Migration script to add the annotations column to the tools table.
4+
5+
This migration adds support for MCP tool annotations like readOnlyHint, destructiveHint, etc.
6+
"""
7+
8+
import os
9+
import sys
10+
11+
from sqlalchemy import text
12+
13+
# Add the project root to the path so we can import mcpgateway modules
14+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
15+
16+
from mcpgateway.db import engine, get_db
17+
18+
19+
def migrate_up():
20+
"""Add annotations column to tools table."""
21+
print("Adding annotations column to tools table...")
22+
23+
# Check if column already exists
24+
with engine.connect() as conn:
25+
# Try to describe the table first
26+
try:
27+
result = conn.execute(text("PRAGMA table_info(tools)"))
28+
columns = [row[1] for row in result]
29+
30+
if 'annotations' in columns:
31+
print("Annotations column already exists, skipping migration.")
32+
return
33+
except Exception:
34+
# For non-SQLite databases, use a different approach
35+
try:
36+
conn.execute(text("SELECT annotations FROM tools LIMIT 1"))
37+
print("Annotations column already exists, skipping migration.")
38+
return
39+
except Exception:
40+
pass # Column doesn't exist, continue with migration
41+
42+
# Add the annotations column
43+
try:
44+
conn.execute(text("ALTER TABLE tools ADD COLUMN annotations JSON DEFAULT '{}'"))
45+
conn.commit()
46+
print("Successfully added annotations column to tools table.")
47+
except Exception as e:
48+
print(f"Error adding annotations column: {e}")
49+
conn.rollback()
50+
raise
51+
52+
def migrate_down():
53+
"""Remove annotations column from tools table."""
54+
print("Removing annotations column from tools table...")
55+
56+
with engine.connect() as conn:
57+
try:
58+
# Note: SQLite doesn't support DROP COLUMN, so this would require table recreation
59+
# For now, we'll just print a warning
60+
print("Warning: SQLite doesn't support DROP COLUMN. Manual intervention required to remove annotations column.")
61+
except Exception as e:
62+
print(f"Error removing annotations column: {e}")
63+
raise
64+
65+
if __name__ == "__main__":
66+
if len(sys.argv) > 1 and sys.argv[1] == "down":
67+
migrate_down()
68+
else:
69+
migrate_up()

0 commit comments

Comments
 (0)