feat:接入mcp,为模型提供图表信息
This commit is contained in:
@@ -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")
|
||||
Reference in New Issue
Block a user