MCP 工具开发实战:在 MCP-Server-and-PostgreSQL 零售分析项目中构建安全、高性能的数据库查询工具
MCP 工具开发实战:在 MCP-Server-and-PostgreSQL 零售分析项目中构建安全、高性能的数据库查询工具
本指南聚焦 mcp-for-beginners 仓库中 11-MCPServerHandsOnLabs/06-Tools 实验的核心内容:为 Zava Retail 分析型 MCP 服务器构建一套完整的工具层——涵盖统一的工具基座架构、SQL 注入防护、销售分析、Schema 自省与商业智能分析。读完本文,你将掌握如何设计带参数校验与度量埋点的 MCP 工具基类、如何用多层校验保障自定义查询安全、如何用模板化 SQL 提供稳定的分析能力,以及如何将工具接入 MCP Server 实现实验 中定义的数据库连接层。
学习目标
完成本实验后,你将能够:
- 设计带复杂参数校验的进阶 MCP 工具
- 实现具备 SQL 注入防护的安全数据库查询工具
- 创建用于动态查询的 Schema 自省(Schema Introspection)能力
- 构建面向商业智能的定制化分析工具
- 应用全面的错误处理与优雅降级策略
- 优化工具在生成环境负载下的性能
工具层整体架构
MCP 工具是 AI 助手与数据系统之间的接口。一个设计良好的工具应在维持安全性与性能的同时,为复杂操作提供结构化、经过校验的访问。本实验围绕一个零售 MCP 服务器展开:它内置一套完整的工具集,支持以自然语言查询销售数据、产品目录与业务分析,同时保持严格的安全边界与最优性能。
工具层的设计遵循"统一基类 + 分类注册 + 统一返回结构"的模式,所有工具最终都通过 MCP Server 实现实验 中的 mcp.tool() 注册暴露给 AI 客户端,并共享同一数据库连接池 db_provider。
工具分类与统一返回结构
工具分类使用枚举 ToolCategory 组织,便于发现与归档:
class ToolCategory(Enum):
"""Tool categorization for organization and discovery."""
DATABASE_QUERY = "database_query"
SCHEMA_INTROSPECTION = "schema_introspection"
ANALYTICS = "analytics"
UTILITY = "utility"
ADMINISTRATIVE = "administrative"
所有工具的执行结果统一封装为 ToolResult 数据类,携带执行状态、数据、错误信息、元数据、执行耗时与行数,方便上层统一处理:
@dataclass
class ToolResult:
"""Standardized tool result structure."""
success: bool
data: Any = None
error: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
execution_time_ms: Optional[float] = None
row_count: Optional[int] = None
BaseTool 基类:校验、度量与错误处理的统一入口
BaseTool 是所有 MCP 工具的抽象基类。它要求子类实现两个抽象方法:execute(执行工具逻辑)与 get_input_schema(返回用于输入校验的 JSON Schema)。
class BaseTool(ABC):
"""Abstract base class for all MCP tools."""
def __init__(self, name: str, description: str, category: ToolCategory):
self.name = name
self.description = description
self.category = category
self.call_count = 0
self.total_execution_time = 0.0
@abstractmethod
async def execute(self, **kwargs) -> ToolResult:
"""Execute the tool with given parameters."""
pass
@abstractmethod
def get_input_schema(self) -> Dict[str, Any]:
"""Get JSON schema for tool input validation."""
pass
关键设计在于 call 包装方法——它是所有工具执行的统一入口,顺序完成三件事:
- 输入校验:调用
_validate_input,对照get_input_schema中声明的required与properties检查必填参数是否齐全;缺失时抛出ValueError(生产环境可引入jsonschema库做完整的类型校验); - 指标埋点与日志:记录
call_count、执行耗时execution_time_ms与累计耗时,并通过_sanitize_parameters对日志参数脱敏; - 错误兜底:
try/except捕获所有异常,返回success=False的ToolResult,保证工具失败时 AI 客户端拿到结构化错误而非未处理异常。
async def call(self, **kwargs) -> ToolResult:
"""Wrapper for tool execution with metrics and error handling."""
start_time = time.time()
self.call_count += 1
try:
# Validate input parameters
self._validate_input(kwargs)
# Log tool execution
logger.info(
f"Executing tool: {self.name}",
extra={
'tool_name': self.name,
'tool_category': self.category.value,
'parameters': self._sanitize_parameters(kwargs)
}
)
# Execute the tool
result = await self.execute(**kwargs)
# Record execution time
execution_time = (time.time() - start_time) * 1000
result.execution_time_ms = execution_time
self.total_execution_time += execution_time
# Log success
logger.info(
f"Tool execution completed: {self.name}",
extra={
'tool_name': self.name,
'execution_time_ms': execution_time,
'success': result.success,
'row_count': result.row_count
}
)
return result
except Exception as e:
execution_time = (time.time() - start_time) * 1000
logger.error(
f"Tool execution failed: {self.name}",
extra={
'tool_name': self.name,
'execution_time_ms': execution_time,
'error': str(e)
},
exc_info=True
)
return ToolResult(
success=False,
error=f"Tool execution failed: {str(e)}",
execution_time_ms=execution_time
)
日志脱敏逻辑默认屏蔽 password、token、secret、key 等敏感字段:
def _sanitize_parameters(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
"""Sanitize parameters for logging (remove sensitive data)."""
# Remove or mask sensitive parameters
sanitized = kwargs.copy()
sensitive_keys = ['password', 'token', 'secret', 'key']
for key in sanitized:
if any(sensitive in key.lower() for sensitive in sensitive_keys):
sanitized[key] = "***MASKED***"
return sanitized
get_statistics 提供工具级的使用统计,可用于监控与调优:
def get_statistics(self) -> Dict[str, Any]:
"""Get tool usage statistics."""
return {
'name': self.name,
'category': self.category.value,
'call_count': self.call_count,
'total_execution_time_ms': self.total_execution_time,
'average_execution_time_ms': (
self.total_execution_time / self.call_count
if self.call_count > 0 else 0
)
}
DatabaseTool:数据库工具的公共能力
DatabaseTool 继承 BaseTool,将类别固定为 DATABASE_QUERY,并注入 db_provider(即 MCP Server 实现实验 中定义的 PostgreSQLSchemaProvider 全局实例)。它提供两个核心能力:
连接上下文管理——通过 asynccontextmanager 保证连接正确获取与释放:
@asynccontextmanager
async def get_connection(self):
"""Get database connection with proper context management."""
conn = None
try:
conn = await self.db_provider.get_connection()
yield conn
finally:
if conn:
await self.db_provider.release_connection(conn)
统一查询执行——execute_query 是安全与性能的汇聚点:
- 若传入
store_id,先执行SELECT retail.set_store_context($1)设置门店上下文,这对应 Database 实验 中定义的retail.set_store_context函数——该函数会校验门店存在且激活、写入app.current_store_id配置项并记入审计日志,从而触发 PostgreSQL 行级安全(RLS)策略,实现多租户数据隔离; - 使用
conn.fetch(query, *params)参数化查询执行,参数通过占位符$1/$2/$3绑定,从机制上杜绝字符串拼接型注入; - 记录耗时、将行转换为字典并返回
ToolResult。
async def execute_query(
self,
query: str,
params: tuple = None,
store_id: str = None
) -> ToolResult:
"""Execute database query with security and performance monitoring."""
async with self.get_connection() as conn:
try:
# Set store context if provided
if store_id:
await conn.execute("SELECT retail.set_store_context($1)", store_id)
# Execute query
start_time = time.time()
if params:
rows = await conn.fetch(query, *params)
else:
rows = await conn.fetch(query)
execution_time = (time.time() - start_time) * 1000
# Convert rows to dictionaries
data = [dict(row) for row in rows]
return ToolResult(
success=True,
data=data,
row_count=len(data),
execution_time_ms=execution_time
)
except Exception as e:
logger.error(f"Database query failed: {str(e)}")
return ToolResult(
success=False,
error=f"Query execution failed: {str(e)}"
)
查询校验与安全
自定义 SQL 查询是注入攻击的高发入口,本实验通过 QueryValidator 提供四层防护:语句类型白名单、危险关键字检测、注入特征正则匹配、表/模式访问白名单。
import re
import sqlparse
from typing import List, Dict, Any, Set
from enum import Enum
class QueryRisk(Enum):
"""Query risk levels."""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class QueryValidator:
"""Validate and analyze SQL queries for security risks."""
# Dangerous SQL keywords and patterns
DANGEROUS_KEYWORDS = {
'DROP', 'DELETE', 'TRUNCATE', 'ALTER', 'CREATE', 'INSERT',
'UPDATE', 'GRANT', 'REVOKE', 'EXEC', 'EXECUTE', 'sp_',
'xp_', 'BULK', 'OPENROWSET', 'OPENDATASOURCE'
}
# Allowed read-only operations
SAFE_KEYWORDS = {
'SELECT', 'WITH', 'UNION', 'ORDER', 'GROUP', 'HAVING',
'WHERE', 'FROM', 'JOIN', 'AS', 'ON', 'IN', 'EXISTS',
'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'AND', 'OR', 'NOT'
}
# Allowed schemas and tables
ALLOWED_SCHEMAS = {'retail', 'information_schema', 'pg_catalog'}
ALLOWED_TABLES = {
'customers', 'products', 'sales_transactions',
'sales_transaction_items', 'product_categories',
'product_embeddings', 'stores'
}
def __init__(self):
self.injection_patterns = [
# SQL injection patterns
r"(\b(UNION|union)\s+(ALL\s+)?(SELECT|select))",
r"(\b(DROP|drop)\s+(TABLE|table|DATABASE|database))",
r"(\b(DELETE|delete)\s+(FROM|from))",
r"(\b(INSERT|insert)\s+(INTO|into))",
r"(\b(UPDATE|update)\s+\w+\s+(SET|set))",
r"(\b(EXEC|exec|EXECUTE|execute)\s*\()",
r"(\b(sp_|xp_)\w+)",
r"(--\s*$)", # SQL comments
r"(/\*.*?\*/)", # Block comments
r"(;\s*(DROP|DELETE|INSERT|UPDATE|CREATE|ALTER))",
r"(\bOR\b\s+['\"]?\w+['\"]?\s*=\s*['\"]?\w+['\"]?)", # OR injection
r"(\bAND\b\s+['\"]?\w+['\"]?\s*=\s*['\"]?\w+['\"]?)", # AND injection
]
self.compiled_patterns = [re.compile(pattern, re.IGNORECASE) for pattern in self.injection_patterns]
注意 ALLOWED_TABLES 与 Database 实验 中创建的 retail schema 表一一对应(stores、customers、products、sales_transactions、sales_transaction_items、product_categories、product_embeddings),形成"代码层白名单 + 数据库层 RLS"的双重防线。
validate_query 是校验主流程,输出结构化的验证结果:
def validate_query(self, query: str) -> Dict[str, Any]:
"""Comprehensive query validation."""
validation_result = {
'is_safe': True,
'risk_level': QueryRisk.LOW,
'issues': [],
'warnings': [],
'allowed_operations': [],
'metadata': {}
}
try:
# Parse the query
parsed = sqlparse.parse(query)
if not parsed:
validation_result['is_safe'] = False
validation_result['issues'].append("Unable to parse query")
validation_result['risk_level'] = QueryRisk.HIGH
return validation_result
# Analyze each statement
for statement in parsed:
self._analyze_statement(statement, validation_result)
# Check for injection patterns
self._check_injection_patterns(query, validation_result)
# Validate table/schema access
self._validate_table_access(query, validation_result)
# Determine final risk level
self._determine_risk_level(validation_result)
except Exception as e:
validation_result['is_safe'] = False
validation_result['issues'].append(f"Query analysis failed: {str(e)}")
validation_result['risk_level'] = QueryRisk.CRITICAL
return validation_result
各辅助方法职责如下:
_analyze_statement:通过sqlparse解析语句类型,仅允许SELECT与WITH;遍历 token,命中DANGEROUS_KEYWORDS判定不安全,命中SAFE_KEYWORDS则记录到allowed_operations;_check_injection_patterns:对 12 条注入特征正则逐一匹配(UNION SELECT、DROP TABLE、注释符--、块注释、分号拼接多语句、OR/AND等值注入等);_validate_table_access:从FROM与JOIN子句中提取表引用。带模式名的引用校验 schema 是否在ALLOWED_SCHEMAS中,未授权 schema 记为 issue;不带模式名的引用按retail推断,未在白名单的表记入warnings;_determine_risk_level:注入特征命中 →CRITICAL;DROP/DELETE等危险操作 →HIGH;其余不安全 →MEDIUM;仅含警告或完全安全 →LOW。
模块底部创建全局单例 query_validator = QueryValidator(),供各工具复用。
数据库查询工具
销售分析工具(SalesAnalysisTool)
SalesAnalysisTool 对外注册名为 execute_sales_query,定位是"支持自然语言的复杂销售分析"。它内置 5 条预构建查询模板,覆盖零售分析高频场景:
| 模板键 | 分析内容 | 查询要点 |
|---|---|---|
daily_sales |
每日销售汇总 | 按 DATE(transaction_date) 分组统计交易数、总收入、客单价、独立客户数 |
top_products |
热销商品排行 | 三表 JOIN(sales_transaction_items、sales_transactions、products),按收入降序并 LIMIT $3 |
customer_analysis |
客户价值分析 | 关联客户与交易,输出消费频次、总消费额、最近购买距今天数,HAVING 过滤无交易客户 |
category_performance |
品类表现 | 四表 JOIN 统计品类下的商品数、销量、收入、均价与交易数 |
sales_trends |
销售趋势 | CTE + 窗口函数:LAG 计算日环比/周环比增长率,7 日滚动平均 |
模板 SQL 全部使用 $1/$2/$3 参数占位符,例如 daily_sales:
'daily_sales': """
SELECT
DATE(transaction_date) as sales_date,
COUNT(*) as transaction_count,
SUM(total_amount) as total_revenue,
AVG(total_amount) as avg_transaction_value,
COUNT(DISTINCT customer_id) as unique_customers
FROM retail.sales_transactions
WHERE transaction_date >= $1 AND transaction_date <= $2
AND transaction_type = 'sale'
GROUP BY DATE(transaction_date)
ORDER BY sales_date DESC
""",
执行逻辑分派 query_type:
async def execute(self, **kwargs) -> ToolResult:
"""Execute sales analysis query."""
query_type = kwargs.get('query_type', 'custom')
store_id = kwargs.get('store_id')
if not store_id:
return ToolResult(
success=False,
error="store_id is required for sales analysis"
)
try:
if query_type in self.query_templates:
return await self._execute_template_query(query_type, kwargs)
elif query_type == 'custom':
return await self._execute_custom_query(kwargs)
else:
return ToolResult(
success=False,
error=f"Unknown query type: {query_type}"
)
except Exception as e:
return ToolResult(
success=False,
error=f"Sales analysis failed: {str(e)}"
)
模板查询 _execute_template_query 提供合理的默认参数:start_date 默认近 30 天、end_date 默认今天、limit 默认 20;自动识别字符串日期并解析为 date;按模板是否含 $3 决定参数元组是 (start_date, end_date, limit) 还是 (start_date, end_date);成功后在 metadata 中回填查询类型、日期范围、store_id 与分析类型:
async def _execute_template_query(self, query_type: str, kwargs: Dict[str, Any]) -> ToolResult:
"""Execute pre-built template query."""
query = self.query_templates[query_type]
store_id = kwargs['store_id']
# Default parameters for template queries
start_date = kwargs.get('start_date', (datetime.now() - timedelta(days=30)).date())
end_date = kwargs.get('end_date', datetime.now().date())
limit = kwargs.get('limit', 20)
# Convert string dates if needed
if isinstance(start_date, str):
start_date = datetime.fromisoformat(start_date).date()
if isinstance(end_date, str):
end_date = datetime.fromisoformat(end_date).date()
# Execute query with parameters
params = (start_date, end_date, limit) if '$3' in query else (start_date, end_date)
result = await self.execute_query(query, params, store_id)
if result.success:
result.metadata = {
'query_type': query_type,
'date_range': f"{start_date} to {end_date}",
'store_id': store_id,
'analysis_type': 'template'
}
return result
自定义查询 _execute_custom_query 是"安全自定义 SQL"的关键路径:查询必须先通过 query_validator.validate_query,不安全时拒绝执行并返回校验结果与风险等级;通过后以无参数形式交给 execute_query 执行,并把校验警告带回 metadata:
async def _execute_custom_query(self, kwargs: Dict[str, Any]) -> ToolResult:
"""Execute custom SQL query with validation."""
custom_query = kwargs.get('query')
store_id = kwargs['store_id']
if not custom_query:
return ToolResult(
success=False,
error="Custom query is required when query_type is 'custom'"
)
# Validate the query for security
validation = query_validator.validate_query(custom_query)
if not validation['is_safe']:
return ToolResult(
success=False,
error=f"Query validation failed: {', '.join(validation['issues'])}",
metadata={
'validation_result': validation,
'risk_level': validation['risk_level'].value
}
)
# Execute validated query
result = await self.execute_query(custom_query, None, store_id)
if result.success:
result.metadata = {
'query_type': 'custom',
'store_id': store_id,
'validation_warnings': validation.get('warnings', []),
'analysis_type': 'custom'
}
return result
输入 Schema 是工具与 AI 客户端之间的契约。get_input_schema 定义了完整的参数元信息,其中 query_type 通过 enum 限制为 5 个模板键加 custom;store_id 用正则 ^[a-zA-Z0-9_-]+$ 约束;limit 限定 1~1000、默认 20;并设置 required: ["store_id"] 与 additionalProperties: False(禁止未知参数,进一步收紧攻击面):
def get_input_schema(self) -> Dict[str, Any]:
"""Get input schema for the sales analysis tool."""
return {
"type": "object",
"properties": {
"query_type": {
"type": "string",
"enum": list(self.query_templates.keys()) + ["custom"],
"description": "Type of sales analysis to perform",
"default": "daily_sales"
},
"store_id": {
"type": "string",
"description": "Store ID for data isolation",
"pattern": "^[a-zA-Z0-9_-]+$"
},
"start_date": {
"type": "string",
"format": "date",
"description": "Start date for analysis (YYYY-MM-DD)"
},
"end_date": {
"type": "string",
"format": "date",
"description": "End date for analysis (YYYY-MM-DD)"
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 1000,
"description": "Maximum number of results to return",
"default": 20
},
"query": {
"type": "string",
"description": "Custom SQL query (required when query_type is 'custom')"
}
},
"required": ["store_id"],
"additionalProperties": False
}
Schema 自省工具(SchemaIntrospectionTool)
SchemaIntrospectionTool 对外注册名为 get_table_schema,属于 SCHEMA_INTROSPECTION 类别。它让 AI 模型在编写查询前先了解表结构——这与 MCP Server 实现实验 中 execute_sales_query 工具文档"先取表结构再写查询"的最佳实践相呼应。执行时通过 table_name 的有无区分单表与全表模式:
async def execute(self, **kwargs) -> ToolResult:
"""Execute schema introspection."""
table_name = kwargs.get('table_name')
include_constraints = kwargs.get('include_constraints', True)
include_indexes = kwargs.get('include_indexes', True)
include_statistics = kwargs.get('include_statistics', False)
try:
if table_name:
return await self._get_single_table_schema(
table_name, include_constraints, include_indexes, include_statistics
)
else:
return await self._get_all_tables_schema(include_constraints, include_indexes)
except Exception as e:
return ToolResult(
success=False,
error=f"Schema introspection failed: {str(e)}"
)
单表模式 _get_single_table_schema 通过 information_schema.columns 获取列定义(列名、类型、可空性、默认值、长度、精度、序号、UDT),可选查询约束(table_constraints + key_column_usage + referential_constraints 多表关联)、索引(pg_indexes)与表统计(pg_stat_user_tables 的插入/更新/删除计数、存活/死元组、最近 vacuum/analyze 时间),全部限定 table_schema = 'retail':
# Get column information
columns_query = """
SELECT
column_name,
data_type,
is_nullable,
column_default,
character_maximum_length,
numeric_precision,
numeric_scale,
ordinal_position,
udt_name
FROM information_schema.columns
WHERE table_schema = 'retail' AND table_name = $1
ORDER BY ordinal_position
"""
全表模式 _get_all_tables_schema 先列出 retail schema 下所有表,再逐表取列信息,返回 schema_name + tables 数组,并在 metadata 中记录 table_count。
输入 Schema 支持可选参数:table_name 用 ^[a-zA-Z_][a-zA-Z0-9_]*$ 约束(缺省返回全部表),三个布尔开关控制约束/索引/统计信息的包含与否。
多表 Schema 工具(MultiTableSchemaTool)
MultiTableSchemaTool 对外注册名为 get_multiple_table_schemas,在一次调用中批量获取多张表的结构,显著减少 AI 模型规划查询时的往返次数。执行逻辑在单连接内循环查询各表:
async def execute(self, **kwargs) -> ToolResult:
"""Execute multi-table schema introspection."""
table_names = kwargs.get('table_names', [])
if not table_names:
return ToolResult(
success=False,
error="At least one table name is required"
)
try:
schemas = {}
async with self.get_connection() as conn:
for table_name in table_names:
# Get table schema
schema_query = """
SELECT
c.column_name,
c.data_type,
c.is_nullable,
c.column_default,
c.character_maximum_length,
tc.constraint_type,
kcu.constraint_name
FROM information_schema.columns c
LEFT JOIN information_schema.key_column_usage kcu
ON c.table_name = kcu.table_name
AND c.column_name = kcu.column_name
AND c.table_schema = kcu.table_schema
LEFT JOIN information_schema.table_constraints tc
ON kcu.constraint_name = tc.constraint_name
AND kcu.table_schema = tc.table_schema
WHERE c.table_schema = 'retail' AND c.table_name = $1
ORDER BY c.ordinal_position
"""
columns = await conn.fetch(schema_query, table_name)
if columns:
schemas[table_name] = {
'table_name': table_name,
'columns': [dict(col) for col in columns]
}
else:
schemas[table_name] = {
'table_name': table_name,
'error': 'Table not found or not accessible'
}
return ToolResult(
success=True,
data=schemas,
metadata={
'requested_tables': table_names,
'found_tables': [name for name, info in schemas.items() if 'error' not in info],
'missing_tables': [name for name, info in schemas.items() if 'error' in info]
}
)
except Exception as e:
return ToolResult(
success=False,
error=f"Multi-table schema introspection failed: {str(e)}"
)
不存在的表不会导致整体失败,而是以 error 字段标记并在 metadata 中通过 missing_tables 汇总,体现优雅降级。其输入 Schema 要求 table_names 数组(minItems: 1、maxItems: 20,每项正则约束),并以 required: ["table_names"] 强制必填。
分析与辅助工具
商业智能工具(BusinessIntelligenceTool)
BusinessIntelligenceTool 对外注册名为 generate_business_insights,属于 ANALYTICS 类别,用于生成综合性的业务报告与洞察。它通过 analysis_type 分派四种分析(summary 汇总、customer_segmentation 客户分群、product_performance 产品表现、seasonal_trends 季节性趋势),store_id 为必填:
async def execute(self, **kwargs) -> ToolResult:
"""Execute business intelligence analysis."""
analysis_type = kwargs.get('analysis_type', 'summary')
store_id = kwargs.get('store_id')
if not store_id:
return ToolResult(
success=False,
error="store_id is required for business intelligence analysis"
)
try:
if analysis_type == 'summary':
return await self._generate_business_summary(kwargs)
elif analysis_type == 'customer_segmentation':
return await self._analyze_customer_segmentation(kwargs)
elif analysis_type == 'product_performance':
return await self._analyze_product_performance(kwargs)
elif analysis_type == 'seasonal_trends':
return await self._analyze_seasonal_trends(kwargs)
else:
return ToolResult(
success=False,
error=f"Unknown analysis type: {analysis_type}"
)
except Exception as e:
return ToolResult(
success=False,
error=f"Business intelligence analysis failed: {str(e)}"
)
_generate_business_summary 用一条多 CTE 查询聚合销售汇总、产品汇总与最热品类,并在此基础上计算派生指标(日均交易数、客单价、单笔件数等),追加到结果中:
if result.success and result.data:
summary = result.data[0]
# Add derived insights
insights = {
'revenue_trend': 'stable', # Would calculate based on historical data
'customer_retention': f"{summary.get('unique_customers', 0)} active customers",
'performance_indicators': {
'transactions_per_day': round(summary.get('total_transactions', 0) / max(summary.get('active_days', 1), 1), 2),
'revenue_per_customer': round(summary.get('total_revenue', 0) / max(summary.get('unique_customers', 1), 1), 2),
'items_per_transaction': round(summary.get('total_items_sold', 0) / max(summary.get('total_transactions', 1), 1), 2)
}
}
summary['insights'] = insights
result.data = [summary]
result.metadata = {
'analysis_type': 'business_summary',
'period_days': days,
'store_id': store_id
}
其输入 Schema 中 analysis_type 用 enum 限定四种取值(默认 summary),days 限定 1~365(默认 30)。
辅助工具(UtilityTool)
UtilityTool 对外注册名为 get_current_utc_date,属于 UTILITY 类别,为 AI 提供统一的时间基准(对"最近 30 天"等时间敏感查询至关重要)。它根据 format 参数(iso/epoch/date_only,默认 iso)选择不同的 PostgreSQL 表达式:
async def execute(self, **kwargs) -> ToolResult:
"""Execute utility operation."""
format_type = kwargs.get('format', 'iso')
try:
async with self.get_connection() as conn:
if format_type == 'iso':
query = "SELECT CURRENT_TIMESTAMP AT TIME ZONE 'UTC' as current_utc_datetime"
elif format_type == 'epoch':
query = "SELECT EXTRACT(EPOCH FROM CURRENT_TIMESTAMP AT TIME ZONE 'UTC') as current_utc_epoch"
elif format_type == 'date_only':
query = "SELECT CURRENT_DATE as current_date"
else:
return ToolResult(
success=False,
error=f"Unknown format type: {format_type}"
)
result = await conn.fetchrow(query)
return ToolResult(
success=True,
data=dict(result),
metadata={
'format_type': format_type,
'timezone': 'UTC'
}
)
except Exception as e:
return ToolResult(
success=False,
error=f"Utility operation failed: {str(e)}"
)
与服务器实现和数据库层的衔接
本实验的工具设计并非孤岛,而是与整条实验链深度咬合:
- 连接与 RLS:
DatabaseTool.execute_query中retail.set_store_context($1)直接调用 Database 实验 定义的函数,该函数校验门店有效性、写入app.current_store_id并记录审计日志;配合customers/products/sales_transactions等表上的 RLS 策略,实现"同一 SQL 面对不同门店返回不同数据"的多租户隔离。 - 工具注册:MCP Server 实现实验 中
sales_analysis.py通过@mcp.tool()注册的get_multiple_table_schemas、execute_sales_query、get_current_utc_date,与本实验的MultiTableSchemaTool、SalesAnalysisTool、UtilityTool一一对应;FastMCP 会基于工具的 Pydantic 字段描述自动生成 MCPtools/list所需的 JSON Schema。 - 查询超时与结果截断:服务器层的
execute_query(见 sales_analysis_postgres.py 相关实现)通过asyncio.wait_for设置command_timeout(默认 30 秒)并默认只返回前 20 行,防止长查询与超大结果集拖垮 AI 会话。
关键要点
完成本实验后,你应当掌握:
- ✅ 进阶工具架构:实现了带全面错误处理、参数校验与统计埋点的复杂 MCP 工具(统一继承
BaseTool/DatabaseTool,返回ToolResult) - ✅ 查询校验:构建了防止注入攻击的安全 SQL 校验器(语句类型白名单 + 危险关键字 + 注入正则 + 表/模式白名单四层防线)
- ✅ 数据库工具:创建了强大的销售分析与 Schema 自省能力(模板化查询 + 单表/多表/全表自省)
- ✅ 商业智能:开发了面向全面业务洞察的分析工具(汇总、客户分群、产品表现、季节性趋势)
- ✅ 性能优化:应用了连接管理、查询超时、结果截断与执行耗时度量
- ✅ 安全集成:实现了基于角色的访问控制(
store_id强制校验 + RLS 上下文)与动作审计(日志脱敏 +set_store_context审计写入)
下一步
继续学习 实验 07:语义搜索集成,你将在此基础上:
- 将向量搜索能力与 MCP 工具集成
- 构建语义化商品搜索功能
- 实现 AI 驱动的查询理解
- 创建融合传统查询与向量查询的混合搜索
相关前置阅读:实验 05:MCP 服务器实现(工具所依赖的连接池、RLS 上下文与 FastMCP 注册层)、实验 04:数据库设计与 Schema(retail schema、RLS 策略与 set_store_context 函数)。
说明:本实验文档由 AI 翻译服务生成(见仓库 translations 版本),本文基于英文原版 11-MCPServerHandsOnLabs/06-Tools/README.md 编写;涉及外部链接的资源章节请以原文档为准。