Compare commits

..

2 Commits

Author SHA1 Message Date
4bce8dbad0 feat(file_tools): 添加解析JSON或文本文件的新工具
新增 parse_json 工具,用于读取 txt 或 json 文件内容并智能解析。
工具优先尝试将内容解析为JSON,返回结构化数据;若解析失败则作为纯文本返回。
同时更新项目版本号至 0.1.3。
2026-05-09 14:57:42 +08:00
7e33e398ce feat(k3cloud): 新增单据查看功能并重构演示代码
- 在 client.py 中新增 view() 函数,支持调用金蝶云星空 DynamicFormService.View 接口
- 在 tools.py 中新增 view_form 工具,供 MCP 客户端调用
- 添加演示脚本 demo_view.py 和示例配置文件 demo_view.json
- 重构 demo_save.py 为通用演示脚本,支持自定义 payload 文件
- 删除过时的 demo.json,新增 userful_save.json 作为实用示例
- 添加项目交接文档,详细说明各子项目架构和配置
- 优化 payload 序列化逻辑,提取为共享函数 _serialize_payload
2026-05-08 17:15:20 +08:00
12 changed files with 1348 additions and 103 deletions

View File

@@ -120,6 +120,24 @@ tools: List[Tool] = [
}, },
"required": ["file_path"] "required": ["file_path"]
} }
),
Tool(
name="parse_json",
description="读取txt或json文件内容优先按JSON解析并返回结构化数据",
inputSchema={
"type": "object",
"properties": {
"file": {
"type": "object",
"properties": {
"name": {"type": "string"},
"content_base64": {"type": "string"}
}
},
"file_path": {"type": "string"}
},
"required": []
}
) )
] ]
@@ -430,6 +448,49 @@ def _build_path_data_uri(arguments: Dict[str, Any]) -> str:
return _build_data_uri(data, mime_type) return _build_data_uri(data, mime_type)
def _build_txt_or_json_result(data: bytes, name: Optional[str]) -> str:
text = data.decode("utf-8-sig")
try:
parsed = json.loads(text)
return json.dumps(
{
"type": "json",
"name": name,
"data": parsed
},
ensure_ascii=False
)
except json.JSONDecodeError:
return json.dumps(
{
"type": "text",
"name": name,
"data": text
},
ensure_ascii=False
)
async def _parse_json(arguments: Dict[str, Any]) -> str:
raw_file_path = str(arguments.get("file_path", ""))
file_path = raw_file_path.strip().strip("`").strip()
if _is_url(file_path):
async with httpx.AsyncClient(follow_redirects=True, timeout=20) as client:
response = await client.get(file_path)
response.raise_for_status()
name = Path(urlparse(file_path).path).name or None
return _build_txt_or_json_result(response.content, name)
if file_path and file_path != arguments.get("file_path"):
arguments = dict(arguments)
arguments["file_path"] = file_path
file_info, data, _ = _extract_file_payload(arguments)
if data is None:
raise ValueError("missing file content")
return _build_txt_or_json_result(data, file_info.get("name"))
async def _build_url_data_uri(arguments: Dict[str, Any]) -> str: async def _build_url_data_uri(arguments: Dict[str, Any]) -> str:
url = arguments.get("url") url = arguments.get("url")
if not url: if not url:
@@ -519,6 +580,8 @@ async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> List[TextCon
if not file_path: if not file_path:
raise ValueError("missing file_path") raise ValueError("missing file_path")
result = await asyncio.to_thread(_upload_file_to_minio_sync, file_path) result = await asyncio.to_thread(_upload_file_to_minio_sync, file_path)
elif name == "parse_json":
result = await _parse_json(arguments)
else: else:
raise ValueError(f"unknown tool name: {name}") raise ValueError(f"unknown tool name: {name}")
return [TextContent(type="text", text=result)] return [TextContent(type="text", text=result)]

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "lzwcai-file-tools-mcp" name = "lzwcai-file-tools-mcp"
version = "0.1.1" version = "0.1.3"
description = "File tools MCP server" description = "File tools MCP server"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [

View File

@@ -12,6 +12,22 @@ def build_save_url(path: str) -> str:
return f"{Config.BASE_URL.rstrip('/')}{path}" return f"{Config.BASE_URL.rstrip('/')}{path}"
def _serialize_payload(payload: Any, field_name: str) -> str:
if payload is None:
raise ValueError(f"{field_name} 不能为空")
if isinstance(payload, str):
payload_value = payload.strip()
if not payload_value:
raise ValueError(f"{field_name} 不能为空字符串")
return payload_value
try:
return json.dumps(payload, ensure_ascii=False)
except TypeError as exc:
raise TypeError(
f"{field_name} 必须是可序列化的 JSON 对象或 JSON 字符串"
) from exc
def save(formid: str, data_payload: Any, timeout: int = 30) -> Dict[str, Any]: def save(formid: str, data_payload: Any, timeout: int = 30) -> Dict[str, Any]:
""" """
Call the K3Cloud Save API. Call the K3Cloud Save API.
@@ -22,18 +38,7 @@ def save(formid: str, data_payload: Any, timeout: int = 30) -> Dict[str, Any]:
normalized_formid = str(formid or "").strip() normalized_formid = str(formid or "").strip()
if not normalized_formid: if not normalized_formid:
raise ValueError("formid 不能为空") raise ValueError("formid 不能为空")
if data_payload is None: payload_value = _serialize_payload(data_payload, "data_payload")
raise ValueError("data_payload 不能为空")
if isinstance(data_payload, str):
payload_value = data_payload.strip()
if not payload_value:
raise ValueError("data_payload 不能为空字符串")
else:
try:
payload_value = json.dumps(data_payload, ensure_ascii=False)
except TypeError as exc:
raise TypeError("data_payload 必须是可序列化的 JSON 对象或 JSON 字符串") from exc
payload = { payload = {
"formid": normalized_formid, "formid": normalized_formid,
@@ -48,3 +53,30 @@ def save(formid: str, data_payload: Any, timeout: int = 30) -> Dict[str, Any]:
) )
response.raise_for_status() response.raise_for_status()
return response.json() return response.json()
def view(formid: str, payload: Any, timeout: int = 30) -> Dict[str, Any]:
"""
Call the K3Cloud View API.
`payload` can be a JSON object or a pre-serialized JSON string.
"""
view_service_path = "/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.View.common.kdsvc"
normalized_formid = str(formid or "").strip()
if not normalized_formid:
raise ValueError("formid 不能为空")
payload_value = _serialize_payload(payload, "payload")
request_body = {
"formid": normalized_formid,
"data": payload_value,
}
response = requests.post(
build_save_url(view_service_path),
json=request_body,
headers=build_headers(view_service_path),
timeout=timeout,
)
response.raise_for_status()
return response.json()

View File

@@ -1,78 +0,0 @@
{
"NeedUpDateFields": [],
"NeedReturnFields": [],
"IsDeleteEntry": "true",
"SubSystemId": "",
"IsVerifyBaseDataField": "false",
"IsEntryBatchFill": "true",
"ValidateFlag": "true",
"NumberSearch": "true",
"IsAutoAdjustField": "false",
"InterationFlags": "",
"IgnoreInterationFlag": "",
"IsControlPrecision": "false",
"ValidateRepeatJson": "false",
"Model": {
"FID": 0,
"FBillTypeID": {
"FNUMBER": ""
},
"FBillNo": "1",
"FPAYORGID": {
"FNumber": ""
},
"FDATE": "1900-01-01",
"FCONTACTUNITTYPE": "",
"FCONTACTUNIT": {
"FNumber": ""
},
"FPAYUNITTYPE": "",
"FPAYUNIT": {
"FNumber": ""
},
"FCURRENCYID": {
"FNumber": ""
},
"FSETTLECUR": {
"FNUMBER": ""
},
"FDOCUMENTSTATUS": "",
"FBUSINESSTYPE": "",
"FCancelStatus": "",
"FSETTLEMAINBOOKID": {
"FNUMBER": ""
},
"FRECEIVEBILLENTRY": [
{
"FEntryID": 0,
"FSETTLETYPEID": {
"FNumber": ""
},
"FPURPOSEID": {
"FNumber": "SFKYT01_SYS"
},
"FPOSTDATE": "1900-01-01",
"FASSSALESORDER": [
{
"FDetailID": 0
}
]
}
],
"FRECEIVEBILLSRCENTRY": [
{
"FEntryID": 0
}
],
"FBILLRECEIVABLEENTRY": [
{
"FEntryID": 0
}
],
"FBILLSKDRECENTRY": [
{
"FEntryID": 0
}
]
}
}

View File

@@ -0,0 +1,34 @@
{
"Model": {
"FID": 0,
"FBillTypeID": {
"FNUMBER": "SKDLX02_SYS"
},
"FBillNo": "1211",
"FPAYORGID": {
"FNumber": "100"
},
"FDATE": "2026-05-02",
"FCONTACTUNITTYPE": "BD_Empinfo",
"FCONTACTUNIT": {
"FNumber": "220804"
},
"FPAYUNITTYPE": "BD_Empinfo",
"FCURRENCYID": {
"FNumber": "PRE001"
},
"FRECEIVEBILLENTRY": [
{
"FEntryID": 0,
"FRECAMOUNTFOR_E": 123.0,
"FRECTOTALAMOUNTFOR": 123456.0,
"FSETTLETYPEID": {
"FNumber": "JSFS01_SYS"
},
"FPURPOSEID": {
"FNumber": "SFKYT004"
}
}
]
}
}

View File

@@ -8,7 +8,7 @@ from typing import Any
CURRENT_DIR = Path(__file__).resolve().parent CURRENT_DIR = Path(__file__).resolve().parent
PROJECT_ROOT = CURRENT_DIR.parent PROJECT_ROOT = CURRENT_DIR.parent
DEFAULT_PAYLOAD_PATH = CURRENT_DIR / "demo.json" DEFAULT_PAYLOAD_PATH = CURRENT_DIR / "demo_save.json"
MCP_SERVER_PATH = PROJECT_ROOT / "mcp-server.json" MCP_SERVER_PATH = PROJECT_ROOT / "mcp-server.json"

View File

@@ -0,0 +1,6 @@
{
"CreateOrgId": 0,
"Number": "SKD00126864",
"Id": "",
"IsSortBySeq": "false"
}

View File

@@ -0,0 +1,72 @@
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any
CURRENT_DIR = Path(__file__).resolve().parent
PROJECT_ROOT = CURRENT_DIR.parent
DEFAULT_PAYLOAD_PATH = CURRENT_DIR / "demo_view.json"
MCP_SERVER_PATH = PROJECT_ROOT / "mcp-server.json"
def _ensure_project_root_on_path() -> None:
root = str(PROJECT_ROOT)
if root not in sys.path:
sys.path.insert(0, root)
def _load_payload(payload_path: Path) -> Any:
raw_text = payload_path.read_text(encoding="utf-8").strip()
if not raw_text:
raise ValueError(f"payload 文件为空: {payload_path}")
return json.loads(raw_text)
def _bootstrap_env_from_mcp_server() -> None:
if not MCP_SERVER_PATH.exists():
return
data = json.loads(MCP_SERVER_PATH.read_text(encoding="utf-8"))
env_map = data.get("mcpServers", {}).get("k3cloud-mcp", {}).get("env", {})
for key, value in env_map.items():
if key not in os.environ and value is not None:
os.environ[key] = str(value)
def main() -> None:
parser = argparse.ArgumentParser(
description="读取 payload JSON 并直接调用 k3cloud_mcp.client.view()"
)
parser.add_argument("formid", help="K3Cloud 表单标识,例如 BD_Customer")
parser.add_argument(
"--payload",
default=str(DEFAULT_PAYLOAD_PATH),
help="payload JSON 文件路径,默认读取同级 demo_view.json",
)
parser.add_argument(
"--timeout",
type=int,
default=30,
help="请求超时时间,单位秒,默认 30",
)
args = parser.parse_args()
payload_path = Path(args.payload).expanduser().resolve()
_ensure_project_root_on_path()
_bootstrap_env_from_mcp_server()
from k3cloud_mcp.client import view
result = view(
formid=args.formid,
payload=_load_payload(payload_path),
timeout=args.timeout,
)
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@@ -3,7 +3,7 @@ from typing import Any, Dict, List
from mcp.types import TextContent, Tool from mcp.types import TextContent, Tool
from .client import save from .client import save, view
tools = [ tools = [
Tool( Tool(
@@ -28,17 +28,40 @@ tools = [
}, },
"required": ["formid", "data_payload"], "required": ["formid", "data_payload"],
}, },
) ),
Tool(
name="view_form",
description="调用金蝶云星空 DynamicFormService.View 接口查看单据。",
inputSchema={
"type": "object",
"properties": {
"formid": {
"type": "string",
"description": "业务对象表单 Id例如 SAL_QUOTATION",
},
"payload": {
"description": "View 接口的 payload 参数,支持 JSON 对象或 JSON 字符串",
},
"timeout": {
"type": "integer",
"description": "HTTP 超时时间(秒),默认 30",
"default": 30,
"minimum": 1,
},
},
"required": ["formid", "payload"],
},
),
] ]
def _normalize_payload(value: object) -> Any: def _normalize_payload(value: object, field_name: str) -> Any:
if value is None: if value is None:
raise ValueError("missing data_payload") raise ValueError(f"missing {field_name}")
if isinstance(value, str): if isinstance(value, str):
normalized = value.strip() normalized = value.strip()
if not normalized: if not normalized:
raise ValueError("missing data_payload") raise ValueError(f"missing {field_name}")
try: try:
return json.loads(normalized) return json.loads(normalized)
except json.JSONDecodeError: except json.JSONDecodeError:
@@ -48,7 +71,7 @@ def _normalize_payload(value: object) -> Any:
async def handle_call_tool(name: str, arguments: Dict[str, object]) -> List[TextContent]: async def handle_call_tool(name: str, arguments: Dict[str, object]) -> List[TextContent]:
try: try:
if name != "save_form": if name not in {"save_form", "view_form"}:
raise ValueError(f"unknown tool name: {name}") raise ValueError(f"unknown tool name: {name}")
formid = str(arguments.get("formid", "")).strip() formid = str(arguments.get("formid", "")).strip()
@@ -60,11 +83,18 @@ async def handle_call_tool(name: str, arguments: Dict[str, object]) -> List[Text
if timeout <= 0: if timeout <= 0:
raise ValueError("timeout must be greater than 0") raise ValueError("timeout must be greater than 0")
result = save( if name == "save_form":
formid=formid, result = save(
data_payload=_normalize_payload(arguments.get("data_payload")), formid=formid,
timeout=timeout, data_payload=_normalize_payload(arguments.get("data_payload"), "data_payload"),
) timeout=timeout,
)
else:
result = view(
formid=formid,
payload=_normalize_payload(arguments.get("payload"), "payload"),
timeout=timeout,
)
return [ return [
TextContent( TextContent(
type="text", type="text",

View File

@@ -0,0 +1,47 @@
{
"NeedUpDateFields": [],
"NeedReturnFields": [],
"IsDeleteEntry": "true",
"SubSystemId": "",
"IsVerifyBaseDataField": "false",
"IsEntryBatchFill": "true",
"ValidateFlag": "true",
"NumberSearch": "true",
"IsAutoAdjustField": "false",
"InterationFlags": "",
"IgnoreInterationFlag": "",
"IsControlPrecision": "false",
"ValidateRepeatJson": "false",
"Model": {
"FID": 0,
"FBillTypeID": {
"FNUMBER": "SKDLX02_SYS" "--其他业务收款单"
},
"FBillNo": "3", "--单据编号"
"FPAYORGID": {
"FNumber": "100" "--收款组织"
},
"FDATE": "2026-05-02", "--业务日期"
"FCONTACTUNITTYPE": "BD_Empinfo", "--往来单位类型"
"FCONTACTUNIT": {
"FNumber": "220804" "--往来单位"
},
"FPAYUNITTYPE": "BD_Empinfo", "--付款单位类型"
"FCURRENCYID": {
"FNumber": "PRE001" "--币别"
},
"FRECEIVEBILLENTRY": [
{
"FEntryID": 0,
"FRECAMOUNTFOR_E": 123.0, "--收款金额"
"FRECTOTALAMOUNTFOR": 123456.0, "--应收金额"
"FSETTLETYPEID": {
"FNumber": "JSFS01_SYS" "--结算方式 JSFS01_SYS 现金、JSFS04_SYS 电汇"
},
"FPURPOSEID": {
"FNumber": "SFKYT004" "--收款用途 SFKYT10 内部往来-费用往来、SFKYT004 借支还款"
}
}
]
}
}

View File

@@ -0,0 +1,631 @@
{
"Result": {
"ResponseStatus": {
"IsSuccess": true
},
"Result": {
"Id": 263989,
"BILLNo": "SKD00126001",
"DOCUMENTSTATUS": "C",
"FCreatorId_Id": 121428040,
"FCreatorId": {
"Id": 121428040,
"Name": "杨苏梅",
"UserAccount": "杨苏梅"
},
"APPROVERID_Id": 676841452,
"APPROVERID": {
"Id": 676841452,
"Name": "郑旭君",
"UserAccount": "郑旭君"
},
"FCreateDate": "2025-01-14T14:56:23.627",
"SETTLEORGID_Id": 1,
"SETTLEORGID": {
"Id": 1,
"MultiLanguageText": [
{
"PkId": 1,
"LocaleId": 2052,
"Name": "海圆圆集团"
},
{
"PkId": 100001,
"LocaleId": 1033,
"Name": "海圆圆集团"
},
{
"PkId": 100002,
"LocaleId": 3076,
"Name": "海圆圆集团"
}
],
"Name": [
{
"Key": 2052,
"Value": "海圆圆集团"
},
{
"Key": 1033,
"Value": "海圆圆集团"
},
{
"Key": 3076,
"Value": "海圆圆集团"
}
],
"Number": "100",
"FRadio": null,
"ParentOrg_Id": 1,
"ParentOrg": {
"Id": 1,
"Number": "100",
"MultiLanguageText": [
{
"PkId": 1,
"LocaleId": 2052,
"Name": "海圆圆集团"
},
{
"PkId": 100001,
"LocaleId": 1033,
"Name": "海圆圆集团"
},
{
"PkId": 100002,
"LocaleId": 3076,
"Name": "海圆圆集团"
}
],
"Name": [
{
"Key": 2052,
"Value": "海圆圆集团"
},
{
"Key": 1033,
"Value": "海圆圆集团"
},
{
"Key": 3076,
"Value": "海圆圆集团"
}
]
}
},
"ApproveDate": "2025-01-15T08:53:57.68",
"SALEORGID_Id": 0,
"SALEORGID": null,
"EXCHANGETYPE_Id": 1,
"EXCHANGETYPE": {
"Id": 1,
"msterID": 1,
"MultiLanguageText": [
{
"PkId": 1,
"LocaleId": 2052,
"Name": "固定汇率"
}
],
"Name": [
{
"Key": 2052,
"Value": "固定汇率"
}
],
"Number": "HLTX01_SYS",
"Digits": 4,
"ReverseDigits": 4
},
"RECEIVEAMOUNTFOR": 820000.0,
"MAINBOOKCURID_Id": 1,
"MAINBOOKCURID": {
"Id": 1,
"msterID": 1,
"MultiLanguageText": [
{
"PkId": 2,
"LocaleId": 2052,
"Name": "人民币"
}
],
"Name": [
{
"Key": 2052,
"Value": "人民币"
}
],
"Number": "PRE001",
"Sysmbol": "¥",
"PriceDigits": 6,
"AmountDigits": 2,
"IsShowCSymbol": true,
"FormatOrder": "1",
"RoundType": "1"
},
"RECEIVEAMOUNT": 820000.0,
"CURRENCYID_Id": 1,
"CURRENCYID": {
"Id": 1,
"msterID": 1,
"MultiLanguageText": [
{
"PkId": 2,
"LocaleId": 2052,
"Name": "人民币"
}
],
"Name": [
{
"Key": 2052,
"Value": "人民币"
}
],
"Number": "PRE001",
"Sysmbol": "¥",
"PriceDigits": 6,
"AmountDigits": 2,
"IsShowCSymbol": true,
"FormatOrder": "1",
"RoundType": "1"
},
"DATE": "2025-01-02T00:00:00",
"FModifyDate": "2025-01-15T08:53:55.51",
"ModifierId_Id": 676841452,
"ModifierId": {
"Id": 676841452,
"Name": "郑旭君",
"UserAccount": "郑旭君"
},
"EXCHANGERATE": 1.0,
"WRITTENOFFSTATUS": "A",
"SALEERID_Id": 0,
"SALEERID": null,
"SALEGROUPID_Id": 0,
"SALEGROUPID": null,
"SALEDEPTID_Id": 0,
"SALEDEPTID": null,
"BillTypeID_Id": "670b3608188c425a9f696d7ccb47843d",
"BillTypeID": {
"Id": "670b3608188c425a9f696d7ccb47843d",
"MultiLanguageText": [
{
"PkId": "557479e6741c42469036c5548c9d9407",
"LocaleId": 2052,
"Name": "其他业务收款单"
}
],
"Name": [
{
"Key": 2052,
"Value": "其他业务收款单"
}
],
"Number": "SKDLX02_SYS"
},
"REALRECAMOUNTFOR": 820000.0,
"REALRECAMOUNT": 820000.0,
"ACCOUNTSYSTEM_Id": 0,
"ACCOUNTSYSTEM": null,
"CancelDate": null,
"CancelStatus": "A",
"CancellerId_Id": 0,
"CancellerId": null,
"CONTACTUNITTYPE": "BD_Empinfo",
"CONTACTUNIT_Id": 279577322,
"CONTACTUNIT": {
"Id": 279577322,
"msterID": 279577322,
"MultiLanguageText": [
{
"PkId": 102684,
"LocaleId": 2052,
"Name": "吴喜燕"
}
],
"Name": [
{
"Key": 2052,
"Value": "吴喜燕"
}
],
"Number": "220804"
},
"PAYUNITTYPE": "BD_Empinfo",
"PAYUNIT_Id": 279577322,
"PAYUNIT": {
"Id": 279577322,
"msterID": 279577322,
"MultiLanguageText": [
{
"PkId": 102684,
"LocaleId": 2052,
"Name": "吴喜燕"
}
],
"Name": [
{
"Key": 2052,
"Value": "吴喜燕"
}
],
"Number": "220804"
},
"BUSINESSTYPE": "3",
"CreditCheckResult": "0",
"ISINIT": false,
"Department_Id": 0,
"Department": null,
"FPAYORGID_Id": 1,
"FPAYORGID": {
"Id": 1,
"MultiLanguageText": [
{
"PkId": 1,
"LocaleId": 2052,
"Name": "海圆圆集团"
},
{
"PkId": 100001,
"LocaleId": 1033,
"Name": "海圆圆集团"
},
{
"PkId": 100002,
"LocaleId": 3076,
"Name": "海圆圆集团"
}
],
"Name": [
{
"Key": 2052,
"Value": "海圆圆集团"
},
{
"Key": 1033,
"Value": "海圆圆集团"
},
{
"Key": 3076,
"Value": "海圆圆集团"
}
],
"Number": "100",
"FRadio": null,
"ParentOrg_Id": 1,
"ParentOrg": {
"Id": 1,
"Number": "100",
"MultiLanguageText": [
{
"PkId": 1,
"LocaleId": 2052,
"Name": "海圆圆集团"
},
{
"PkId": 100001,
"LocaleId": 1033,
"Name": "海圆圆集团"
},
{
"PkId": 100002,
"LocaleId": 3076,
"Name": "海圆圆集团"
}
],
"Name": [
{
"Key": 2052,
"Value": "海圆圆集团"
},
{
"Key": 1033,
"Value": "海圆圆集团"
},
{
"Key": 3076,
"Value": "海圆圆集团"
}
]
}
},
"FIsSameOrg": true,
"FSOURCESYSTEM": "0",
"FCASHSALE": false,
"SETTLECUR_Id": 1,
"SETTLECUR": {
"Id": 1,
"msterID": 1,
"MultiLanguageText": [
{
"PkId": 2,
"LocaleId": 2052,
"Name": "人民币"
}
],
"Name": [
{
"Key": 2052,
"Value": "人民币"
}
],
"Number": "PRE001",
"Sysmbol": "¥",
"PriceDigits": 6,
"AmountDigits": 2,
"IsShowCSymbol": true,
"FormatOrder": "1",
"RoundType": "1"
},
"SETTLERATE": 1.0,
"RECAMOUNTFOR": 820000.0,
"RECAMOUNT": 820000.0,
"ISB2C": false,
"WBSETTLENO": " ",
"IsWriteOff": false,
"MatchMethodID": 0,
"FScanPoint_Id": 0,
"FScanPoint": null,
"FKDPAYNO": " ",
"FREMARK": " ",
"FTHIRDBILLNO": " ",
"FSETTLEMAINBOOKID_Id": 1,
"FSETTLEMAINBOOKID": {
"Id": 1,
"msterID": 1,
"MultiLanguageText": [
{
"PkId": 2,
"LocaleId": 2052,
"Name": "人民币"
}
],
"Name": [
{
"Key": 2052,
"Value": "人民币"
}
],
"Number": "PRE001",
"Sysmbol": "¥",
"PriceDigits": 6,
"AmountDigits": 2,
"IsShowCSymbol": true,
"FormatOrder": "1",
"RoundType": "1"
},
"FSETTLEEXCHANGETYPE_Id": 1,
"FSETTLEEXCHANGETYPE": {
"Id": 1,
"msterID": 1,
"MultiLanguageText": [
{
"PkId": 1,
"LocaleId": 2052,
"Name": "固定汇率"
}
],
"Name": [
{
"Key": 2052,
"Value": "固定汇率"
}
],
"Number": "HLTX01_SYS",
"Digits": 4,
"ReverseDigits": 4
},
"FOUTCONTACTID_Id": 0,
"FOUTCONTACTID": null,
"FOUTCONTACTTYPE": " ",
"FGYACCOUNTWATERID": 0,
"FISCARRYRATE": false,
"PRESETBASE1_Id": 0,
"PRESETBASE1": null,
"PRESETBASE2_Id": 0,
"PRESETBASE2": null,
"FPRESETASSISTANT1_Id": " ",
"FPRESETASSISTANT1": null,
"FPRESETASSISTANT2_Id": " ",
"FPRESETASSISTANT2": null,
"FPRESETTEXT1": " ",
"FPRESETTEXT2": " ",
"FISFROMSALORDER": false,
"FVirIsSameAcctOrg": false,
"SourceBillNumber": " ",
"RECEIVEBILLENTRY": [
{
"Id": 288025,
"Seq": 1,
"SETTLETYPEID_Id": 1,
"SETTLETYPEID": {
"Id": 1,
"msterID": 1,
"MultiLanguageText": [
{
"PkId": 1,
"LocaleId": 2052,
"Name": "现金"
}
],
"Name": [
{
"Key": 2052,
"Value": "现金"
}
],
"Number": "JSFS01_SYS",
"SETTLECATEGORY": "1",
"TYPE": "1",
"FRECBILLTYPE": " ",
"FACCOUNTTYPE": " "
},
"SETTLERECAMOUNTFOR": 820000.0,
"SETTLEDISTAMOUNTFOR": 0.0,
"RECTOTALAMOUNTFOR": 820000.0,
"SETTLERECAMOUNT": 820000.0,
"SETTLEDISTAMOUNT": 0.0,
"RECTOTALAMOUNT": 820000.0,
"WRITTENOFFSTATUS": "A",
"WRITTENOFFAMOUNTFOR": 0.0,
"COMMENT": "收到吴喜燕归还借支款",
"OPPOSITEBANKACCOUNT": " ",
"OPPOSITECCOUNTNAME": " ",
"RECEIVEITEM": " ",
"HANDLINGCHARGEFOR": 0.0,
"HANDLINGCHARGE": 0.0,
"REALRECAMOUNTFOR": 820000.0,
"FREALRECAMOUNT": 820000.0,
"ASSTOTALAMOUNTFOR": 0.0,
"RECEIVEITEMTYPE": " ",
"SaleOrderID": 0,
"ACCOUNTID_Id": 0,
"ACCOUNTID": null,
"OVERUNDERAMOUNTFOR": 0.0,
"OPPOSITEBANKNAME": " ",
"SETTLENO": " ",
"BLEND": false,
"PURPOSEID_Id": 266819,
"PURPOSEID": {
"Id": 266819,
"MultiLanguageText": [
{
"PkId": 100005,
"LocaleId": 2052,
"Name": "借支还款"
}
],
"Name": [
{
"Key": 2052,
"Value": "借支还款"
}
],
"Number": "SFKYT004",
"RECPAYTYPE": "0",
"BUSINESSTYPE": "3",
"FINMANEGEMENT": true,
"FPAYMENTADVANCE": false
},
"OVERUNDERAMOUNT": 0.0,
"FINNERACCOUNTID_Id": 0,
"FINNERACCOUNTID": null,
"ReFundAmount": 0.0,
"CashAccount_Id": 100402,
"CashAccount": {
"Id": 100402,
"msterID": 100402,
"MultiLanguageText": [
{
"PkId": 100001,
"LocaleId": 2052,
"Name": "海圆圆"
}
],
"Name": [
{
"Key": 2052,
"Value": "海圆圆"
}
],
"Number": "海圆圆"
},
"RECAMOUNTFOR_E": 820000.0,
"RECAMOUNT_E": 820000.0,
"FPOSTDATE": "2025-01-02T00:00:00",
"FISPOST": true,
"FMATERIALID_Id": 0,
"FMATERIALID": null,
"FSALEORDERNO": " ",
"FMATERIALSEQ": 0,
"FORDERENTRYID": 0,
"TOPAYMENTAMOUNTFOR": 0.0,
"FWRITTENOFFAMOUNT": 0.0,
"FNOTVERIFICATEAMOUNT": 820000.0,
"FPRICEUNITID_Id": 0,
"FPRICEUNITID": null,
"FPrice": 0.0,
"FQty": 0.0,
"COSTID_Id": 0,
"COSTID": null,
"COSTDEPARTMENTID_Id": 0,
"COSTDEPARTMENTID": null,
"FLINKROWID": " ",
"FSALEORDERBASE_Id": 0,
"FSALEORDERBASE": null,
"FRelateReFundAmount": 0.0,
"ENTRYTAXRATE": 0.0,
"TAXAMOUNTFOR": 0.0,
"TAXAMOUNT": 0.0,
"RECNOTAXAMOUNTFOR": 820000.0,
"RECNOTAXAMOUNT": 820000.0,
"WRITTENOFFTAXAMOUNTFOR": 0.0,
"FWRITTENOFFTAXAMOUNT": 0.0,
"FSALEORDERNUMBER": null,
"AR_ASSSALESORDER": []
}
],
"RECEIVEBILLSRCENTRY": [
{
"Id": 0,
"Seq": 1,
"SRCBILLID": 0,
"AFTTAXTOTALAMOUNT": 0.0,
"SRCSETTLETYPEID_Id": 0,
"SRCSETTLETYPEID": null,
"SRCCURRENCYID_Id": 0,
"SRCCURRENCYID": null,
"EXPIRY": null,
"PLANRECAMOUNT": 0.0,
"REALRECAMOUNT": 0.0,
"SRCBILLTYPEID": "",
"SRCBILLNO": null,
"SRCSEQ": 0,
"SRCROWID": 0,
"ORDERBILLNO": null,
"FSRCMATERIALID_Id": 0,
"FSRCMATERIALID": null,
"FSRCMATERIALSEQ": 0,
"FSRCORDERENTRYID": 0,
"FSETTLEAMOUNT": 0.0,
"FREALRECAMOUNTFOR": 0.0,
"FSRCPRICEUNITID_Id": 0,
"FSRCPRICEUNITID": null,
"FSRCPRICE": 0.0,
"FSRCQTY": 0.0,
"SRCCOSTID_Id": 0,
"SRCCOSTID": null,
"SRCCOSTDEPARTMENTID_Id": 0,
"SRCCOSTDEPARTMENTID": null,
"FSRCLINKROWID": null,
"SRCREMARK": null,
"FASSORDERAMOUNT": 0.0,
"FSRCPURPOSEID_Id": 0,
"FSRCPURPOSEID": null,
"FRECEIVEBILLSRCENTRY_Link": []
}
],
"RECEIVEBILLREC": [
{
"Id": 0,
"Seq": 1,
"BILLID_Id": 0,
"BILLID": null,
"PARLEFTAMOUNTFOR": 0.0,
"USEDAMOUNTFOR": 0.0,
"FInnerAccountID_B_Id": 0,
"FInnerAccountID_B": null,
"FTempOrgId_Id": 0,
"FTempOrgId": null,
"FBPBILLNUMBER": null,
"BILLPARAMOUNT": 0.0,
"PARLEFTAMOUNTSTD": 0.0,
"USEDAMOUNTSTD": 0.0,
"FBPBILLPARAMOUNT": 0.0
}
],
"BILLSKDRECEIVABLEENTRY": [],
"BOS_ConvertTakeDataInfo": null
}
}
}

408
交接文档.md Normal file
View File

@@ -0,0 +1,408 @@
# lzwcai-mcp 项目交接文档
本文档面向接手本仓库的技术人员,重点说明整体架构、各子项目职责、运行方式与配置要点,便于快速上手维护与扩展。
## 一、项目整体概览
- 仓库名称:`lzwcai-mcp`
- 项目类型:一组基于 MCPModel Context Protocol的独立服务每个服务都是一个「技能包」通过 `stdio` 与上层 MCP Agent 通信。
- 主要能力:
- Feishu/Lark 卡片发送、图片上传、OKR 查询等企业协同能力(`lzwcai_lark_mcp`
- 文件与 URL 的 Base64 编解码、临时文件处理、MinIO 上传等通用文件工具(`file_tools`
- 金蝶云星空单据保存与查询能力,封装 DynamicFormService.Save / View 接口(`k3cloud_mcp`
- 访客信息查询与登记、门禁控制(`terminal_dhr_mcp`
- Temi 轮足机器人导航与业务动作控制(`terminal_temi_mcp`
- Go2 机器人运动控制、避障、服务管理等(`terminal_go2_mcp`
各子项目都是独立的 Python 包,拥有各自的 `pyproject.toml` 与入口脚本,通过 JSON 配置文件被 MCP Agent 挂载为工具服务器。
## 二、代码结构概览
仓库根目录主要结构:
- `file_tools/`
- `file_tools/`:文件工具 MCP 服务器实现(入口 `file_tools.main`)。
- `pyproject.toml`:使用 `hatchling` 构建,脚本名 `lzwcai-file-tools-mcp`
- `k3cloud_mcp/`
- `k3cloud_mcp/`:金蝶云星空 MCP 服务器实现。
- `mcp-server.json`MCP Agent 挂载示例,包含 `k3cloud-mcp` 的启动方式与环境变量示例。
- `pyproject.toml`:使用 `hatchling` 构建,脚本名 `k3cloud-mcp`
- `lzwcai_lark_mcp/`
- `lzwcai_lark_mcp/`Lark 相关 MCP 服务器实现(入口 `lzwcai_lark_mcp.main`)。
- `pyproject.toml`:使用 `hatchling` 构建,脚本名 `lzwcai-lark-mcp`
- `lzwcai_temi_mcp/`
- `lzwcai_temi_mcp/`:另一个 Temi 机器人相关 MCP 服务(目前主要用到 `main.py``mcp_mqtt.py``nav_server.py`)。
- `mcp-server.json`MCP Agent 挂载示例。
- `pyproject.toml`:使用 `hatchling` 构建,脚本名 `lzwcai_temi_mcp`
- `terminal_dhr_mcp/`
- `terminal_dhr_mcp/`:访客系统 + 门禁控制 MCP 服务器。
- `.env`:示例环境变量。
- `pyproject.toml`:基于 `setuptools` 的构建配置,脚本名 `terminal-dhr-mcp`
- `terminal_go2_mcp/`
- `terminal_go2_mcp/`Go2 机器人 MCP 服务器。
- `pyproject.toml`:使用 `hatchling` 构建,脚本名 `terminal_go2_mcp`
- `terminal_temi_mcp/`
- `terminal_temi_mcp/`Temi 轮足机器人 MCP 服务器。
- `pyproject.toml`:使用 `hatchling` 构建,脚本名 `terminal_temi_mcp`
- 根目录其他文件:
- `README.md`:顶层简介与部分子项目说明。
- `mcp.json`:示例 MCP 服务器配置(目前包含一个 JS 实现的 `lark-mcp` 示例,与本仓库 Python 版本的 Lark MCP 可共存)。
- `LICENSE``.gitignore` 等。
## 三、运行环境与基础依赖
总体建议:
- Python 版本:优先使用 **3.10+**
- `terminal-dhr-mcp` 声明支持 3.8+,但其他项目为 3.10+,统一到 3.10 可以减少环境碎片。
- 依赖管理:建议使用虚拟环境(`venv``uv` 等)分别在各子项目目录下执行安装。
核心依赖:
- 统一:
- `mcp[cli]``mcp`MCP 协议实现。
- `python-dotenv`:从 `.env` 加载配置。
- Lark 相关(`lzwcai_lark_mcp`
- `requests`:调用 Feishu/Lark HTTP API。
- `openpyxl`:从 Excel 中解析 `image_key` 及图片路径。
- 文件工具(`file_tools`
- `httpx`:下载远程文件。
- `minio`:将文件上传到 MinIO 对象存储。
- `openpyxl`Excel 图片解析。
- 金蝶云星空(`k3cloud_mcp`
- `requests`:调用 K3Cloud Web API。
- 访客/门禁(`terminal_dhr_mcp`
- `httpx``pydantic``python-dotenv` 等,用于 HTTP 调用与配置。
- 机器人相关(`terminal_temi_mcp``terminal_go2_mcp`
- `fastapi``uvicorn`(部分场景)、`paho-mqtt``pydantic``requests`
## 四、各子项目详细说明
### 4.1 Lark MCP`lzwcai_lark_mcp`
- 入口:
- 包:`lzwcai_lark_mcp`
- 入口函数:`lzwcai_lark_mcp.main:main`
- MCP Server 名称:`lzwcai-mcpskills-lark-mcp`
- 核心文件:
- `main.py`MCP server 启动逻辑,负责:
- 初始化 `Server`
- 通过 `_request_token` 根据 `app_id` / `app_secret` 获取 Lark `tenant_access_token`,带过期时间缓存。
- 注册 `list_tools` / `call_tool`,在调用工具前自动确保 token 有效。
- `config.py`
- 从环境变量读取:
- `app_id`
- `app_secret`
- `tools.py`
- 定义对外暴露的 `Tool` 列表。
- 实现具体业务函数,如:
- 图片上传:`upload_image_by_url` / `upload_image_by_file`
- Excel 定位图片源:`_resolve_image_source_from_excel`
- OKR 查询:`get_user_okr_list``batch_get_okr`
- 资产确认/复核/陌生人处理/反馈等多种交互卡片:`send_confirmation_card``send_review_card``send_stranger_card``send_feedback_card`
- 入库通知卡片:`send_notion_card` / `send_card_message`
- `handle_call_tool`:根据 `name` 分发到对应的 Python 函数,处理参数校验、错误返回等。
- 外部依赖与接口:
- Feishu/Lark Open API
- 认证:`/open-apis/auth/v3/tenant_access_token/internal`
- 消息/图片:`/open-apis/im/v1/images``/open-apis/im/v1/messages`
- 通讯录:`/open-apis/contact/v3/users/{user_id}`
- OKR`/open-apis/okr/v1/users/{user_id}/okrs``/open-apis/okr/v1/okrs/batch_get`
- 配置要点:
- 必需环境变量:
- `app_id`Lark 应用的 app id。
- `app_secret`Lark 应用的 app secret。
- 可选:`LOG_LEVEL`(通过 logging.basicConfig 读取)。
### 4.2 文件工具 MCP`file_tools`
- 入口:
- 包:`file_tools`
- 入口函数:`file_tools.main:main`
- MCP Server 名称:`lzwcai-mcpskills-file-tools-mcp`
- 核心文件:
- `main.py`
- 初始化 MCP `Server`,注册 `list_tools` / `call_tool`
- `tools.py`
- 工具列表:
- `file_to_json`:将文件对象或本地路径转成 JSON 结构(带 base64 内容)。
- `file_to_data_uri`:将文件转为 `data URI`
- `file_path_to_data_uri`:本地文件路径 -> `data URI`
- `url_to_data_uri`URL 下载并转为 `data URI`
- `url_to_temp_file`URL 下载为本地临时文件,返回路径。
- `excel_image_key_to_temp_file`:从 Excel 中按 `image_key` 找到图片并写入临时文件。
- `upload_file_to_minio`:将文件上传到 MinIO返回访问 URL。
- 工具内部大量使用 `Path``tempfile``httpx``minio` 等。
- 配置要点:
- MinIO 相关配置位于 `config.py``minio_endpoint``minio_access_key``minio_secret_key`),通过环境变量读取。
- 当前 `upload_file_to_minio` 里的 bucket 名称固定为 `lzwcai`(写在 `tools.py`),如需多环境隔离建议改为可配置项。
### 4.3 金蝶云星空 MCP`k3cloud_mcp`
- 入口:
- 包:`k3cloud_mcp`
- 入口函数:`k3cloud_mcp.main:main`
- MCP Server 名称:`k3cloud-mcp`
- 核心文件:
- `main.py`
- 初始化 MCP `Server`
- 注册 `list_tools` / `call_tool`
- 运行 `stdio_server`,供上层 MCP Agent 调用。
- `tools.py`
- 当前暴露两个工具:`save_form``view_form`
- `save_form` 参数:
- `formid`:业务对象表单 Id例如 `SAL_QUOTATION`
- `data_payload`:可传 JSON 对象或 JSON 字符串。
- `timeout`HTTP 超时时间,默认 30 秒。
- `view_form` 参数:
- `formid`:业务对象表单 Id例如 `SAL_QUOTATION`
- `payload`:可传 JSON 对象或 JSON 字符串。
- `timeout`HTTP 超时时间,默认 30 秒。
- `handle_call_tool()` 负责做参数校验、分发到底层 `save()` / `view()`,并把响应序列化为文本返回。
- `client.py`
- 封装 `save(formid, data_payload, timeout=30)``view(formid, payload, timeout=30)`
- 分别调用 `DynamicFormService.Save.common.kdsvc``DynamicFormService.View.common.kdsvc` 接口。
- 会自动将对象型 payload 转为 JSON 字符串后再提交。
- `signing.py`
- 负责生成 K3Cloud 所需签名头。
- 包含 `APP_ID` 拆分、`APP_SECRET` 解码、HMAC 签名等逻辑。
- `config.py`
- 从环境变量读取 `K3CLOUD_*` 配置。
- 缺少必填项时会在导入阶段直接抛出异常。
- `demo_save.py` / `demo_save.json`
- 用于本地直接测试 `save()` 函数。
- `demo_view.py` / `demo_view.json`
- 用于本地直接测试 `view()` 函数。
- 外部依赖与接口:
- 金蝶云星空 Web API
- 保存接口:`/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Save.common.kdsvc`
- 查询接口:`/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.View.common.kdsvc`
- 配置要点:
- 必需环境变量:
- `K3CLOUD_BASE_URL`K3Cloud 站点根地址。
- `K3CLOUD_ACCT_ID`:账套标识。
- `K3CLOUD_APP_ID`:应用标识。
- `K3CLOUD_USERNAME`:调用用户名。
- `K3CLOUD_APP_SECRET`:应用密钥。
- 可选环境变量:
- `K3CLOUD_LCID`:语言代码,默认 `2052`
- `K3CLOUD_ORG_NUM`:组织编号,默认 `0`
- `LOG_LEVEL`:日志级别。
- 本地测试方式:
- 保存接口联调:
- `python k3cloud_mcp/demo_save.py <formid> --payload k3cloud_mcp/demo_save.json`
- 查询接口联调:
- `python k3cloud_mcp/demo_view.py <formid> --payload k3cloud_mcp/demo_view.json`
- 当前测试脚本会优先使用当前进程环境变量;若未设置,则补读子项目目录下 `mcp-server.json``k3cloud-mcp.env` 的配置,便于本地快速验证。
### 4.4 访客系统 + 门禁 MCP`terminal_dhr_mcp`
- 入口:
- 包:`terminal_dhr_mcp`
- 入口函数:`terminal_dhr_mcp.main:main`
- MCP Server 名称:`terminal-dhr-mcp`
- 核心文件:
- `main.py`
- 定义 `DhrMCPServer` 类,注册 `list_tools` / `call_tool`,调用 `tools.py` 中的逻辑。
- `tools.py`
- 工具列表包括:
- `query_visitor`:根据 `user_id` 查询访客预约记录。
- `register_visitor`:登记新访客。
- `control_door`:控制门禁开关。
- `get_door_status`:查询门禁状态。
- `config.py`
- 关键配置:
- `EMPLOYEE_ID`:从环境变量 `employeeId` 读取,用于请求访客系统。
- `BASE_URL`:访客系统服务地址。
- `VISITOR_QUERY_ENDPOINT``VISITOR_REGISTER_ENDPOINT`:固定后缀路径。
- 门禁相关:
- `DOOR_API_ENDPOINT`
- `DOOR_ENTERPRISE_ID`
- `DOOR_DEFAULT_ENTITY_ID`
- `LOG_LEVEL`、若干长度限制常量等。
- 典型环境配置:
- 可在 `terminal_dhr_mcp/.env` 中配置:
- `employeeId`
- `BASE_URL`
- `DOOR_ENTERPRISE_ID`
- `DOOR_DEFAULT_ENTITY_ID`
- `LOG_LEVEL`
### 4.5 Temi 轮足机器人 MCP`terminal_temi_mcp`
- 入口:
- 包:`terminal_temi_mcp`
- 入口函数:`terminal_temi_mcp.main:main`
- MCP Server 名称:`terminal_temi_mcp`
- 核心文件:
- `main.py`
- 定义 `NavServer`,通过 `mcp_mqtt.get_mcpmqtt_handler()` 获取 MQTT 客户端。
- 封装业务命令:
- `nav_to`:导航到指定地点。
- `speak`:语音播报。
- `reception`:迎宾。
- `notification`(目前工具层注释掉)。
- `repose`:重新定位。
- `delivery`:配送任务。
- `patrol`:巡逻。
- 若干人脸识别 / 扫码相关函数目前未对外暴露。
- `list_tools` 中暴露的工具包括 `nav_to``speak``reception``repose``delivery``patrol` 等,对输入参数做了详细描述。
- `call_tool` 根据工具名分发到 `NavServer` 方法,并做参数校验与错误转换。
- `mcp_mqtt.py`
- 封装 MQTT Broker 连接、客户端初始化与复用。
- 配置要点:
- `.env`(参考 README
- `LOG_LEVEL`
- `MQTT_BROKER_ADDRESS`
- `MQTT_PORT`
- `MQTT_USERNAME`
- `MQTT_PASSWORD`
- MCP Agent 配置文件(示例在子项目 README 中)会把 `employeeId``userId` 作为环境变量注入。
### 4.6 Go2 机器人 MCP`terminal_go2_mcp`
- 入口:
- 包:`terminal_go2_mcp`
- 入口函数:`terminal_go2_mcp.main:main`
- MCP Server 名称:`terminal_go2_mcp`
- 核心文件:
- `main.py`
- `TerminalGo2McpServer` 提供对 Go2 机器人的封装:
- 低层统一接口 `pubCmd(type, cmd, params)` 通过 MQTT 发布到 `unitree/go2/cmd`
- 运动相关:`custom_action``ContinuousMove``move``euler``set_speed_level``pose` 等。
- 避障相关:`obstacle_avoidance_switch``obstacle_move`
- 导航相关:`start_mapping``end_mapping``initialize_pose``pose_navigation``pause_navigation``resume_navigation` 等(部分在工具层已注释)。
- 充电:`start_recharge``stop_recharge`
- 服务管理:`list_services``switch_service``set_report_freq`
- `serve()` 中:
- 注册工具列表Tool 定义),描述参数与取值范围。
-`call_tool` 中根据 `name` 分发到对应协程函数。
- `mcp_mqtt.py`
- 和 Temi 类似,管理 MQTT 客户端。
- 配置要点:
- MQTT 连接参数在 `mcp_mqtt.py``.env` 中通过环境变量读取(具体字段请在接手时查看该文件)。
### 4.7 Temi 机器人 MCP旧版`lzwcai_temi_mcp`
- 入口:
- 包:`lzwcai_temi_mcp`
- 入口函数:`lzwcai_temi_mcp.main:main`
- MCP Server 名称:`lzwcai_temi_mcp`
- 核心文件:
- `main.py`
- 当前暴露工具包含 `recharge``terminate``goto``speak``reception``notification``repose``patrol``dance`
- `mcp_mqtt.py`
- 管理 MQTT 连接与消息发布。
- `nav_server.py`
- 封装导航与动作编排逻辑。
- 维护建议:
- 仓库内同时存在 `terminal_temi_mcp``lzwcai_temi_mcp` 两套 Temi 服务,接手时建议先和业务方确认实际在线使用的是哪一套,避免重复维护。
## 五、安装与运行方式(面向开发/调试)
以下步骤均建议在虚拟环境中执行。
1. 克隆仓库:
```bash
git clone <repo-url>
cd lzwcai-mcp
```
2. 进入某个子项目目录并安装:
```bash
cd terminal_dhr_mcp
pip install -e .[dev]
# 或仅运行所需:
pip install -e .
```
其他子项目类似:
- `cd lzwcai_lark_mcp && pip install -e .`
- `cd file_tools && pip install -e .`
- `cd k3cloud_mcp && pip install -e .`
- `cd lzwcai_temi_mcp && pip install -e .`
- `cd terminal_temi_mcp && pip install -e .`
- `cd terminal_go2_mcp && pip install -e .`
3. 启动某个 MCP 服务器(本地调试):
```bash
# Lark
lzwcai-lark-mcp
# 文件工具
lzwcai-file-tools-mcp
# 金蝶云星空
k3cloud-mcp
# 访客系统 + 门禁
terminal-dhr-mcp
# Temi 机器人(旧版)
lzwcai_temi_mcp
# Temi 机器人
terminal_temi_mcp
# Go2 机器人
terminal_go2_mcp
```
4. 与 MCP Agent 集成:
- 顶层或子项目中提供示例配置文件,如:
- `lzwcai_lark_mcp/mcp-server.json`
- `terminal_dhr_mcp/mcp-server-dhr.json`
- `terminal_temi_mcp/mcp-server-temi.json`
- `terminal_go2_mcp/mcp-server-go2.json`
- `lzwcai_temi_mcp/mcp-server.json`
- `file_tools/mcp-server-file-tools.json`
- `k3cloud_mcp/mcp-server.json`
- 在上层 Agent 的配置(例如 IDE 插件或自研 Agent中引入这些 JSON 即可通过 `stdio` 调起对应服务器。
## 六、配置与敏感信息管理
- 所有敏感信息token、密钥、账号密码均通过环境变量或 `.env` 文件注入,不应直接写入代码或仓库。
- 当前仓库中的部分 `mcp-server*.json` 已包含真实或准生产敏感配置示例;对外共享仓库前建议先做脱敏或改成占位符。
- 推荐做法:
- 针对每个子项目,在其根目录创建 `.env`,仅存放本项目相关配置。
- 在部署环境中,通过进程管理器(如 systemd、Docker、K8s把 `.env` 或环境变量挂载进去。
- 接手时建议重点确认:
- Lark 应用的 `app_id`/`app_secret` 是否有效,是否具备所需 API 权限。
- K3Cloud 的 `BASE_URL`、账套、应用标识、用户名、密钥是否仍然有效,签名规则是否与现网一致。
- 访客系统 / 门禁接口 `BASE_URL`、企业 ID、门禁实体 ID 是否与现网一致。
- MQTT Broker 地址、账号密码是否与机器人当前网络环境匹配。
- MinIO 的 endpoint、access key、secret key、bucket 名称是否存在且有权限。
## 七、开发规范与注意事项
- 编码风格:
- Python 代码整体接近 PEP 8 风格。
- 部分子项目(如 `terminal_dhr_mcp`)在 `pyproject.toml` 中配置了 `black`、`isort`、`ruff`、`mypy` 等工具,对应规则可作为编码参考。
- 日志:
- 所有服务器都使用 `logging.basicConfig` 统一输出日志,日志级别可通过 `LOG_LEVEL` 环境变量控制。
- 出错时通常会在日志中打印详细异常,并向 MCP Agent 返回友好文本。
- 错误处理:
- 多数工具会对必需参数做显式校验,缺失时抛出 `ValueError` 或返回带错误信息的字符串。
- 对外 HTTP/MQTT 调用失败时,会记录错误日志,并抛出异常或返回失败说明。
- 工具定义:
- 工具使用 `mcp.types.Tool` 定义,`inputSchema` 为 JSON Schema 格式,方便上层 Agent 自动生成表单或进行参数校验。
- 若新增工具,请保持命名风格一致,并且在 `list_tools` 与 `call_tool` 实现中同步更新。
## 八、后续扩展建议
- 如需扩展新的硬件或业务系统,建议:
- 新增独立子项目目录(与现有结构一致),保持「一个 MCP 服务一个 Python 包」的模式。
- 优先复用现有 MQTT/HTTP 封装模式与错误处理风格。
- 在子项目内补充 README简要说明工具列表与配置方式并在顶层 `README.md` 中更新项目清单。
## 九、已知问题与接手优先项
- `file_tools/config.py` 默认从 `mcp-server-file-tools.json` 读取配置时,查找的 server key 是 `lzwcai-mcpskills-file-tools-mcp`,而当前示例文件使用的是 `lzwcai-file-tools-mcp`,可能导致本地自动注入环境变量失效。
- `terminal_temi_mcp` 与 `lzwcai_temi_mcp` 为并行存在的两套 Temi MCP 服务,能力与工具名不完全一致,建议先统一服务边界再继续演进。
如在接手过程中遇到不清楚的业务规则(例如具体资产变动业务、访客流程、机器人业务场景),建议先与业务方确认需求,再在对应子项目中补充或调整工具逻辑。