Skip to content

Commit 84916ef

Browse files
bryankthompsonweb-flowclaude
authored
feat: Add tool annotations for improved LLM tool understanding (#122)
Add readOnlyHint and destructiveHint annotations to all tools to help LLMs better understand tool behavior and make safer decisions about tool execution. Changes: - Bump MCP SDK dependency from >=1.5.0 to >=1.8.0 (required for annotation support) - Add ToolAnnotations import from mcp.types - Add readOnlyHint: true to 8 read-only tools: - list_schemas: Lists database schemas - list_objects: Lists objects in a schema - get_object_details: Shows detailed info about objects - explain_query: Explains SQL query execution plans - analyze_workload_indexes: Analyzes queries and recommends indexes - analyze_query_indexes: Analyzes specific queries for indexes - analyze_db_health: Checks database health - get_top_queries: Reports slow/resource-intensive queries - Add execute_sql tool with mode-aware annotations: - UNRESTRICTED mode: destructiveHint: true (can modify data) - RESTRICTED mode: readOnlyHint: true (read-only queries) - Add title annotations for human-readable display This improves tool safety metadata for MCP clients. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: triepod-ai <noreply@github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4cd8d44 commit 84916ef

3 files changed

Lines changed: 1323 additions & 546 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "PostgreSQL Tuning and Analysis Tool"
55
readme = "README.md"
66
requires-python = ">=3.12"
77
dependencies = [
8-
"mcp[cli]>=1.5.0",
8+
"mcp[cli]>=1.8.0",
99
"psycopg[binary]>=3.2.6",
1010
"humanize>=4.8.0",
1111
"pglast==7.2.0",

src/postgres_mcp/server.py

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import mcp.types as types
1515
from mcp.server.fastmcp import FastMCP
16+
from mcp.types import ToolAnnotations
1617
from pydantic import Field
1718
from pydantic import validate_call
1819

@@ -80,7 +81,13 @@ def format_error_response(error: str) -> ResponseType:
8081
return format_text_response(f"Error: {error}")
8182

8283

83-
@mcp.tool(description="List all schemas in the database")
84+
@mcp.tool(
85+
description="List all schemas in the database",
86+
annotations=ToolAnnotations(
87+
title="List Schemas",
88+
readOnlyHint=True,
89+
),
90+
)
8491
async def list_schemas() -> ResponseType:
8592
"""List all schemas in the database."""
8693
try:
@@ -106,7 +113,13 @@ async def list_schemas() -> ResponseType:
106113
return format_error_response(str(e))
107114

108115

109-
@mcp.tool(description="List objects in a schema")
116+
@mcp.tool(
117+
description="List objects in a schema",
118+
annotations=ToolAnnotations(
119+
title="List Objects",
120+
readOnlyHint=True,
121+
),
122+
)
110123
async def list_objects(
111124
schema_name: str = Field(description="Schema name"),
112125
object_type: str = Field(description="Object type: 'table', 'view', 'sequence', or 'extension'", default="table"),
@@ -174,7 +187,13 @@ async def list_objects(
174187
return format_error_response(str(e))
175188

176189

177-
@mcp.tool(description="Show detailed information about a database object")
190+
@mcp.tool(
191+
description="Show detailed information about a database object",
192+
annotations=ToolAnnotations(
193+
title="Get Object Details",
194+
readOnlyHint=True,
195+
),
196+
)
178197
async def get_object_details(
179198
schema_name: str = Field(description="Schema name"),
180199
object_name: str = Field(description="Object name"),
@@ -307,7 +326,13 @@ async def get_object_details(
307326
return format_error_response(str(e))
308327

309328

310-
@mcp.tool(description="Explains the execution plan for a SQL query, showing how the database will execute it and provides detailed cost estimates.")
329+
@mcp.tool(
330+
description="Explains the execution plan for a SQL query, showing how the database will execute it and provides detailed cost estimates.",
331+
annotations=ToolAnnotations(
332+
title="Explain Query",
333+
readOnlyHint=True,
334+
),
335+
)
311336
async def explain_query(
312337
sql: str = Field(description="SQL query to explain"),
313338
analyze: bool = Field(
@@ -402,7 +427,13 @@ async def execute_sql(
402427
return format_error_response(str(e))
403428

404429

405-
@mcp.tool(description="Analyze frequently executed queries in the database and recommend optimal indexes")
430+
@mcp.tool(
431+
description="Analyze frequently executed queries in the database and recommend optimal indexes",
432+
annotations=ToolAnnotations(
433+
title="Analyze Workload Indexes",
434+
readOnlyHint=True,
435+
),
436+
)
406437
@validate_call
407438
async def analyze_workload_indexes(
408439
max_index_size_mb: int = Field(description="Max index size in MB", default=10000),
@@ -423,7 +454,13 @@ async def analyze_workload_indexes(
423454
return format_error_response(str(e))
424455

425456

426-
@mcp.tool(description="Analyze a list of (up to 10) SQL queries and recommend optimal indexes")
457+
@mcp.tool(
458+
description="Analyze a list of (up to 10) SQL queries and recommend optimal indexes",
459+
annotations=ToolAnnotations(
460+
title="Analyze Query Indexes",
461+
readOnlyHint=True,
462+
),
463+
)
427464
@validate_call
428465
async def analyze_query_indexes(
429466
queries: list[str] = Field(description="List of Query strings to analyze"),
@@ -460,7 +497,11 @@ async def analyze_query_indexes(
460497
"- buffer - checks for buffer cache hit rates for indexes and tables\n"
461498
"- constraint - checks for invalid constraints\n"
462499
"- all - runs all checks\n"
463-
"You can optionally specify a single health check or a comma-separated list of health checks. The default is 'all' checks."
500+
"You can optionally specify a single health check or a comma-separated list of health checks. The default is 'all' checks.",
501+
annotations=ToolAnnotations(
502+
title="Analyze Database Health",
503+
readOnlyHint=True,
504+
),
464505
)
465506
async def analyze_db_health(
466507
health_type: str = Field(
@@ -482,6 +523,10 @@ async def analyze_db_health(
482523
@mcp.tool(
483524
name="get_top_queries",
484525
description=f"Reports the slowest or most resource-intensive queries using data from the '{PG_STAT_STATEMENTS}' extension.",
526+
annotations=ToolAnnotations(
527+
title="Get Top Queries",
528+
readOnlyHint=True,
529+
),
485530
)
486531
async def get_top_queries(
487532
sort_by: str = Field(
@@ -546,11 +591,25 @@ async def main():
546591
global current_access_mode
547592
current_access_mode = AccessMode(args.access_mode)
548593

549-
# Add the query tool with a description appropriate to the access mode
594+
# Add the query tool with a description and annotations appropriate to the access mode
550595
if current_access_mode == AccessMode.UNRESTRICTED:
551-
mcp.add_tool(execute_sql, description="Execute any SQL query")
596+
mcp.add_tool(
597+
execute_sql,
598+
description="Execute any SQL query",
599+
annotations=ToolAnnotations(
600+
title="Execute SQL",
601+
destructiveHint=True,
602+
),
603+
)
552604
else:
553-
mcp.add_tool(execute_sql, description="Execute a read-only SQL query")
605+
mcp.add_tool(
606+
execute_sql,
607+
description="Execute a read-only SQL query",
608+
annotations=ToolAnnotations(
609+
title="Execute SQL (Read-Only)",
610+
readOnlyHint=True,
611+
),
612+
)
554613

555614
logger.info(f"Starting PostgreSQL MCP Server in {current_access_mode.upper()} mode")
556615

0 commit comments

Comments
 (0)