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
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
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: