122 lines
4.2 KiB
Python
122 lines
4.2 KiB
Python
"""物料 MCP 服务:给模型查询组件目录、物料默认配置和字段定义
|
||
|
||
只监听本机,由应用按 mcp_servers.json 拉起,不对外暴露;查询逻辑在 app.resources.material_fields,
|
||
动作校验在主进程直接调用同一套逻辑,这里只是给模型使用的入口
|
||
|
||
单独运行:uv run python -X utf8 -m mcp_servers.material_server(端口取 PORT,默认 8201)
|
||
"""
|
||
|
||
import asyncio
|
||
import os
|
||
from typing import Annotated, Any
|
||
|
||
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
|
||
|
||
mcp = FastMCP(
|
||
"materials",
|
||
host="127.0.0.1",
|
||
# 由应用拉起时通过 PORT 传入,单独运行时默认 8201
|
||
port=int(os.environ.get("PORT") or 8201),
|
||
streamable_http_path="/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_components() -> dict[str, Any]:
|
||
"""列出可以新增的组件:key、type、名称、用途和默认宽高"""
|
||
return material_fields.list_components(_require_catalog())
|
||
|
||
|
||
@mcp.tool()
|
||
async def get_component_materials(
|
||
component_types: Annotated[
|
||
list[ComponentType],
|
||
Field(description="要使用的物料类型,可一次传多个", min_length=1),
|
||
],
|
||
) -> dict[str, Any]:
|
||
"""获取物料的完整默认配置(schema)和使用说明"""
|
||
return material_fields.get_materials(_require_catalog(), component_types)
|
||
|
||
|
||
@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")
|