From 0a308726a693d0516e0d3adad3075756b2f72b4c Mon Sep 17 00:00:00 2001 From: Sucan <632190820@qq.com> Date: Wed, 11 Feb 2026 18:57:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E9=A3=9E=E4=B9=A6?= =?UTF-8?q?=E5=92=8C=E6=96=87=E4=BB=B6=E5=B7=A5=E5=85=B7MCP=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=B9=B6=E4=BC=98=E5=8C=96=E6=9C=BA=E5=99=A8=E4=BA=BA?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增飞书MCP服务,包含图片上传和消息卡片发送功能 - 新增文件工具MCP服务,提供文件转JSON和data URI功能 - 更新机器人MCP服务配置,移除环境变量硬编码 - 升级机器人版本至0.1.28并优化迎宾功能参数 - 添加.gitignore和项目配置文件 --- .gitignore | 1 + file_tools/README.md | 117 ++++++++ file_tools/file_tools/__init__.py | 1 + file_tools/file_tools/main.py | 47 +++ file_tools/file_tools/tools.py | 256 ++++++++++++++++ file_tools/mcp-server-file-tools.json | 14 + file_tools/pyproject.toml | 20 ++ file_tools/test_tools.py | 67 +++++ lzwcai_lark_mcp/lzwcai_lark_mcp/__init__.py | 1 + lzwcai_lark_mcp/lzwcai_lark_mcp/config.py | 9 + lzwcai_lark_mcp/lzwcai_lark_mcp/main.py | 85 ++++++ lzwcai_lark_mcp/lzwcai_lark_mcp/tools.py | 280 ++++++++++++++++++ lzwcai_lark_mcp/mcp-server.json | 17 ++ lzwcai_lark_mcp/pyproject.toml | 20 ++ lzwcai_lark_mcp/test.py | 38 +++ .../terminal_temi_mcp-0.1.21-py3-none-any.whl | Bin 7227 -> 0 bytes .../dist/terminal_temi_mcp-0.1.21.tar.gz | Bin 6933 -> 0 bytes terminal_temi_mcp/mcp-server-temi.json | 8 +- terminal_temi_mcp/pyproject.toml | 2 +- terminal_temi_mcp/terminal_temi_mcp/main.py | 44 ++- 20 files changed, 1005 insertions(+), 22 deletions(-) create mode 100644 .gitignore create mode 100644 file_tools/README.md create mode 100644 file_tools/file_tools/__init__.py create mode 100644 file_tools/file_tools/main.py create mode 100644 file_tools/file_tools/tools.py create mode 100644 file_tools/mcp-server-file-tools.json create mode 100644 file_tools/pyproject.toml create mode 100644 file_tools/test_tools.py create mode 100644 lzwcai_lark_mcp/lzwcai_lark_mcp/__init__.py create mode 100644 lzwcai_lark_mcp/lzwcai_lark_mcp/config.py create mode 100644 lzwcai_lark_mcp/lzwcai_lark_mcp/main.py create mode 100644 lzwcai_lark_mcp/lzwcai_lark_mcp/tools.py create mode 100644 lzwcai_lark_mcp/mcp-server.json create mode 100644 lzwcai_lark_mcp/pyproject.toml create mode 100644 lzwcai_lark_mcp/test.py delete mode 100644 terminal_temi_mcp/dist/terminal_temi_mcp-0.1.21-py3-none-any.whl delete mode 100644 terminal_temi_mcp/dist/terminal_temi_mcp-0.1.21.tar.gz diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba0430d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ \ No newline at end of file diff --git a/file_tools/README.md b/file_tools/README.md new file mode 100644 index 0000000..dc6d8ee --- /dev/null +++ b/file_tools/README.md @@ -0,0 +1,117 @@ +# File Tools MCP 技能包 + +该技能包提供文件与 URL 的 Base64 处理能力,主要用于将文件内容转换为 JSON 字符串或 data URI,供 MCP Agent 直接调用。 + +## 功能 + +- 文件对象转 JSON 字符串 +- 文件对象转 data URI(Base64) +- 文件路径转 data URI(Base64) +- 文件 URL 转 data URI(Base64) + +## 工具列表 + +### 1. file_to_json + +将文件对象转储为 JSON 字符串。 + +**入参** + +```json +{ + "file": { + "name": "example.txt", + "type": "text/plain", + "size": 123, + "last_modified": 1700000000, + "content_base64": "SGVsbG8=" + } +} +``` + +也可以使用 `file_path`: + +```json +{ + "file_path": "e:\\lzwcai-mcp\\README.md" +} +``` + +### 2. file_to_data_uri + +将文件对象转换为 data URI。 + +**入参** + +```json +{ + "file": { + "name": "hello.txt", + "content_base64": "SGVsbG8=" + }, + "type": "text/plain" +} +``` + +### 3. file_path_to_data_uri + +将本地文件路径转换为 data URI。 + +**入参** + +```json +{ + "file_path": "e:\\lzwcai-mcp\\README.md", + "type": "text/plain" +} +``` + +### 4. url_to_data_uri + +将文件 URL 转换为 data URI。 + +**入参** + +```json +{ + "url": "https://example.com/image.png" +} +``` + +## 启动方式 + +### uvx + +```json +{ + "mcpServers": { + "file-tools-mcp": { + "command": "uvx", + "type": "stdio", + "args": [ + "lzwcai-mcpskills-file-tools-mcp" + ], + "env": {} + } + } +} +``` + +### python + +```json +{ + "mcpServers": { + "file-tools-mcp": { + "disabled": false, + "type": "stdio", + "timeout": 30, + "command": "python", + "args": [ + "-m", + "file_tools.main" + ] + } + } +} +``` diff --git a/file_tools/file_tools/__init__.py b/file_tools/file_tools/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/file_tools/file_tools/__init__.py @@ -0,0 +1 @@ + diff --git a/file_tools/file_tools/main.py b/file_tools/file_tools/main.py new file mode 100644 index 0000000..2e2949c --- /dev/null +++ b/file_tools/file_tools/main.py @@ -0,0 +1,47 @@ +import asyncio +import logging + +from mcp.server import Server +from mcp.server.stdio import stdio_server + +from .tools import handle_call_tool, tools + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +class FileToolsServer: + def __init__(self) -> None: + self.server: Server = Server("lzwcai-mcpskills-file-tools-mcp") + self._register_handlers() + + def _register_handlers(self) -> None: + @self.server.list_tools() + async def list_tools(): + return tools + + @self.server.call_tool() + async def call_tool(name: str, arguments: dict): + logger.info("Received tool call request: %s", name) + return await handle_call_tool(name, arguments) + + +async def _main() -> None: + server = FileToolsServer() + async with stdio_server() as (read_stream, write_stream): + await server.server.run( + read_stream, + write_stream, + server.server.create_initialization_options() + ) + + +def main() -> None: + asyncio.run(_main()) + + +if __name__ == "__main__": + main() diff --git a/file_tools/file_tools/tools.py b/file_tools/file_tools/tools.py new file mode 100644 index 0000000..aa63b2d --- /dev/null +++ b/file_tools/file_tools/tools.py @@ -0,0 +1,256 @@ +import base64 +import json +import mimetypes +import tempfile +from pathlib import Path +from urllib.parse import urlparse +from typing import Any, Dict, List, Optional, Tuple + +import httpx +from mcp.types import TextContent, Tool + +tools: List[Tool] = [ + Tool( + name="file_to_json", + description="将文件对象转储为 JSON 字符串", + inputSchema={ + "type": "object", + "properties": { + "file": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "type": {"type": "string"}, + "size": {"type": "integer"}, + "last_modified": {"type": "integer"}, + "content_base64": {"type": "string"} + }, + "required": ["name"] + }, + "file_path": {"type": "string"} + }, + "required": [] + } + ), + Tool( + name="file_to_data_uri", + description="将文件转换为 data URI(Base64 编码)", + inputSchema={ + "type": "object", + "properties": { + "file": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "type": {"type": "string"}, + "size": {"type": "integer"}, + "last_modified": {"type": "integer"}, + "content_base64": {"type": "string"} + }, + "required": ["name", "content_base64"] + }, + "type": {"type": "string"}, + "file_path": {"type": "string"} + }, + "required": [] + } + ), + Tool( + name="file_path_to_data_uri", + description="将文件路径转换为 data URI(Base64 编码)", + inputSchema={ + "type": "object", + "properties": { + "file_path": {"type": "string"}, + "type": {"type": "string"} + }, + "required": ["file_path"] + } + ), + Tool( + name="url_to_data_uri", + description="将文件 URL 转换为 data URI(Base64 编码)", + inputSchema={ + "type": "object", + "properties": { + "url": {"type": "string"} + }, + "required": ["url"] + } + ), + Tool( + name="url_to_temp_file", + description="将文件 URL 下载为临时文件并返回路径", + inputSchema={ + "type": "object", + "properties": { + "url": {"type": "string"}, + "file_path": {"type": "string"} + }, + "required": ["url"] + } + ) +] + + +def _guess_mime_type(name: Optional[str]) -> str: + if not name: + return "application/octet-stream" + mime_type, _ = mimetypes.guess_type(name) + return mime_type or "application/octet-stream" + + +def _is_url(value: Optional[str]) -> bool: + if not value: + return False + parsed = urlparse(value) + return parsed.scheme in {"http", "https"} and bool(parsed.netloc) + + +def _decode_base64(content_base64: str) -> bytes: + try: + return base64.b64decode(content_base64, validate=True) + except Exception: + return base64.b64decode(content_base64) + + +def _build_data_uri(data: bytes, mime_type: str) -> str: + encoded = base64.b64encode(data).decode("ascii") + return f"data:{mime_type};base64,{encoded}" + + +def _extract_file_payload(arguments: Dict[str, Any]) -> Tuple[Dict[str, Any], Optional[bytes], Optional[str]]: + file_payload = arguments.get("file") + file_path = arguments.get("file_path") + if file_payload is None and file_path is None: + raise ValueError("missing file or file_path") + + name = None + mime_type = None + size = None + last_modified = None + content_base64 = None + data = None + + if file_payload is not None: + name = file_payload.get("name") + mime_type = file_payload.get("type") + size = file_payload.get("size") + last_modified = file_payload.get("last_modified") + content_base64 = file_payload.get("content_base64") + + if file_path: + path = Path(file_path) + if name is None: + name = path.name + if content_base64 is None: + data = path.read_bytes() + + if data is None and content_base64 is not None: + data = _decode_base64(content_base64) + + return ( + { + "name": name, + "type": mime_type, + "size": size, + "last_modified": last_modified, + "content_base64": content_base64 + }, + data, + name + ) + + +def _normalize_mime_type(mime_type: Optional[str], name: Optional[str]) -> str: + if mime_type: + return mime_type + return _guess_mime_type(name) + + +def _build_file_json(arguments: Dict[str, Any]) -> str: + file_info, data, name = _extract_file_payload(arguments) + if file_info["type"] is None: + file_info["type"] = _guess_mime_type(name) + if file_info["size"] is None and data is not None: + file_info["size"] = len(data) + return json.dumps(file_info, ensure_ascii=False) + + +def _build_file_data_uri(arguments: Dict[str, Any]) -> str: + file_info, data, name = _extract_file_payload(arguments) + if data is None: + raise ValueError("missing file content for data uri") + mime_type = arguments.get("type") or file_info.get("type") + mime_type = _normalize_mime_type(mime_type, name) + return _build_data_uri(data, mime_type) + + +def _build_path_data_uri(arguments: Dict[str, Any]) -> str: + file_path = arguments.get("file_path") + if not file_path: + raise ValueError("missing file_path") + path = Path(file_path) + data = path.read_bytes() + mime_type = arguments.get("type") or _guess_mime_type(path.name) + return _build_data_uri(data, mime_type) + + +async def _build_url_data_uri(arguments: Dict[str, Any]) -> str: + url = arguments.get("url") + if not url: + raise ValueError("missing url") + async with httpx.AsyncClient(follow_redirects=True, timeout=20) as client: + response = await client.get(url) + response.raise_for_status() + content_type = response.headers.get("content-type", "") + mime_type = content_type.split(";")[0].strip() if content_type else "" + if not mime_type: + mime_type = _guess_mime_type(url) + return _build_data_uri(response.content, mime_type) + + +async def _build_url_file_path(arguments: Dict[str, Any]) -> str: + url = arguments.get("url") + if not url: + raise ValueError("missing url") + file_path = arguments.get("file_path") + if file_path: + path = Path(file_path) + path.parent.mkdir(parents=True, exist_ok=True) + else: + suffix = Path(urlparse(url).path).suffix + with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file: + path = Path(temp_file.name) + async with httpx.AsyncClient(follow_redirects=True, timeout=20) as client: + response = await client.get(url) + response.raise_for_status() + path.write_bytes(response.content) + return str(path) + + +async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]: + try: + if name == "file_to_json": + result = _build_file_json(arguments) + elif name == "file_to_data_uri": + file_path = arguments.get("file_path") + if _is_url(file_path): + result = await _build_url_data_uri({"url": file_path}) + else: + result = _build_file_data_uri(arguments) + elif name == "file_path_to_data_uri": + file_path = arguments.get("file_path") + if _is_url(file_path): + result = await _build_url_data_uri({"url": file_path}) + else: + result = _build_path_data_uri(arguments) + elif name == "url_to_data_uri": + result = await _build_url_data_uri(arguments) + elif name == "url_to_temp_file": + result = await _build_url_file_path(arguments) + else: + raise ValueError(f"unknown tool name: {name}") + return [TextContent(type="text", text=result)] + except Exception as exc: + return [TextContent(type="text", text=f"Failed to call tool {name}: {exc}")] diff --git a/file_tools/mcp-server-file-tools.json b/file_tools/mcp-server-file-tools.json new file mode 100644 index 0000000..0a49d92 --- /dev/null +++ b/file_tools/mcp-server-file-tools.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "lzwcai-mcpskills-file-tools-mcp": { + "disabled": false, + "type": "stdio", + "timeout": 30, + "command": "uvx", + "args": [ + "lzwcai-mcpskills-file-tools-mcp" + ], + "env": {} + } + } +} diff --git a/file_tools/pyproject.toml b/file_tools/pyproject.toml new file mode 100644 index 0000000..f2a715a --- /dev/null +++ b/file_tools/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "lzwcai-mcpskills-file-tools-mcp" +version = "0.1.9" +description = "File tools MCP server" +requires-python = ">=3.10" +dependencies = [ + "mcp[cli]>=1.6.0", + "httpx>=0.24.0" +] + +[project.scripts] +lzwcai-mcpskills-file-tools-mcp = "file_tools.main:main" + +[tool.hatch.build.targets.wheel] +packages = ["file_tools"] + diff --git a/file_tools/test_tools.py b/file_tools/test_tools.py new file mode 100644 index 0000000..01d09c7 --- /dev/null +++ b/file_tools/test_tools.py @@ -0,0 +1,67 @@ +import argparse +import asyncio +import base64 +from typing import Optional + +from file_tools.tools import handle_call_tool + + +async def _run( + url: Optional[str], + file_path: Optional[str], + text: Optional[str], + name: str, + download_url: Optional[str], + download_path: Optional[str] +) -> None: + if url: + result = await handle_call_tool("url_to_data_uri", {"url": url}) + print("url_to_data_uri:", result[0].text[:80], "len=", len(result[0].text)) + + if file_path: + result = await handle_call_tool("file_path_to_data_uri", {"file_path": file_path}) + print("file_path_to_data_uri:", result[0].text[:80], "len=", len(result[0].text)) + + if download_url: + payload = {"url": download_url} + if download_path: + payload["file_path"] = download_path + result = await handle_call_tool("url_to_temp_file", payload) + print("url_to_temp_file:", result[0].text) + + if text is not None: + payload = { + "file": { + "name": name, + "content_base64": base64.b64encode(text.encode("utf-8")).decode("ascii") + } + } + result = await handle_call_tool("file_to_json", payload) + print("file_to_json:", result[0].text) + result = await handle_call_tool("file_to_data_uri", payload) + print("file_to_data_uri:", result[0].text[:80], "len=", len(result[0].text)) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--url") + parser.add_argument("--file-path") + parser.add_argument("--download-url") + parser.add_argument("--download-path") + parser.add_argument("--text") + parser.add_argument("--name", default="sample.txt") + args = parser.parse_args() + asyncio.run( + _run( + args.url, + args.file_path, + args.text, + args.name, + args.download_url, + args.download_path + ) + ) + + +if __name__ == "__main__": + main() diff --git a/lzwcai_lark_mcp/lzwcai_lark_mcp/__init__.py b/lzwcai_lark_mcp/lzwcai_lark_mcp/__init__.py new file mode 100644 index 0000000..470d693 --- /dev/null +++ b/lzwcai_lark_mcp/lzwcai_lark_mcp/__init__.py @@ -0,0 +1 @@ +__all__ = ["main"] diff --git a/lzwcai_lark_mcp/lzwcai_lark_mcp/config.py b/lzwcai_lark_mcp/lzwcai_lark_mcp/config.py new file mode 100644 index 0000000..cea8f0d --- /dev/null +++ b/lzwcai_lark_mcp/lzwcai_lark_mcp/config.py @@ -0,0 +1,9 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + + +class Config: + APP_ID = os.getenv("app_id", "") + APP_SECRET = os.getenv("app_secret", "") diff --git a/lzwcai_lark_mcp/lzwcai_lark_mcp/main.py b/lzwcai_lark_mcp/lzwcai_lark_mcp/main.py new file mode 100644 index 0000000..38ae3a5 --- /dev/null +++ b/lzwcai_lark_mcp/lzwcai_lark_mcp/main.py @@ -0,0 +1,85 @@ +import asyncio +import logging +import os +import time +from typing import Tuple + +import requests +from mcp.server import Server +from mcp.server.stdio import stdio_server + +from .config import Config +from .tools import handle_call_tool, tools + + +logging.basicConfig( + level=getattr(logging, os.getenv("LOG_LEVEL", "INFO"), logging.INFO), + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +class LarkMcpServer: + def __init__(self) -> None: + self.server = Server("lzwcai-mcpskills-lark-mcp") + self.tenant_access_token: str | None = None + self.token_expire_at: float = 0 + self._register_handlers() + + def _register_handlers(self) -> None: + @self.server.list_tools() + async def list_tools(): + return tools + + @self.server.call_tool() + async def call_tool(name: str, arguments: dict): + await self.ensure_token() + return await handle_call_tool(name, arguments, self.tenant_access_token or "") + + async def ensure_token(self) -> None: + if self.tenant_access_token and time.time() < self.token_expire_at - 60: + return + token, expires_in = await asyncio.to_thread(self._request_token) + self.tenant_access_token = token + self.token_expire_at = time.time() + expires_in + + def _request_token(self) -> Tuple[str, int]: + if not Config.APP_ID or not Config.APP_SECRET: + raise ValueError("app_id or app_secret is missing") + auth_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" + headers = {"Content-Type": "application/json"} + payload = {"app_id": Config.APP_ID, "app_secret": Config.APP_SECRET} + response = requests.post( + auth_url, + json=payload, + headers=headers, + timeout=10 + ) + response.raise_for_status() + data = response.json() + if data.get("code") not in (0, None): + raise RuntimeError(f"lark auth failed: {data}") + token = data.get("tenant_access_token") + if not token: + raise RuntimeError(f"lark auth response missing token: {data}") + expires_in = int(data.get("expire", 3600)) + return token, expires_in + + +async def _main() -> None: + server = LarkMcpServer() + await server.ensure_token() + async with stdio_server() as (read_stream, write_stream): + await server.server.run( + read_stream, + write_stream, + server.server.create_initialization_options() + ) + + +def main() -> None: + asyncio.run(_main()) + + +if __name__ == "__main__": + main() diff --git a/lzwcai_lark_mcp/lzwcai_lark_mcp/tools.py b/lzwcai_lark_mcp/lzwcai_lark_mcp/tools.py new file mode 100644 index 0000000..60533bf --- /dev/null +++ b/lzwcai_lark_mcp/lzwcai_lark_mcp/tools.py @@ -0,0 +1,280 @@ +import json +import mimetypes +import os +import re +from datetime import datetime +from typing import Dict, List +from urllib.parse import urlparse + +import requests +from mcp.types import Tool, TextContent + +tools = [ + Tool( + name="upload_image_by_url", + description="上传图片并返回image_key,入参为可下载的文件URL", + inputSchema={ + "type": "object", + "properties": { + "image_type": { + "type": "string", + "description": "图片类型,例如 message" + }, + "url": { + "type": "string", + "description": "可直接下载的图片文件URL" + } + }, + "required": ["url"] + } + ), + Tool( + name="send_card_message", + description="发送消息卡片,入参为receiver_ids、person_id和image_key", + inputSchema={ + "type": "object", + "properties": { + "receiver_ids": { + "type": "array", + "description": "消息接收者ID列表", + "items": { + "type": "string" + } + }, + "person_id": { + "type": "string", + "description": "卡片内person组件的ID" + }, + "image_key": { + "type": "string", + "description": "图片image_key" + } + }, + "required": ["image_key", "person_id"] + } + ) +] + + +def upload_image_by_url(token: str, url: str, image_type: str) -> str: + download_response = requests.get(url, stream=True, timeout=30) + download_response.raise_for_status() + content = download_response.content + content_disposition = download_response.headers.get("Content-Disposition") or download_response.headers.get("content-disposition") + filename = "" + if content_disposition: + match = re.search(r'filename\*?=(?:UTF-8\'\')?"?([^";]+)"?', content_disposition) + if match: + filename = match.group(1).split("/")[-1] + if not filename: + path = urlparse(url).path + filename = path.split("/")[-1] if path else "" + if not filename: + filename = "image" + content_type = download_response.headers.get("Content-Type") + if not content_type: + content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream" + headers = {"Authorization": f"Bearer {token}"} + data = {"image_type": image_type} + files = {"image": (filename, content, content_type)} + response = requests.post( + "https://open.feishu.cn/open-apis/im/v1/images", + headers=headers, + data=data, + files=files, + timeout=30 + ) + response.raise_for_status() + payload = response.json() + if payload.get("code") not in (0, None): + raise RuntimeError(f"lark image upload failed: {payload}") + image_key = payload.get("data", {}).get("image_key") + if not image_key: + raise RuntimeError(f"lark image upload missing image_key: {payload}") + return image_key + + +def send_card_message(token: str, receiver_id: str, person_id: str, image_key: str) -> str: + current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + content = { + "schema": "2.0", + "config": { + "update_multi": True, + "locales": ["en_us"], + "style": { + "text_size": { + "normal_v2": { + "default": "normal", + "pc": "normal", + "mobile": "heading" + } + } + } + }, + "body": { + "direction": "vertical", + "padding": "12px 12px 12px 12px", + "elements": [ + { + "tag": "markdown", + "content": f"时间 {current_time}", + "i18n_content": { + "en_us": f"Incident time\n{current_time}" + }, + "text_align": "center", + "text_size": "normal_v2", + "margin": "0px 0px 0px 0px" + }, + { + "tag": "markdown", + "content": f"\n进入仓库", + "i18n_content": { + "en_us": "Alert details\nMobile client crash rate at 5%" + }, + "text_align": "center", + "text_size": "normal", + "margin": "0px 0px 0px 0px" + }, + { + "tag": "column_set", + "horizontal_spacing": "8px", + "horizontal_align": "left", + "columns": [ + { + "tag": "column", + "width": "weighted", + "elements": [], + "vertical_spacing": "8px", + "horizontal_align": "left", + "vertical_align": "top", + "weight": 1 + }, + { + "tag": "column", + "width": "auto", + "elements": [ + { + "tag": "img", + "img_key": image_key, + "preview": True, + "transparent": False, + "scale_type": "crop_center", + "size": "large", + "corner_radius": "16px", + "margin": "0px 0px 0px 0px" + } + ], + "vertical_spacing": "8px", + "horizontal_align": "left", + "vertical_align": "top" + }, + { + "tag": "column", + "width": "weighted", + "elements": [], + "vertical_spacing": "8px", + "horizontal_align": "left", + "vertical_align": "top", + "weight": 1 + } + ], + "margin": "0px 0px 0px 0px" + }, + { + "tag": "hr", + "margin": "0px 0px 0px 0px" + } + ] + }, + "header": { + "title": { + "tag": "plain_text", + "content": "人员进入仓库通知", + "i18n_content": { + "en_us": "[Action Needed] Alert: Process Error - Please Address Promptly" + } + }, + "subtitle": { + "tag": "plain_text", + "content": "" + }, + "template": "wathet", + "icon": { + "tag": "standard_icon", + "token": "bell_filled" + }, + "padding": "12px 12px 12px 12px" + } + } + payload = { + "receive_id": receiver_id, + "msg_type": "interactive", + "content": json.dumps(content, ensure_ascii=False) + } + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } + response = requests.post( + "https://open.feishu.cn/open-apis/im/v1/messages", + params={"receive_id_type": "user_id"}, + headers=headers, + json=payload, + timeout=30 + ) + try: + data = response.json() + except ValueError: + data = {"raw": response.text} + if not response.ok: + raise RuntimeError(f"lark send message http error: {data}") + if data.get("code") not in (0, None): + raise RuntimeError(f"lark send message failed: {data}") + message_id = data.get("data", {}).get("message_id") + if not message_id: + raise RuntimeError(f"lark send message missing message_id: {data}") + return message_id + + +def send_card_messages(token: str, receiver_ids: List[str], person_id: str, image_key: str) -> str: + if not receiver_ids: + raise ValueError("missing receiver_ids") + message_ids = [] + for receiver_id in receiver_ids: + normalized_receiver_id = str(receiver_id).strip() + if not normalized_receiver_id: + continue + message_ids.append(send_card_message(token, normalized_receiver_id, person_id, image_key)) + if not message_ids: + raise ValueError("missing receiver_ids") + return json.dumps(message_ids, ensure_ascii=False) + + +async def handle_call_tool(name: str, arguments: Dict[str, object], token: str) -> List[TextContent]: + try: + if name == "upload_image_by_url": + image_type = str(arguments.get("image_type") or "message").strip() + url = str(arguments.get("url", "")).strip() + if not image_type: + raise ValueError("missing image_type") + if not url: + raise ValueError("missing url") + + result = upload_image_by_url(token, url, image_type) + elif name == "send_card_message": + image_key = str(arguments.get("image_key", "")).strip() + receiver_ids = arguments.get("receiver_ids") + person_id = str(arguments.get("person_id", "")).strip() + if not image_key: + raise ValueError("missing image_key") + if not person_id: + raise ValueError("missing person_id") + if receiver_ids is not None: + if not isinstance(receiver_ids, list): + raise ValueError("receiver_ids must be a list") + result = send_card_messages(token, receiver_ids, person_id, image_key) + else: + raise ValueError(f"unknown tool name: {name}") + return [TextContent(type="text", text=result)] + except Exception as exc: + return [TextContent(type="text", text=f"Failed to call tool {name}: {exc}")] diff --git a/lzwcai_lark_mcp/mcp-server.json b/lzwcai_lark_mcp/mcp-server.json new file mode 100644 index 0000000..63c2aaf --- /dev/null +++ b/lzwcai_lark_mcp/mcp-server.json @@ -0,0 +1,17 @@ +{ + "mcpServers": { + "lzwcai-mcpskills-lark-mcp": { + "disabled": false, + "type": "stdio", + "timeout": 30, + "command": "uvx", + "args": [ + "lzwcai-mcpskills-lark-mcp" + ], + "env": { + "app_id": "cli_a8d0e0c140169013", + "app_secret": "yEc0E8Aoo8Mo9NPPzphidez51xB71HXW" + } + } + } +} \ No newline at end of file diff --git a/lzwcai_lark_mcp/pyproject.toml b/lzwcai_lark_mcp/pyproject.toml new file mode 100644 index 0000000..cae69c4 --- /dev/null +++ b/lzwcai_lark_mcp/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "lzwcai-mcpskills-lark-mcp" +version = "0.1.4" +description = "Lark MCP server" +requires-python = ">=3.10" +dependencies = [ + "mcp[cli]>=1.6.0", + "python-dotenv>=0.21.0", + "requests>=2.25.0" +] + +[project.scripts] +lzwcai-mcpskills-lark-mcp = "lzwcai_lark_mcp.main:main" + +[tool.hatch.build.targets.wheel] +packages = ["lzwcai_lark_mcp"] diff --git a/lzwcai_lark_mcp/test.py b/lzwcai_lark_mcp/test.py new file mode 100644 index 0000000..09bedbe --- /dev/null +++ b/lzwcai_lark_mcp/test.py @@ -0,0 +1,38 @@ +import asyncio +import json +import os +from pathlib import Path + + +def main() -> None: + if not os.getenv("app_id") or not os.getenv("app_secret"): + config_path = Path(__file__).with_name("mcp-server.json") + if config_path.exists(): + config_data = json.loads(config_path.read_text(encoding="utf-8")) + env_data = ( + config_data.get("mcpServers", {}) + .get("lzwcai-mcpskills-lark-mcp", {}) + .get("env", {}) + ) + if env_data.get("app_id") and env_data.get("app_secret"): + os.environ["app_id"] = env_data["app_id"] + os.environ["app_secret"] = env_data["app_secret"] + if not os.getenv("app_id") or not os.getenv("app_secret"): + raise RuntimeError("missing app_id or app_secret") + from lzwcai_lark_mcp.main import LarkMcpServer + from lzwcai_lark_mcp.tools import send_card_message + + async def _run() -> None: + server = LarkMcpServer() + await server.ensure_token() + image_key = "img_v3_02uq_d48b3ee1-0f89-44a3-80cd-a5afa4f8c39g" + receiver_id = "843ga2gb" + person_id = receiver_id + result = send_card_message(server.tenant_access_token or "", receiver_id, person_id, image_key) + print(result) + + asyncio.run(_run()) + + +if __name__ == "__main__": + main() diff --git a/terminal_temi_mcp/dist/terminal_temi_mcp-0.1.21-py3-none-any.whl b/terminal_temi_mcp/dist/terminal_temi_mcp-0.1.21-py3-none-any.whl deleted file mode 100644 index 9b7b395813647c9ab4f1e8d1e695fee3894b253c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7227 zcma)>WmH_twx}Bj65JaP?$Bs(PvhFSOXDsboFKs^!6CR?2(BSmZ~`>GAsryNOVHq# zbMG7PyqmGl8@tw+wSLuCbF5W!))Y+@lxM^M0084jWHp~A0P?@4r$PNBFdI)72UkmH zbC`{bgSiXT9b|6q;OYP~H|KWuLjh?1uf-tRfbKqA0H9b803iR{#V(c(u1^aOO?KT7 z@}!SP-C>DZI%qne?-G(2lU6<*IGVHw*JP_CdW>iC6e7+{T6C374i-J?P~)!LThA=f zdZ>^CWf>%LE<+RVZo8#Q zY$8+~cx#o|KAyZNihW=z*GebdhD2DBumO{-2y*Bv=cg@oRa6?la)e6F5rjq|I&m9B zhG?5oGUkrQ-~4bkUB7ikVew4X-4Npq+(j$Ju z&YFqEF{X7THr5f7gRcbh7{d*GR&=ju&bVYaZbmkYGxH;X}8)l++59(6Uc5DK#QWt@ZW`{SAdSp6t$!mS~rDn@+d zx>_%Zh&)AXllm1rX?W{@;{ovyr3K|@oE&eM6T&`cCY=K-t2D(k(q_*_uDl%dj44c0 zA*^hrFVwX%A5-wd=lRB!HA~RY2jRf&E%AxVANj_J<^$iB^!Oohq#3_@S6Gn zFNj(jxndy&52pfc^Q2qD&}jA*Yl&!n;YcHQgO>iN%i^kz9&!^NKNQRm8>l}25W*2p zP={9@*B(Y;qedP39B3{0YdKM+B&?|@8OWFPtw)G`_M>i)EqLXoUdy!F4W1Pc;O{rV z$^~|2KDWUn5m-ezMR|wfuO{t^fP~;OfRobfnLSlg8(6vQ8|FUADo<<{;CQU^of{J4 zB!qpX1)iiO5#_Uji3^}Y$wq~#KoGOwhr;26tm+&>40^++V5CEm3H9iXRI)p&oL6ehkqv#nP7dU#6;zYR9X@rKHWzE z#2c*|=7=a70uGlTVOwV~q|+zX(LiONwNiMVz$+9&grasi-_R*j3@}LPF&uYY4N2W! zg9s4Qnn*C|eu@mapinL4F+3wm zy8>&@Y7&CW-b@(p80?Q7ra7HrAHeRPaNNi3A; zmXn9j`dme~z*NBUSno z2GD{*saoV8fsavy~PGO1}`KEiQF4G(rTt1CxY> z?DC?p(~5jQ|DB^81B{_K%gx)}ugDvI{-o(mgq+%->zkVRk_w}Sfw!BMB}^Ufo9Y(a z-X-{~H~(lh?D}SET0nGWxkYYXs^Rr95{Ok3kNYq_hi&0qY92Avh)*;qTxWgd#@Lj! zLym>2rM=;dV6BlgN!IzqwRZ6-ZOfoJL--Tg`gdlLlDSW#U^77~fEIrg(hK(X;GfS^ zw@t?!i;Ad;w09VlM;QEeIc5h2vnj#8G6Am<)v9}?+e;xsNmLj#*^DM9;V3EC2K?BG z7VW<8G&(ASEp}P|!pcg`l=3(Y5?K;6h~M2FT(b(YtI@h||K*~;dhvSb8!8iOIyMR~ zi#n!(z;5lsYUx`YkWqge>+>1W%5@08L%w>GZ{~f}OUcu-Ag?0=YU zR{44}D!R_%MR=T8Xhj_AI{BSgPM@8(QeYwx^;edt5^tGNZLaUsq!;gqth#vsvQNi` zOlA#$pVQh~o?+4G^T7V|OsR@nELgp+c-#J!oU;08++s7^>1Oso(?HM|@1U@Pk6ZwOm%p@6f_$eMt8ZP}|*rpj)=+ z=?tIvfV7Ne^wbQ`xmG*#`7Nq6@t3JOWNjc0-3bZC z6z}Blr>+L$6;v(`?F6y}%v5LgLZ(cDt3YNmEN>i_IkVHQGn#K{ds$6qfK{0pWW7w| z$!_snv%y>+x}Dd?HgWC;8@FbuFwS`!Q$fVd+LdRlEX>GjXkEJ@K2aN9`Gu)l3~x0_ zh}%Pm)npwJbK>1%ipdZ*IA{C zR?kX&P?QwjmKtnln!d!!Kc(kgfp)}qkOb|rS>#&P|B};vv~BTttcr-_ScqC22$_V^ ztCG^bjr#q~sM4_0DP7-uzKoX>ch+coOQzypG4ed>(Yd(TO(|rH0M(RL8S|@SzDFII zSC-d&-*$}!>wL2tlqE`{{iWAqXgl9f{^;O^6Thtu1>XKZ>DI<;@v_=+>FsH%HR#}C z`yc_?%H;>=%nF|TciQWK)5=fhBhNeUoPI;^1f?bR3*C9()H;`>)4;01uUTE_oxjPF zJ+dNM0Ii0Ld{;nsUba4zo}$6h5{xy$SJo8IMtHLLXp#x|_~qK&-JAGAJN@kB%mc0T zd#f75r4zi9J}w{f^_v?ptY>`)rB@UcBpmp;$a$O%CDNe)t)$OCd%m%_^+_M7oYe>!bfcJmsnCet0t>PlM>soQ%zaefy3|(T8uKO2 z8KqOBsA%`2T60)>kO=U;9PFl2*P%ZD#rt+6xW&;um+Ut2wY#7*K^?zSpaenra|%5K zy3tBi7zmo}^->#@WuC5e0NgG;FhUEYbJEYBdc8rZR_;H`kZ0jx>4T67# zd`5Vog9;^hX$|99*I%?$;XEU-ckqB*Xx6RD)^ft&xzA0)FX7zrUe4;SPUo7##t|ZQ zBQK@olG)1xc;-^nE^z6s!`d#(WxO}8&D2%C&`?dv$ZoOR6dwJy$ zYH}GFSscAFUomV>X&We23wXd>Tf9qq*>7+|Qze4gH{zARIJ;OP<}3@+@(P!Vaqc`O zbmS0XacJC`wEC7MG_%Dm;Bz}d>O5%V&zrOP`Stq}P;ZLa_n4U%s!h#S;tlg78c)XQ z^u^AF6aH0dYqCXe>|&(&C*5x`aZ_0(9pWhvaeK6JtH%;Bmtqcl^2K*hNvK-IXDls2 zot_D2Q}p95B(1QE>#bC{(#XC9Ah!n3ZFO`)D8QtCTFvqcQIi>8`u4?#G-0*GL@`9g zM2cY#iPt^mQg<(5x_)c6x!c0XR^L$IG-oY@JC=Ukbs$TN z@Pi&zL8)_%v{1lR9exJ{dLv>khRe{+BVWts%{Gpnb`BI`^HAZ~Sptm?3nA@XGaKB( zg@=ClnbjVhiXsxM(nFc`2dQnU*(+@9PgxU~A4t~ghEfcdXY|W0#5)&Nc9*n;sV{E5 zwM4IR_AghEB+xq)ltO8uUMNlyIA=_4#H5^0O|(Sij8FWI&MxS;jo=IrQA!W+^fZEn zr`Ma}}=$?)+q378b;FIx*R)zICPS%2eR4^Q&nQVY>XZoJq^hFhDix zG@l`Qk}I@-#&6fA(cWYdY~1PK1sm9Pv6T@rpZ zhx_t=j`YSsaY{!HWLb-{JKu(KLVGVwewKiVEf7{dJh#gcwP>nlU-eS6F>XQAoG!kk2zDg#-^}T9@4MBB zN?RMPklXP#T@u95ZLV>Pu6*xqqfE<$`99`|qw{uN4lE!sRz}F?!Td^8bpq(po4C?kfpENA;UM<(-9c1<5=C0rTlnzl| zx=QSuiTmi;=5FkZYh}=_xbOx$Nn7&~iFg!4WTArgxTm$zr9=Mv_M3-s=S7*Tp|Sw| zdwhvlW3isP+s!?c8}i$wGs$-H)^u@b+-sEIrU`3{OWwy^(skOgms2u9B&#wK_TBYY zCAU;mI#TfBODdXSG%HO5cjlw0y}R5icD#ejATu6Z&5@e;*7BJXY5nDzZBfzlc{2+H zZ>^=2@bLi+)9r1y?ryy@vlXY5lekf_le9rxN-Hx%*KxwH{b5$K6qag)CgYAa5spaJ z#X}ZlH{qOqFi)`Ax|zJw)JQqg%>HkSk#7oQo5-&{p7$DAawyK?U!#>X+LqSt7#GKn zHSayhPET}5=1@?7b%_ZJfe}{8SaCB47An#vl(h-befkS^H@+|v&o|?+}tBWav>ekY9Ftg#D}9?uA7>k#z#{$%r~L7H&;L2sjdP9 zv%jF^Dpq@84=$|Z#gtPOSQ0=o!n%i&itWxkL@YU(2Q!B%f0J{{512U43F!Kyr;1|O zlWK=k?>X1-3ank|ibRfblLhHKJFY9dg=cATfAn7rZnh;zjZ4*(l}9y*WA;Nka35{b zk;@RwZ#UA7TMCO0`8mhlPkI@GeegE55cT4kc!t9)slFXhMoS|ISg_ z{hAh8WJNP+SffqAEE(30%%r54`)F$`PmxRZB0DE(5Kg!Sxw+dxRi>`iVgTQ4GlHTU zPmCnRAQk1>mGqoi#^Q4j5{EDGiCAj5CF3Wcbikrai^N zS_~?=5JUts2`SwuRDNzWh#58#tEk7Ds*ILg$7HNK7A3A4+@nn3M+esu)sL1o)Gg`w zalI;=cgB78saltAJ?t-Hdn1dndxhx`W)kdUOM2bb-w=OnI+Retrc}c$5gJ_Dve2yk zFn`in3kfbpK@AE~)ly=$L9)r7Ag+CIg71PCeM}l-JTK&;@V`RG32XIvoAE;83J-2817_jG#aYSU&0CCtjA;+>HO$o+-?2$ zek=&2M5TTFt8G{p(S7*B<#6F+!o*QcXTx_%1Nw|+N|15X`|p&-pIUDxE>|vZ9P!>t zL~tq+T+oM=F>@qtb4KV*A)B+Km4jXmeVm}v81CP{=TOQ3M$ zch(a*VicNN84`+}X|ot9ZXkcQym)7Az)gS!v@S_$+L|GlzciY8xF}m{3x;*9I(Hu& zbX_&HUmVp19fK3cmmHy8Vv(X0h3l+NkIPMz8V+R96J}u0O;_R=>T|ic<4Z`H!W@P~ zJ+>)5wMrDG8euI~c*z?=l4#x!Xj;$Z_gJ)aJn#hR1QX*DVvjaF%_N#z&0)AN#GK$i zYMvY2Y9RKdE=TK}`I_l@RkMc2ex}fVc*SJXQ@Q%Unq`>@qb;=Oco-&&`9UzPB+E_S znb&!yuw85SfT0x8M;D*$1Vi(EX$=@hatlcpXpiRyuq8PvmM=L!%fr20U7uK1lZ|5* zf=F|tN8U!!xpZI87@D;n-;HUeUlrB6x9A^=X&ih45(Ll8dmf0fScn4?*>|ay8PHmT z+tq)BrhYc3ajyL(gaoJTtvmB)qHgg3`Jz-ecWb0J9e?pouR$*_*CHQEz?nRdKeKlj zaQ)%_@^WXmBLJ}+^fNR*|K4sl^8POJ%(vs>(yeOJZ245xTQ5E5FfH$M*^&)y;e_0; zo)3F~k28UTAv8Zu>vjZ;g5OZtOi6<(=AEq%hHz*Nf0~QPmrJ#Dg;S!LYWfZ4(S3`Q zt{O4fShx9F;^<|_1D{x;cHlv{^ZA4<+wdM2k}voG3Nfl#`jF-%>K zds6R`MCf{HB9RkOd41otb7m)A+XteDmE$wQ3$U!_7`YpZHngH{{yFJ z42|l*T{Sj~!Ak$Z;+x!Y{mb6zsO%G#`z-f5ec@J$%Zak6IscCN#(?CPtCe(1Y)P4> zV8W?hiu>hab+7fE%c{n9;O=A*YgxzAL?mrkm%_+qyyJE4P0rO}aZC`Gie(>%9IebL{i5n5{?9kIx0u&i7T@as@G<#6G?z3R~} zz04mfmX!Odj!L$R$v31UOOrIm)Uf9J4idNur5!AhVs76QQ|shyxm!7ARVw99eCF@C zH~pnE3pG#e&3Cc;m(79*rlVET=HO_7X8ujgd{GQpml}j_-VzLm+LuY4V;bp8!>cqZ z^&@tj2SO<#zW>MVE?ZSg`&7~f=syYTKl>*7ieRuR7FWPR}5(iO$&D|ERh1x3s$#9-fglgagC0Tsf@*dl*Y&7;cDEPb-F$HGvCz4#u||cO#V>ez*C&V_UX9j zJT(s3{+S98u$%_uRl?*q&oCZ<^i)tcYDIW--3~HuM1hW96Sga#mUc{L2)=n>kp2X0 zkoyK@d%|Quw3kVOaNrq>6{3sqE0caW*`et^2j<`7t0G z1J~dS4@#2#o~nkk+wzzGS{fw(` r|Fq>lH~7;w|Gj|#**{YIe=Jo~1@$Sr0RYgSo>xz1zWT5B9^k(KsLtd+ diff --git a/terminal_temi_mcp/dist/terminal_temi_mcp-0.1.21.tar.gz b/terminal_temi_mcp/dist/terminal_temi_mcp-0.1.21.tar.gz deleted file mode 100644 index 809f31745de166b288c0c72a355a568f1ddd8731..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6933 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 diff --git a/terminal_temi_mcp/mcp-server-temi.json b/terminal_temi_mcp/mcp-server-temi.json index 325a683..1b7e99b 100644 --- a/terminal_temi_mcp/mcp-server-temi.json +++ b/terminal_temi_mcp/mcp-server-temi.json @@ -1,17 +1,13 @@ { "mcpServers": { - "terminal-temi-mcp": { + "terminal_temi_mcp": { "disabled": false, "type": "stdio", "timeout": 30, "command": "uvx", "args": [ "terminal_temi_mcp" - ], - "env": { - "employeeId": "$employeeId$", - "userId": "$employeeId$" - } + ] } } } \ No newline at end of file diff --git a/terminal_temi_mcp/pyproject.toml b/terminal_temi_mcp/pyproject.toml index ac2080e..19508fd 100644 --- a/terminal_temi_mcp/pyproject.toml +++ b/terminal_temi_mcp/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "terminal_temi_mcp" -version = "0.1.21" +version = "0.1.28" description = "MQTT-based navigation server for robot" requires-python = ">=3.10" dependencies = [ diff --git a/terminal_temi_mcp/terminal_temi_mcp/main.py b/terminal_temi_mcp/terminal_temi_mcp/main.py index 1292f37..d6b41c8 100644 --- a/terminal_temi_mcp/terminal_temi_mcp/main.py +++ b/terminal_temi_mcp/terminal_temi_mcp/main.py @@ -50,7 +50,7 @@ class NavServer: params = { "data": {"location": location, "flag": flag} } - return await self.publish_Cmd("123456", user_id, "nav", params) + return await self.publish_Cmd("111111", user_id, "nav", params) except Exception as e: logger.error(f"Failed to call navigation mcp-tool: {str(e)} ", exc_info=True) return f"Failed to call navigation mcp-tool: {str(e)}" @@ -64,12 +64,12 @@ class NavServer: params = { "data": {"speech": speech} } - return await self.publish_Cmd("123456", user_id, "speak", params) + return await self.publish_Cmd("111111", user_id, "speak", params) except Exception as e: logger.error(f"Failed to call speak mcp-tool: {str(e)} ", exc_info=True) return f"Failed to call speak mcp-tool: {str(e)}" - async def reception(self, user_id: str, location: str): + async def reception(self, user_id: str, location: str, name: str = "贵宾", waitingTime: int = 20*60*1000): """轮足机器人移动到指定位置迎宾""" try: if not location: @@ -77,10 +77,12 @@ class NavServer: else: params = { "data": { - "location": location + "location": location, + "name": name, + "waitingTime": waitingTime } } - return await self.publish_Cmd("123456", user_id, "reception", params) + return await self.publish_Cmd("111111", user_id, "reception", params) except Exception as e: logger.error(f"Failed to call reception mcp-tool: {str(e)} ", exc_info=True) return f"Failed to call reception mcp-tool: {str(e)}" @@ -97,7 +99,7 @@ class NavServer: "text": text } } - return await self.publish_Cmd("123456", user_id, "notification", params) + return await self.publish_Cmd("111111", user_id, "notification", params) except Exception as e: logger.error(f"Failed to call notification mcp-tool: {str(e)} ", exc_info=True) return f"Failed to call notification mcp-tool: {str(e)}" @@ -110,7 +112,7 @@ class NavServer: "action": "repose" } } - return await self.publish_Cmd("123456", user_id, "map", params) + return await self.publish_Cmd("111111", user_id, "map", params) except Exception as e: logger.error(f"Failed to call repose mcp-tool: {str(e)} ", exc_info=True) return f"Failed to call repose mcp-tool: {str(e)}" @@ -127,7 +129,7 @@ class NavServer: "next_location": next_location } } - return await self.publish_Cmd("123456", user_id, "delivery", params) + return await self.publish_Cmd("111111", user_id, "delivery", params) except Exception as e: logger.error(f"Failed to call delivery mcp-tool: {str(e)} ", exc_info=True) return f"Failed to call delivery mcp-tool: {str(e)}" @@ -141,7 +143,7 @@ class NavServer: params = { "data": {"locations": locations} } - return await self.publish_Cmd("123456", user_id, "patrol", params) + return await self.publish_Cmd("111111", user_id, "patrol", params) except Exception as e: logger.error(f"Failed to call patrol mcp-tool: {str(e)} ", exc_info=True) return f"Failed to call patrol mcp-tool: {str(e)}" @@ -155,7 +157,7 @@ class NavServer: params = { "data": {} } - return await self.publish_Cmd("123456", user_id, "face", params) + return await self.publish_Cmd("111111", user_id, "face", params) except Exception as e: logger.error(f"Failed to call face recognize mcp-tool: {str(e)} ", exc_info=True) return f"Failed to call face recognize mcp-tool: {str(e)}" @@ -169,7 +171,7 @@ class NavServer: params = { "data": {} } - return await self.publish_Cmd("123456", user_id, "qrcode", params) + return await self.publish_Cmd("111111", user_id, "qrcode", params) except Exception as e: logger.error(f"Failed to call QR code scan mcp-tool: {str(e)} ", exc_info=True) return f"Failed to call QR code scan mcp-tool: {str(e)}" @@ -183,7 +185,7 @@ class NavServer: params = { "data": {"location_name": location_name} } - return await self.publish_Cmd("123456", user_id, "saveLocation", params) + return await self.publish_Cmd("111111", user_id, "saveLocation", params) except Exception as e: logger.error(f"Failed to call save_position mcp-tool: {str(e)} ", exc_info=True) return f"Failed to call save_position mcp-tool: {str(e)}" @@ -196,7 +198,7 @@ class NavServer: params = { "data": [data.model_dump() for data in datas] } - return await self.publish_Cmd("123456", user_id, "guide", params) + return await self.publish_Cmd("111111", user_id, "guide", params) except Exception as e: logger.error(f"Failed to call guide mcp-tool: {str(e)} ", exc_info=True) return f"Failed to call guide mcp-tool: {str(e)}" @@ -263,12 +265,22 @@ async def serve() -> None: "description": "引导接待客人到这个位置", "minLength": 1 }, + "name": { + "type": "string", + "description": "客人姓名", + "minLength": 1 + }, + "waitingTime": { + "type": "integer", + "description": "等待时间(单位:毫秒)", + "default": 20*60*1000 + }, "user_id": { "type": "string", "description": "用户ID" } }, - "required": ["location", "user_id"] + "required": ["location", "name", "user_id"] } ), # Tool( @@ -437,7 +449,9 @@ async def serve() -> None: raise ValueError("缺少必要参数: location or user_id") result = await nav_server.reception( user_id=arguments["user_id"], - location=arguments["location"] + location=arguments["location"], + name=arguments.get("name", ""), + waitingTime=arguments.get("waitingTime", 20*60*1000) ) # elif name == "notification": # if "location" not in arguments or "text" not in arguments or "user_id" not in arguments: