feat: 新增飞书和文件工具MCP服务并优化机器人配置

- 新增飞书MCP服务,包含图片上传和消息卡片发送功能
- 新增文件工具MCP服务,提供文件转JSON和data URI功能
- 更新机器人MCP服务配置,移除环境变量硬编码
- 升级机器人版本至0.1.28并优化迎宾功能参数
- 添加.gitignore和项目配置文件
This commit is contained in:
2026-02-11 18:57:31 +08:00
parent ff39bdbd8a
commit 0a308726a6
20 changed files with 1005 additions and 22 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
__pycache__/

117
file_tools/README.md Normal file
View File

@@ -0,0 +1,117 @@
# File Tools MCP 技能包
该技能包提供文件与 URL 的 Base64 处理能力,主要用于将文件内容转换为 JSON 字符串或 data URI供 MCP Agent 直接调用。
## 功能
- 文件对象转 JSON 字符串
- 文件对象转 data URIBase64
- 文件路径转 data URIBase64
- 文件 URL 转 data URIBase64
## 工具列表
### 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"
]
}
}
}
```

View File

@@ -0,0 +1 @@

View File

@@ -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()

View File

@@ -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}")]

View File

@@ -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": {}
}
}
}

20
file_tools/pyproject.toml Normal file
View File

@@ -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"]

67
file_tools/test_tools.py Normal file
View File

@@ -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()

View File

@@ -0,0 +1 @@
__all__ = ["main"]

View File

@@ -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", "")

View File

@@ -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()

View File

@@ -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"<font color=\"grey\">时间</font> {current_time}",
"i18n_content": {
"en_us": f"<font color=\"grey\">Incident time</font>\n{current_time}"
},
"text_align": "center",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
},
{
"tag": "markdown",
"content": f"\n<person id='{person_id}' show_name=true show_avatar=true style='normal'></person><font color=\"grey\">进入仓库</font>",
"i18n_content": {
"en_us": "<font color=\"grey\">Alert details</font>\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}")]

View File

@@ -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"
}
}
}
}

View File

@@ -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"]

38
lzwcai_lark_mcp/test.py Normal file
View File

@@ -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()

View File

@@ -1,17 +1,13 @@
{ {
"mcpServers": { "mcpServers": {
"terminal-temi-mcp": { "terminal_temi_mcp": {
"disabled": false, "disabled": false,
"type": "stdio", "type": "stdio",
"timeout": 30, "timeout": 30,
"command": "uvx", "command": "uvx",
"args": [ "args": [
"terminal_temi_mcp" "terminal_temi_mcp"
], ]
"env": {
"employeeId": "$employeeId$",
"userId": "$employeeId$"
}
} }
} }
} }

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "terminal_temi_mcp" name = "terminal_temi_mcp"
version = "0.1.21" version = "0.1.28"
description = "MQTT-based navigation server for robot" description = "MQTT-based navigation server for robot"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [

View File

@@ -50,7 +50,7 @@ class NavServer:
params = { params = {
"data": {"location": location, "flag": flag} "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: except Exception as e:
logger.error(f"Failed to call navigation mcp-tool: {str(e)} ", exc_info=True) logger.error(f"Failed to call navigation mcp-tool: {str(e)} ", exc_info=True)
return f"Failed to call navigation mcp-tool: {str(e)}" return f"Failed to call navigation mcp-tool: {str(e)}"
@@ -64,12 +64,12 @@ class NavServer:
params = { params = {
"data": {"speech": speech} "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: except Exception as e:
logger.error(f"Failed to call speak mcp-tool: {str(e)} ", exc_info=True) logger.error(f"Failed to call speak mcp-tool: {str(e)} ", exc_info=True)
return f"Failed to call speak mcp-tool: {str(e)}" 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: try:
if not location: if not location:
@@ -77,10 +77,12 @@ class NavServer:
else: else:
params = { params = {
"data": { "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: except Exception as e:
logger.error(f"Failed to call reception mcp-tool: {str(e)} ", exc_info=True) logger.error(f"Failed to call reception mcp-tool: {str(e)} ", exc_info=True)
return f"Failed to call reception mcp-tool: {str(e)}" return f"Failed to call reception mcp-tool: {str(e)}"
@@ -97,7 +99,7 @@ class NavServer:
"text": text "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: except Exception as e:
logger.error(f"Failed to call notification mcp-tool: {str(e)} ", exc_info=True) logger.error(f"Failed to call notification mcp-tool: {str(e)} ", exc_info=True)
return f"Failed to call notification mcp-tool: {str(e)}" return f"Failed to call notification mcp-tool: {str(e)}"
@@ -110,7 +112,7 @@ class NavServer:
"action": "repose" "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: except Exception as e:
logger.error(f"Failed to call repose mcp-tool: {str(e)} ", exc_info=True) logger.error(f"Failed to call repose mcp-tool: {str(e)} ", exc_info=True)
return f"Failed to call repose mcp-tool: {str(e)}" return f"Failed to call repose mcp-tool: {str(e)}"
@@ -127,7 +129,7 @@ class NavServer:
"next_location": next_location "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: except Exception as e:
logger.error(f"Failed to call delivery mcp-tool: {str(e)} ", exc_info=True) logger.error(f"Failed to call delivery mcp-tool: {str(e)} ", exc_info=True)
return f"Failed to call delivery mcp-tool: {str(e)}" return f"Failed to call delivery mcp-tool: {str(e)}"
@@ -141,7 +143,7 @@ class NavServer:
params = { params = {
"data": {"locations": locations} "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: except Exception as e:
logger.error(f"Failed to call patrol mcp-tool: {str(e)} ", exc_info=True) logger.error(f"Failed to call patrol mcp-tool: {str(e)} ", exc_info=True)
return f"Failed to call patrol mcp-tool: {str(e)}" return f"Failed to call patrol mcp-tool: {str(e)}"
@@ -155,7 +157,7 @@ class NavServer:
params = { params = {
"data": {} "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: except Exception as e:
logger.error(f"Failed to call face recognize mcp-tool: {str(e)} ", exc_info=True) 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)}" return f"Failed to call face recognize mcp-tool: {str(e)}"
@@ -169,7 +171,7 @@ class NavServer:
params = { params = {
"data": {} "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: except Exception as e:
logger.error(f"Failed to call QR code scan mcp-tool: {str(e)} ", exc_info=True) 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)}" return f"Failed to call QR code scan mcp-tool: {str(e)}"
@@ -183,7 +185,7 @@ class NavServer:
params = { params = {
"data": {"location_name": location_name} "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: except Exception as e:
logger.error(f"Failed to call save_position mcp-tool: {str(e)} ", exc_info=True) 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)}" return f"Failed to call save_position mcp-tool: {str(e)}"
@@ -196,7 +198,7 @@ class NavServer:
params = { params = {
"data": [data.model_dump() for data in datas] "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: except Exception as e:
logger.error(f"Failed to call guide mcp-tool: {str(e)} ", exc_info=True) logger.error(f"Failed to call guide mcp-tool: {str(e)} ", exc_info=True)
return f"Failed to call guide mcp-tool: {str(e)}" return f"Failed to call guide mcp-tool: {str(e)}"
@@ -263,12 +265,22 @@ async def serve() -> None:
"description": "引导接待客人到这个位置", "description": "引导接待客人到这个位置",
"minLength": 1 "minLength": 1
}, },
"name": {
"type": "string",
"description": "客人姓名",
"minLength": 1
},
"waitingTime": {
"type": "integer",
"description": "等待时间(单位:毫秒)",
"default": 20*60*1000
},
"user_id": { "user_id": {
"type": "string", "type": "string",
"description": "用户ID" "description": "用户ID"
} }
}, },
"required": ["location", "user_id"] "required": ["location", "name", "user_id"]
} }
), ),
# Tool( # Tool(
@@ -437,7 +449,9 @@ async def serve() -> None:
raise ValueError("缺少必要参数: location or user_id") raise ValueError("缺少必要参数: location or user_id")
result = await nav_server.reception( result = await nav_server.reception(
user_id=arguments["user_id"], 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": # elif name == "notification":
# if "location" not in arguments or "text" not in arguments or "user_id" not in arguments: # if "location" not in arguments or "text" not in arguments or "user_id" not in arguments: