feat:接入mcp,为模型提供图表信息

This commit is contained in:
2026-10-01 06:33:44 -07:00
parent 618290d852
commit f1b2a70ad1
44 changed files with 6531 additions and 207 deletions
+106
View File
@@ -0,0 +1,106 @@
"""物料字段 MCP 服务:只给 agent 的模型查询组件有哪些字段、字段含义和可取值
只监听本机,由应用启动时拉起,不对外暴露;查询逻辑在 app.resources.material_fields,
动作校验在主进程直接调用同一套逻辑,这里只是给模型使用的入口
单独运行:uv run python -X utf8 -m mcp_servers.material_server
"""
import asyncio
import os
from typing import Annotated, Any
from urllib.parse import urlsplit
from loguru import logger
from mcp.server.fastmcp import FastMCP
from pydantic import Field
from pymongo import AsyncMongoClient
from app.config import settings
from app.resources import material_fields
from app.schemas.component_resource import ComponentCatalog
from app.services.material_mongo import MongoMaterialRepository
_target = urlsplit(settings.mcp_material_url)
mcp = FastMCP(
"materials",
host="127.0.0.1",
# 由应用拉起时通过 PORT 传入,单独运行时取配置地址中的端口
port=int(os.environ.get("PORT") or _target.port or 8201),
streamable_http_path=_target.path or "/mcp",
# 工具只读且无会话状态,每次调用独立处理
stateless_http=True,
)
# 启动时从 MongoDB 载入,与主进程读取同一份目录;返回结果带版本,主进程据此核对
_catalog: ComponentCatalog | None = None
ComponentType = Annotated[
str, Field(description="物料类型,即大屏节点的 type,例如 bar-chart")
]
def _require_catalog() -> ComponentCatalog:
if _catalog is None:
raise RuntimeError("物料目录未载入")
return _catalog
@mcp.tool()
async def list_field_groups(component_type: ComponentType) -> dict[str, Any]:
"""列出物料的字段分组:每组管什么、包含哪些字段名称
先看分组目录,再用 get_fields 查看相关分组的完整字段定义
"""
return material_fields.list_field_groups(_require_catalog(), component_type)
@mcp.tool()
async def get_fields(
component_type: ComponentType,
group_keys: Annotated[
list[str], Field(description="要查看的分组 key,可一次传多个", min_length=1)
],
) -> dict[str, Any]:
"""查看分组内的完整字段定义:路径、含义、值类型、可选值、取值范围和默认值
修改组件属性只能使用这里返回的字段路径,值须符合字段的类型和取值约束
"""
return material_fields.get_fields(_require_catalog(), component_type, group_keys)
@mcp.tool()
async def search_fields(
component_type: ComponentType,
query: Annotated[
str, Field(description="用户对属性的说法,例如“名字”“柱子颜色”“数值标签”")
],
) -> dict[str, Any]:
"""按名称、别名、路径和说明搜索字段,按相关度返回完整字段定义
用户的说法对不上分组时使用;多个结果时根据字段说明判断用户指的是哪一个
"""
return material_fields.search_fields(_require_catalog(), component_type, query)
async def _load_catalog() -> ComponentCatalog:
if not settings.mongodb_uri:
raise SystemExit("未配置 MONGODB_URI,物料字段服务无法载入目录")
client = AsyncMongoClient(settings.mongodb_uri, serverSelectionTimeoutMS=5000)
try:
catalog = await MongoMaterialRepository(client[settings.mongodb_db]).load_all()
finally:
await client.close()
if catalog is None:
raise SystemExit("materials 集合尚未导入,物料字段服务无法载入目录")
return catalog
if __name__ == "__main__":
_catalog = asyncio.run(_load_catalog())
logger.info(
"物料字段服务已载入 {} 个物料,版本 {}",
len(_catalog.components),
_catalog.version,
)
mcp.run(transport="streamable-http")