wechat-oa-mcp-direct 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.4
2
+ Name: wechat-oa-mcp-direct
3
+ Version: 0.2.0
4
+ Summary: 微信公众号 MCP 服务器(直连微信官方 API 版):草稿创建/发布/删除、素材删除、access_token 获取
5
+ Author-email: Jupiter <jupiter3019@163.com>
6
+ License: MIT
7
+ Keywords: wechat,mcp,fastmcp,公众号
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: fastmcp>=4.0
11
+ Requires-Dist: requests>=2.25
12
+
13
+ # wechat-oa-mcp-direct
14
+
15
+ 微信公众号 MCP 服务器 —— **直连微信官方 API 版**。
16
+
17
+ 基于 FastMCP 4.x 实现,提供公众号草稿创建 / 发布 / 删除、永久素材删除、access_token 获取共 5 个 MCP 工具。所有请求直连 `api.weixin.qq.com`,**不经过任何第三方中转服务器**,AppID / AppSecret 只在部署机与微信之间传输。
18
+
19
+ > 与 PyPI 上的 `wechat-oa-mcp 0.1.0` 的区别:旧版会把你的 AppSecret 转发到作者的服务器(106.15.125.133)中转,且依赖已废弃的 MCP SDK v1 API,无法在新环境启动。本包修复了上述问题(含 41001 access_token missing 修复)。
20
+
21
+ ---
22
+
23
+ > ## ⚠️ 部署前必读:IP 白名单(最容易踩的坑)
24
+ >
25
+ > 部署好后首次调用工具,如果返回 **`40164 invalid ip ... not in whitelist`** —— **不是代码问题**,是这台部署机的公网出口 IP 还没有加进公众号的 IP 白名单。
26
+ >
27
+ > - **每台部署机器都要把「它自己的出口 IP」加进白名单**(不同机器的 IP 不一样);
28
+ > - **换网络、换服务器、IP 变化后都要重新加**,否则之前能用的机器会突然报 40164;
29
+ > - 获取出口 IP 和配置方法见下文「2.2 配置 IP 白名单」。
30
+
31
+ ---
32
+
33
+ ## 1. 安装
34
+
35
+ ### Windows CMD 一键安装(最简便,无需虚拟环境)
36
+
37
+ 解压 zip 后,在 CMD 中进入解压出来的目录,一条命令安装:
38
+
39
+ ```cmd
40
+ cd wechat-oa-mcp-direct
41
+ pip install .
42
+ ```
43
+
44
+ 安装完成后验证:
45
+
46
+ ```cmd
47
+ wechat-oa-mcp --help
48
+ ```
49
+
50
+ ### macOS / Linux,或需要环境隔离时
51
+
52
+ ```bash
53
+ cd wechat-oa-mcp-direct
54
+ python3 -m venv .venv
55
+ source .venv/bin/activate # Windows 虚拟环境: .venv\Scripts\activate
56
+ pip install .
57
+ ```
58
+
59
+ 要求:Python >= 3.10(Windows 安装 Python 时记得勾选 **"Add Python to PATH"**,否则 CMD 里 `pip` 会提示找不到命令)。
60
+
61
+ ## 2. 前置配置(部署机必须完成)
62
+
63
+ ### 2.1 获取公众号凭证(AppID / AppSecret 在哪里)
64
+
65
+ 1. 用管理员微信扫码登录微信公众平台:https://mp.weixin.qq.com(需要先注册一个公众号,订阅号/服务号均可);
66
+ 2. 左侧菜单点 **「设置与开发」→「基本配置」**;
67
+ 3. 页面上的 **开发者ID(AppID)** 就是 AppID;
68
+ 4. **开发者密码(AppSecret)** 点击「重置」后生成——**只会完整显示这一次**,请立刻复制保存;之后忘记只能再点「重置」(需管理员扫码确认),且重置后旧 AppSecret 立即失效。
69
+
70
+ ### 2.2 配置 IP 白名单(关键,编辑位置在这里)
71
+
72
+ 1. 登录微信公众平台:https://mp.weixin.qq.com;
73
+ 2. 左侧菜单点 **「设置与开发」→「开发接口管理」**;
74
+ 3. 找到 **「IP白名单」** 栏目,点右侧的 **「修改」**;
75
+ 4. 输入 **部署本服务器那台机器的公网出口 IP**(可填多个,用逗号分隔),点 **「确认修改」** 保存。
76
+
77
+ > 如何获取部署机的出口 IP:在部署机的 CMD / 终端执行
78
+ > ```cmd
79
+ > curl "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=你的AppID&secret=你的AppSecret"
80
+ > ```
81
+ > 若返回 `40164 invalid ip <你的IP> not in whitelist`,尖括号里的 IP 就是要填进白名单的那个;填好后再执行一次,直到返回 `access_token` 即配置成功。
82
+ >
83
+ > ⚠️ 出口 IP 可能随网络环境变化(如家庭宽带重拨、换服务器),**换网络后必须重新加白名单**。
84
+
85
+ ## 3. 启动服务器
86
+
87
+ ```bash
88
+ # 默认 SSE 协议,端口 8000(交互式终端)
89
+ wechat-oa-mcp
90
+ # 或
91
+ python -m wechat_oa_mcp
92
+
93
+ # 指定端口 / 协议 / 调试模式
94
+ wechat-oa-mcp --port 8123
95
+ wechat-oa-mcp --transport stdio # stdio 模式(客户端自行拉起时自动生效)
96
+ wechat-oa-mcp --debug
97
+ ```
98
+
99
+ > ⚠️ **后台 / 服务化部署(nohup、systemd、launchd 等)必须显式加 `--transport sse`**:
100
+ > ```bash
101
+ > nohup wechat-oa-mcp --transport sse --port 8000 > mcp.log 2>&1 &
102
+ > ```
103
+ > 因为非终端环境下程序无法区分「后台运行」与「被 MCP 客户端以管道拉起」,未显式指定协议时默认走 stdio(供 npx inspector / 客户端拉起使用),此时不会监听端口。
104
+
105
+ 启动后 SSE 端点:`http://localhost:8000/sse`
106
+
107
+ ## 4. 接入 MCP 客户端
108
+
109
+ ### SSE 方式(需服务器保持运行)
110
+
111
+ ```json
112
+ {
113
+ "mcpServers": {
114
+ "wechat_oa_mcp": {
115
+ "type": "sse",
116
+ "url": "http://localhost:8000/sse"
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ ### stdio 方式(客户端自动拉起,推荐)
123
+
124
+ ```json
125
+ {
126
+ "mcpServers": {
127
+ "wechat_oa_mcp": {
128
+ "type": "stdio",
129
+ "command": "wechat-oa-mcp",
130
+ "args": ["--transport", "stdio"]
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ > 若 `command` 用虚拟环境安装,请填 venv 内的绝对路径,如 `/path/to/.venv/bin/wechat-oa-mcp`。
137
+
138
+ ## 5. 可用工具
139
+
140
+ | 工具 | 必填参数 | 说明 |
141
+ |---|---|---|
142
+ | `WeChat_get_access_token` | AppID, AppSecret | 获取 access_token(有效期 7200 秒) |
143
+ | `WeChat_create_draft` | access_token, image_url, title, content | 下载封面图→上传永久素材→创建图文草稿;可选 author / digest / content_source_url / need_open_comment |
144
+ | `WeChat_publish_draft` | access_token, draft_media_id | 发布草稿(异步任务,返回 publish_id) |
145
+ | `WeChat_del_draft` | access_token, media_id | 删除草稿 |
146
+ | `WeChat_del_material` | access_token, media_id | 删除永久素材 |
147
+
148
+ **调用顺序**:先 `WeChat_get_access_token` 拿 token,再调其余工具(token 2 小时内有效)。
149
+
150
+ **注意**:`WeChat_create_draft` 会真实上传图片并在公众号后台创建草稿,属真实写操作,建议先在测试号或测试内容上验证。
151
+
152
+ ## 6. 常见错误
153
+
154
+ | 错误码 | 含义 | 处理 |
155
+ |---|---|---|
156
+ | 40164 | IP 不在白名单 | 把部署机出口 IP 加入公众号 IP 白名单 |
157
+ | 41001 | access_token missing / invalid | 确认已先调用 get_access_token 且 token 未过期 |
158
+ | 40007 | invalid media_id | media_id 不存在或已删除 |
159
+ | 45009 | 接口调用超限 | 微信接口有频率限制,稍后重试 |
160
+
161
+ ## 7. 验证安装
162
+
163
+ ```bash
164
+ # 检查命令可用
165
+ wechat-oa-mcp --help
166
+
167
+ # 无副作用验证(不创建任何资源):
168
+ # 用 MCP Inspector 或客户端调用 WeChat_get_access_token,成功即说明配置就绪
169
+ npx @modelcontextprotocol/inspector python -m wechat_oa_mcp
170
+ ```
171
+
172
+ ## 免责声明
173
+
174
+ 本工具仅限研究用途,禁止用于商业目的。使用者需自行遵守微信公众平台服务条款。
@@ -0,0 +1,162 @@
1
+ # wechat-oa-mcp-direct
2
+
3
+ 微信公众号 MCP 服务器 —— **直连微信官方 API 版**。
4
+
5
+ 基于 FastMCP 4.x 实现,提供公众号草稿创建 / 发布 / 删除、永久素材删除、access_token 获取共 5 个 MCP 工具。所有请求直连 `api.weixin.qq.com`,**不经过任何第三方中转服务器**,AppID / AppSecret 只在部署机与微信之间传输。
6
+
7
+ > 与 PyPI 上的 `wechat-oa-mcp 0.1.0` 的区别:旧版会把你的 AppSecret 转发到作者的服务器(106.15.125.133)中转,且依赖已废弃的 MCP SDK v1 API,无法在新环境启动。本包修复了上述问题(含 41001 access_token missing 修复)。
8
+
9
+ ---
10
+
11
+ > ## ⚠️ 部署前必读:IP 白名单(最容易踩的坑)
12
+ >
13
+ > 部署好后首次调用工具,如果返回 **`40164 invalid ip ... not in whitelist`** —— **不是代码问题**,是这台部署机的公网出口 IP 还没有加进公众号的 IP 白名单。
14
+ >
15
+ > - **每台部署机器都要把「它自己的出口 IP」加进白名单**(不同机器的 IP 不一样);
16
+ > - **换网络、换服务器、IP 变化后都要重新加**,否则之前能用的机器会突然报 40164;
17
+ > - 获取出口 IP 和配置方法见下文「2.2 配置 IP 白名单」。
18
+
19
+ ---
20
+
21
+ ## 1. 安装
22
+
23
+ ### Windows CMD 一键安装(最简便,无需虚拟环境)
24
+
25
+ 解压 zip 后,在 CMD 中进入解压出来的目录,一条命令安装:
26
+
27
+ ```cmd
28
+ cd wechat-oa-mcp-direct
29
+ pip install .
30
+ ```
31
+
32
+ 安装完成后验证:
33
+
34
+ ```cmd
35
+ wechat-oa-mcp --help
36
+ ```
37
+
38
+ ### macOS / Linux,或需要环境隔离时
39
+
40
+ ```bash
41
+ cd wechat-oa-mcp-direct
42
+ python3 -m venv .venv
43
+ source .venv/bin/activate # Windows 虚拟环境: .venv\Scripts\activate
44
+ pip install .
45
+ ```
46
+
47
+ 要求:Python >= 3.10(Windows 安装 Python 时记得勾选 **"Add Python to PATH"**,否则 CMD 里 `pip` 会提示找不到命令)。
48
+
49
+ ## 2. 前置配置(部署机必须完成)
50
+
51
+ ### 2.1 获取公众号凭证(AppID / AppSecret 在哪里)
52
+
53
+ 1. 用管理员微信扫码登录微信公众平台:https://mp.weixin.qq.com(需要先注册一个公众号,订阅号/服务号均可);
54
+ 2. 左侧菜单点 **「设置与开发」→「基本配置」**;
55
+ 3. 页面上的 **开发者ID(AppID)** 就是 AppID;
56
+ 4. **开发者密码(AppSecret)** 点击「重置」后生成——**只会完整显示这一次**,请立刻复制保存;之后忘记只能再点「重置」(需管理员扫码确认),且重置后旧 AppSecret 立即失效。
57
+
58
+ ### 2.2 配置 IP 白名单(关键,编辑位置在这里)
59
+
60
+ 1. 登录微信公众平台:https://mp.weixin.qq.com;
61
+ 2. 左侧菜单点 **「设置与开发」→「开发接口管理」**;
62
+ 3. 找到 **「IP白名单」** 栏目,点右侧的 **「修改」**;
63
+ 4. 输入 **部署本服务器那台机器的公网出口 IP**(可填多个,用逗号分隔),点 **「确认修改」** 保存。
64
+
65
+ > 如何获取部署机的出口 IP:在部署机的 CMD / 终端执行
66
+ > ```cmd
67
+ > curl "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=你的AppID&secret=你的AppSecret"
68
+ > ```
69
+ > 若返回 `40164 invalid ip <你的IP> not in whitelist`,尖括号里的 IP 就是要填进白名单的那个;填好后再执行一次,直到返回 `access_token` 即配置成功。
70
+ >
71
+ > ⚠️ 出口 IP 可能随网络环境变化(如家庭宽带重拨、换服务器),**换网络后必须重新加白名单**。
72
+
73
+ ## 3. 启动服务器
74
+
75
+ ```bash
76
+ # 默认 SSE 协议,端口 8000(交互式终端)
77
+ wechat-oa-mcp
78
+ # 或
79
+ python -m wechat_oa_mcp
80
+
81
+ # 指定端口 / 协议 / 调试模式
82
+ wechat-oa-mcp --port 8123
83
+ wechat-oa-mcp --transport stdio # stdio 模式(客户端自行拉起时自动生效)
84
+ wechat-oa-mcp --debug
85
+ ```
86
+
87
+ > ⚠️ **后台 / 服务化部署(nohup、systemd、launchd 等)必须显式加 `--transport sse`**:
88
+ > ```bash
89
+ > nohup wechat-oa-mcp --transport sse --port 8000 > mcp.log 2>&1 &
90
+ > ```
91
+ > 因为非终端环境下程序无法区分「后台运行」与「被 MCP 客户端以管道拉起」,未显式指定协议时默认走 stdio(供 npx inspector / 客户端拉起使用),此时不会监听端口。
92
+
93
+ 启动后 SSE 端点:`http://localhost:8000/sse`
94
+
95
+ ## 4. 接入 MCP 客户端
96
+
97
+ ### SSE 方式(需服务器保持运行)
98
+
99
+ ```json
100
+ {
101
+ "mcpServers": {
102
+ "wechat_oa_mcp": {
103
+ "type": "sse",
104
+ "url": "http://localhost:8000/sse"
105
+ }
106
+ }
107
+ }
108
+ ```
109
+
110
+ ### stdio 方式(客户端自动拉起,推荐)
111
+
112
+ ```json
113
+ {
114
+ "mcpServers": {
115
+ "wechat_oa_mcp": {
116
+ "type": "stdio",
117
+ "command": "wechat-oa-mcp",
118
+ "args": ["--transport", "stdio"]
119
+ }
120
+ }
121
+ }
122
+ ```
123
+
124
+ > 若 `command` 用虚拟环境安装,请填 venv 内的绝对路径,如 `/path/to/.venv/bin/wechat-oa-mcp`。
125
+
126
+ ## 5. 可用工具
127
+
128
+ | 工具 | 必填参数 | 说明 |
129
+ |---|---|---|
130
+ | `WeChat_get_access_token` | AppID, AppSecret | 获取 access_token(有效期 7200 秒) |
131
+ | `WeChat_create_draft` | access_token, image_url, title, content | 下载封面图→上传永久素材→创建图文草稿;可选 author / digest / content_source_url / need_open_comment |
132
+ | `WeChat_publish_draft` | access_token, draft_media_id | 发布草稿(异步任务,返回 publish_id) |
133
+ | `WeChat_del_draft` | access_token, media_id | 删除草稿 |
134
+ | `WeChat_del_material` | access_token, media_id | 删除永久素材 |
135
+
136
+ **调用顺序**:先 `WeChat_get_access_token` 拿 token,再调其余工具(token 2 小时内有效)。
137
+
138
+ **注意**:`WeChat_create_draft` 会真实上传图片并在公众号后台创建草稿,属真实写操作,建议先在测试号或测试内容上验证。
139
+
140
+ ## 6. 常见错误
141
+
142
+ | 错误码 | 含义 | 处理 |
143
+ |---|---|---|
144
+ | 40164 | IP 不在白名单 | 把部署机出口 IP 加入公众号 IP 白名单 |
145
+ | 41001 | access_token missing / invalid | 确认已先调用 get_access_token 且 token 未过期 |
146
+ | 40007 | invalid media_id | media_id 不存在或已删除 |
147
+ | 45009 | 接口调用超限 | 微信接口有频率限制,稍后重试 |
148
+
149
+ ## 7. 验证安装
150
+
151
+ ```bash
152
+ # 检查命令可用
153
+ wechat-oa-mcp --help
154
+
155
+ # 无副作用验证(不创建任何资源):
156
+ # 用 MCP Inspector 或客户端调用 WeChat_get_access_token,成功即说明配置就绪
157
+ npx @modelcontextprotocol/inspector python -m wechat_oa_mcp
158
+ ```
159
+
160
+ ## 免责声明
161
+
162
+ 本工具仅限研究用途,禁止用于商业目的。使用者需自行遵守微信公众平台服务条款。
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "wechat-oa-mcp-direct"
7
+ version = "0.2.0"
8
+ description = "微信公众号 MCP 服务器(直连微信官方 API 版):草稿创建/发布/删除、素材删除、access_token 获取"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Jupiter", email = "jupiter3019@163.com" }]
13
+ keywords = ["wechat", "mcp", "fastmcp", "公众号"]
14
+ dependencies = [
15
+ "fastmcp>=4.0",
16
+ "requests>=2.25",
17
+ ]
18
+
19
+ [project.scripts]
20
+ wechat-oa-mcp = "wechat_oa_mcp.cli:main"
21
+ wechat-oa-mcp-direct = "wechat_oa_mcp.cli:main"
22
+
23
+ [tool.setuptools]
24
+ packages = ["wechat_oa_mcp"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,24 @@
1
+ """
2
+ 微信公众号MCP服务器源代码包
3
+
4
+ 此 MCP 服务器仅限研究用途,禁止用于商业目的。
5
+ """
6
+
7
+ # 导入所有公开API
8
+ from wechat_oa_mcp.server import (
9
+ WeChat_get_access_token,
10
+ WeChat_create_draft,
11
+ WeChat_publish_draft,
12
+ WeChat_del_draft,
13
+ WeChat_del_material,
14
+ run
15
+ )
16
+
17
+ __all__ = [
18
+ 'WeChat_get_access_token',
19
+ 'WeChat_create_draft',
20
+ 'WeChat_publish_draft',
21
+ 'WeChat_del_draft',
22
+ 'WeChat_del_material',
23
+ 'run'
24
+ ]
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ WeChat OA MCP 包的直接执行入口点
4
+ 当使用 python -m wechat_oa_mcp 命令时会执行此文件
5
+ """
6
+
7
+ from wechat_oa_mcp.cli import main
8
+
9
+ if __name__ == "__main__":
10
+ main()
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ WeChat OA MCP 服务器命令行入口模块
4
+ 提供便捷的启动服务器和调用微信公众号API的功能
5
+ """
6
+
7
+ import argparse
8
+ import sys
9
+ import os
10
+ from wechat_oa_mcp.server import run
11
+
12
+ def main():
13
+ """
14
+ 命令行入口点函数
15
+ 启动WeChat OA MCP服务器
16
+ """
17
+ # 创建命令行参数解析器
18
+ parser = argparse.ArgumentParser(description="WeChat OA MCP 服务器")
19
+ parser.add_argument("--port", type=int, help="指定服务器端口")
20
+ parser.add_argument("--transport", type=str, choices=["sse", "stdio", "streamable-http"],
21
+ default=None, help="指定通信协议(默认 sse;未指定且通过管道运行时自动切换 stdio)")
22
+ parser.add_argument("--debug", action="store_true", help="启用调试模式")
23
+ args = parser.parse_args()
24
+
25
+ # 检测是否通过管道方式运行(如 npx inspector)
26
+ is_piped = not os.isatty(sys.stdin.fileno()) or not os.isatty(sys.stdout.fileno())
27
+
28
+ # 显式指定 --transport 时尊重用户选择(后台/服务化部署场景);
29
+ # 未指定时:管道运行(npx inspector)切 stdio,否则默认 sse
30
+ if args.transport is not None:
31
+ transport = args.transport
32
+ else:
33
+ transport = "stdio" if is_piped else "sse"
34
+
35
+ try:
36
+ run(transport=transport, port=args.port, debug=args.debug)
37
+ except KeyboardInterrupt:
38
+ print("\n服务器已停止")
39
+ except Exception as e:
40
+ print(f"服务器异常: {str(e)}")
41
+ return 1
42
+ return 0
43
+
44
+ if __name__ == "__main__":
45
+ main()
@@ -0,0 +1,380 @@
1
+ """
2
+ 微信公众号MCP服务器核心实现(直连微信官方API版)
3
+
4
+ 本版本由原包改造:
5
+ - 移除第三方中转服务器(106.15.125.133),全部请求直连微信官方接口 api.weixin.qq.com
6
+ - 适配 FastMCP 4.x(fastmcp 包,而非 mcp.server.fastmcp)
7
+
8
+ 使用前置条件:
9
+ 1. 公众号后台「设置与开发 → 开发接口管理」获取 AppID / AppSecret
10
+ 2. 将本机公网出口 IP 加入公众号「IP白名单」(否则获取 access_token 会返回 40164)
11
+ 3. 注意微信接口调用频率限制
12
+ """
13
+
14
+ import os
15
+ import tempfile
16
+ from typing import Any, Dict
17
+
18
+ import requests
19
+ from fastmcp import FastMCP
20
+
21
+ # 微信官方 API 基础地址
22
+ API_BASE = "https://api.weixin.qq.com"
23
+
24
+ # 创建 FastMCP 服务器实例
25
+ mcp = FastMCP("wechat_oa_mcp")
26
+
27
+
28
+ def _unwrap(args) -> Dict[str, Any]:
29
+ """兼容两种入参形式:直接字典,或带 input 属性的对象"""
30
+ if hasattr(args, "input"):
31
+ return args.input
32
+ return args
33
+
34
+
35
+ def _post(url: str, payload: Dict[str, Any] = None, timeout: int = 10, access_token: str = None) -> Dict[str, Any]:
36
+ """发送 JSON POST 请求并解析微信响应,业务错误抛出 ValueError
37
+
38
+ 注意:微信的 POST 接口要求 access_token 放在 URL query 中,不能放在 body 里,
39
+ 否则会返回 41001 access_token missing。
40
+ """
41
+ params = {"access_token": access_token} if access_token else None
42
+ resp = requests.post(url, json=payload, params=params, timeout=timeout)
43
+ resp.raise_for_status()
44
+ data = resp.json()
45
+ if isinstance(data, dict) and data.get("errcode") not in (None, 0):
46
+ raise ValueError(f"微信API错误: {data.get('errcode')} {data.get('errmsg')}")
47
+ return data
48
+
49
+
50
+ def _upload_image_to_material(access_token: str, image_url: str) -> str:
51
+ """下载远程封面图并上传为永久图片素材,返回 thumb_media_id"""
52
+ img_resp = requests.get(image_url, timeout=20)
53
+ img_resp.raise_for_status()
54
+
55
+ suffix = os.path.splitext(image_url.split("?")[0])[1] or ".jpg"
56
+ fd, tmp_path = tempfile.mkstemp(suffix=suffix)
57
+ try:
58
+ with os.fdopen(fd, "wb") as f:
59
+ f.write(img_resp.content)
60
+ with open(tmp_path, "rb") as f:
61
+ up_resp = requests.post(
62
+ f"{API_BASE}/cgi-bin/material/add_material",
63
+ params={"access_token": access_token, "type": "image"},
64
+ files={"media": (os.path.basename(tmp_path), f, "application/octet-stream")},
65
+ timeout=60,
66
+ )
67
+ up_resp.raise_for_status()
68
+ data = up_resp.json()
69
+ if data.get("errcode") not in (None, 0):
70
+ raise ValueError(f"上传封面图失败: {data.get('errcode')} {data.get('errmsg')}")
71
+ media_id = data.get("media_id")
72
+ if not media_id:
73
+ raise ValueError(f"上传封面图响应缺少media_id: {data}")
74
+ return media_id
75
+ finally:
76
+ try:
77
+ os.unlink(tmp_path)
78
+ except OSError:
79
+ pass
80
+
81
+
82
+ @mcp.tool()
83
+ def WeChat_get_access_token(args: Dict[str, Any]) -> Dict[str, Any]:
84
+ """
85
+ 获取微信公众号Access Token
86
+
87
+ Metadata:
88
+ - input: AppID, AppSecret
89
+ - output: success, error, access_token, expires_in
90
+
91
+ 参数说明:
92
+ - AppID: 公众号唯一凭证(设置与开发-开发接口管理中获取);未传时自动读取环境变量 WECHAT_APPID
93
+ - AppSecret: 公众号凭证密钥(设置与开发-开发接口管理中获取);未传时自动读取环境变量 WECHAT_APPSECRET
94
+ - expires_in: 凭证有效时间(秒),默认7200
95
+ """
96
+ p = _unwrap(args)
97
+ logger = getattr(args, "logger", None) or type("", (), {"debug": print, "error": print})
98
+ try:
99
+ appid = p.get("AppID") or os.environ.get("WECHAT_APPID", "")
100
+ appsecret = p.get("AppSecret") or os.environ.get("WECHAT_APPSECRET", "")
101
+ if not appid or not appsecret:
102
+ raise ValueError("缺少 AppID/AppSecret:请传入参数或设置环境变量 WECHAT_APPID / WECHAT_APPSECRET")
103
+ resp = requests.get(
104
+ f"{API_BASE}/cgi-bin/token",
105
+ params={
106
+ "grant_type": "client_credential",
107
+ "appid": appid,
108
+ "secret": appsecret,
109
+ },
110
+ timeout=10,
111
+ )
112
+ resp.raise_for_status()
113
+ result = resp.json()
114
+ if result.get("errcode") not in (None, 0):
115
+ raise ValueError(f"获取access_token失败: {result.get('errcode')} {result.get('errmsg')}")
116
+ if not result.get("access_token"):
117
+ raise ValueError(f"未能获取有效的access_token: {result}")
118
+ logger.debug(f"获取token成功,expires_in={result.get('expires_in')}")
119
+ return {
120
+ "success": True,
121
+ "error": None,
122
+ "access_token": result.get("access_token"),
123
+ "expires_in": result.get("expires_in", 7200),
124
+ }
125
+ except requests.exceptions.RequestException as e:
126
+ logger.error(f"HTTP请求失败: {str(e)}")
127
+ return {"success": False, "error": f"HTTP请求异常: {str(e)}", "access_token": None, "expires_in": None}
128
+ except ValueError as e:
129
+ logger.error(f"业务错误: {str(e)}")
130
+ return {"success": False, "error": str(e), "access_token": None, "expires_in": None}
131
+ except Exception as e:
132
+ logger.error(f"未知错误: {str(e)}")
133
+ return {"success": False, "error": f"系统错误: {str(e)}", "access_token": None, "expires_in": None}
134
+
135
+
136
+ @mcp.tool()
137
+ def WeChat_create_draft(args: Dict[str, Any]) -> Dict[str, Any]:
138
+ """
139
+ 创建微信公众号草稿
140
+
141
+ Metadata:
142
+ - input: access_token, image_url, title, content, author, digest, content_source_url, need_open_comment
143
+ - output: success, error, draft_media_id, image_media_id
144
+
145
+ 参数说明:
146
+ - access_token: 接口凭证(通过WeChat_get_access_token获取)
147
+ - image_url: 封面图片URL(将被下载并上传为永久素材)
148
+ - title: 文章标题
149
+ - content: 图文消息内容,支持HTML,<2万字符,<1M
150
+ - author: (可选)作者名称
151
+ - digest: (可选)摘要,默认抓取正文前54字
152
+ - content_source_url: (可选)原文URL,点击"阅读原文"跳转
153
+ - need_open_comment: (可选)0关闭评论(默认),1开启
154
+ """
155
+ p = _unwrap(args)
156
+ logger = getattr(args, "logger", None) or type("", (), {"debug": print, "error": print})
157
+ try:
158
+ access_token = p.get("access_token", "")
159
+ image_url = p.get("image_url", "")
160
+ title = p.get("title", "")
161
+ content = p.get("content", "")
162
+ if not access_token or not image_url or not title or not content:
163
+ raise ValueError("缺少必填参数: access_token/image_url/title/content")
164
+
165
+ # 1. 上传封面图为永久图片素材
166
+ thumb_media_id = _upload_image_to_material(access_token, image_url)
167
+ logger.debug(f"封面图上传成功,thumb_media_id={thumb_media_id}")
168
+
169
+ # 2. 组装图文消息
170
+ article: Dict[str, Any] = {
171
+ "title": title,
172
+ "content": content,
173
+ "thumb_media_id": thumb_media_id,
174
+ }
175
+ if p.get("author"):
176
+ article["author"] = p.get("author")
177
+ if p.get("digest"):
178
+ article["digest"] = p.get("digest")
179
+ if p.get("content_source_url"):
180
+ article["content_source_url"] = p.get("content_source_url")
181
+ if p.get("need_open_comment") in (0, 1):
182
+ article["need_open_comment"] = int(p.get("need_open_comment"))
183
+ if p.get("need_open_reprint") in (0, 1):
184
+ article["need_open_reprint"] = int(p.get("need_open_reprint"))
185
+
186
+ # 3. 创建草稿
187
+ result = _post(
188
+ f"{API_BASE}/cgi-bin/draft/add",
189
+ payload={"articles": [article]},
190
+ timeout=30,
191
+ access_token=access_token,
192
+ )
193
+ draft_media_id = result.get("media_id")
194
+ if not draft_media_id:
195
+ raise ValueError(f"创建草稿响应缺少media_id: {result}")
196
+ logger.debug(f"草稿创建成功,draft_media_id={draft_media_id}")
197
+ return {
198
+ "success": True,
199
+ "error": None,
200
+ "draft_media_id": draft_media_id,
201
+ "image_media_id": thumb_media_id,
202
+ }
203
+ except requests.exceptions.RequestException as e:
204
+ logger.error(f"网络请求异常: {str(e)}")
205
+ return {"success": False, "error": f"网络请求异常: {str(e)}", "draft_media_id": None, "image_media_id": None}
206
+ except ValueError as e:
207
+ logger.error(f"业务错误: {str(e)}")
208
+ return {"success": False, "error": str(e), "draft_media_id": None, "image_media_id": None}
209
+ except Exception as e:
210
+ logger.error(f"未知错误: {str(e)}")
211
+ return {"success": False, "error": f"系统异常: {str(e)}", "draft_media_id": None, "image_media_id": None}
212
+
213
+
214
+ @mcp.tool()
215
+ def WeChat_publish_draft(args: Dict[str, Any]) -> Dict[str, Any]:
216
+ """
217
+ 发布微信公众号草稿
218
+
219
+ Metadata:
220
+ - input: access_token, draft_media_id
221
+ - output: success, error, errmsg, publish_id
222
+
223
+ 参数说明:
224
+ - access_token: 接口凭证(通过WeChat_get_access_token获取)
225
+ - draft_media_id: 草稿ID,即WeChat_create_draft返回的draft_media_id
226
+ - publish_id: 发布任务ID(发布为异步任务,可通过公众号后台查看结果)
227
+ """
228
+ p = _unwrap(args)
229
+ logger = getattr(args, "logger", None) or type("", (), {"debug": print, "error": print})
230
+ try:
231
+ access_token = p.get("access_token", "")
232
+ draft_media_id = p.get("draft_media_id", "")
233
+ if not access_token or not draft_media_id:
234
+ raise ValueError("缺少必填参数: access_token/draft_media_id")
235
+
236
+ result = _post(
237
+ f"{API_BASE}/cgi-bin/freepublish/submit",
238
+ payload={"media_id": draft_media_id},
239
+ timeout=30,
240
+ access_token=access_token,
241
+ )
242
+ publish_id = result.get("publish_id", result.get("msg_id"))
243
+ if not publish_id:
244
+ raise ValueError(f"发布响应缺少publish_id: {result}")
245
+ logger.debug(f"发布任务已提交,publish_id={publish_id}")
246
+ return {
247
+ "success": True,
248
+ "error": None,
249
+ "errmsg": result.get("errmsg"),
250
+ "publish_id": publish_id,
251
+ }
252
+ except requests.exceptions.RequestException as e:
253
+ logger.error(f"网络请求异常: {str(e)}")
254
+ return {"success": False, "error": f"网络请求异常: {str(e)}", "publish_id": None}
255
+ except ValueError as e:
256
+ logger.error(f"业务错误: {str(e)}")
257
+ return {"success": False, "error": str(e), "publish_id": None}
258
+ except Exception as e:
259
+ logger.error(f"未知错误: {str(e)}")
260
+ return {"success": False, "error": f"系统错误: {str(e)}", "publish_id": None}
261
+
262
+
263
+ @mcp.tool()
264
+ def WeChat_del_draft(args: Dict[str, Any]) -> Dict[str, Any]:
265
+ """
266
+ 删除微信公众号草稿
267
+
268
+ Metadata:
269
+ - input: access_token, media_id
270
+ - output: success, error, errcode, errmsg
271
+
272
+ 参数说明:
273
+ - access_token: 接口凭证(通过WeChat_get_access_token获取)
274
+ - media_id: 草稿ID,即WeChat_create_draft返回的draft_media_id
275
+ """
276
+ p = _unwrap(args)
277
+ logger = getattr(args, "logger", None) or type("", (), {"debug": print, "error": print})
278
+ try:
279
+ access_token = p.get("access_token", "")
280
+ media_id = p.get("media_id", "")
281
+ if not access_token or not media_id:
282
+ raise ValueError("缺少必填参数: access_token/media_id")
283
+
284
+ result = _post(
285
+ f"{API_BASE}/cgi-bin/draft/delete",
286
+ payload={"media_id": media_id},
287
+ timeout=15,
288
+ access_token=access_token,
289
+ )
290
+ logger.debug(f"草稿删除成功: {result}")
291
+ return {
292
+ "success": True,
293
+ "error": None,
294
+ "errcode": result.get("errcode"),
295
+ "errmsg": result.get("errmsg"),
296
+ }
297
+ except requests.exceptions.RequestException as e:
298
+ logger.error(f"网络请求异常: {str(e)}")
299
+ return {"success": False, "error": f"网络请求异常: {str(e)}", "errcode": None, "errmsg": None}
300
+ except ValueError as e:
301
+ logger.error(f"业务错误: {str(e)}")
302
+ return {"success": False, "error": str(e), "errcode": None, "errmsg": None}
303
+ except Exception as e:
304
+ logger.error(f"未知错误: {str(e)}")
305
+ return {"success": False, "error": f"系统错误: {str(e)}", "errcode": None, "errmsg": None}
306
+
307
+
308
+ @mcp.tool()
309
+ def WeChat_del_material(args: Dict[str, Any]) -> Dict[str, Any]:
310
+ """
311
+ 删除微信公众号永久素材
312
+
313
+ Metadata:
314
+ - input: access_token, media_id
315
+ - output: success, error, errcode, errmsg
316
+
317
+ 参数说明:
318
+ - access_token: 接口凭证(通过WeChat_get_access_token获取)
319
+ - media_id: 永久素材ID,即WeChat_create_draft返回的image_media_id
320
+ """
321
+ p = _unwrap(args)
322
+ logger = getattr(args, "logger", None) or type("", (), {"debug": print, "error": print})
323
+ try:
324
+ access_token = p.get("access_token", "")
325
+ media_id = p.get("media_id", "")
326
+ if not access_token or not media_id:
327
+ raise ValueError("缺少必填参数: access_token/media_id")
328
+
329
+ result = _post(
330
+ f"{API_BASE}/cgi-bin/material/del_material",
331
+ payload={"media_id": media_id},
332
+ timeout=15,
333
+ access_token=access_token,
334
+ )
335
+ logger.debug(f"素材删除成功: {result}")
336
+ return {
337
+ "success": True,
338
+ "error": None,
339
+ "errcode": result.get("errcode"),
340
+ "errmsg": result.get("errmsg"),
341
+ }
342
+ except requests.exceptions.RequestException as e:
343
+ logger.error(f"网络请求异常: {str(e)}")
344
+ return {"success": False, "error": f"网络请求异常: {str(e)}", "errcode": None, "errmsg": None}
345
+ except ValueError as e:
346
+ logger.error(f"业务错误: {str(e)}")
347
+ return {"success": False, "error": str(e), "errcode": None, "errmsg": None}
348
+ except Exception as e:
349
+ logger.error(f"未知错误: {str(e)}")
350
+ return {"success": False, "error": f"系统错误: {str(e)}", "errcode": None, "errmsg": None}
351
+
352
+
353
+ def run(transport: str = "sse", port: int = None, debug: bool = False) -> None:
354
+ """
355
+ 启动WeChat OA MCP服务器
356
+
357
+ 参数:
358
+ transport: 通信协议,支持 'sse', 'stdio', 'streamable-http'(默认 sse)
359
+ port: 服务器端口,默认 8000
360
+ debug: 是否启用调试模式
361
+ """
362
+ if transport != "stdio" or debug:
363
+ print(f"启动WeChat OA MCP服务器,通信协议: {transport}")
364
+ if port is not None:
365
+ print(f"使用自定义端口: {port}")
366
+
367
+ kwargs: Dict[str, Any] = {"transport": transport}
368
+ # stdio 模式不接受 port 参数;仅 HTTP/SSE 模式才传端口
369
+ if port is not None and transport != "stdio":
370
+ kwargs["port"] = port
371
+
372
+ try:
373
+ mcp.run(**kwargs)
374
+ except KeyboardInterrupt:
375
+ if transport != "stdio" or debug:
376
+ print("服务被用户中断")
377
+ except Exception as e:
378
+ if transport != "stdio" or debug:
379
+ print(f"服务启动失败: {str(e)}")
380
+ raise
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.4
2
+ Name: wechat-oa-mcp-direct
3
+ Version: 0.2.0
4
+ Summary: 微信公众号 MCP 服务器(直连微信官方 API 版):草稿创建/发布/删除、素材删除、access_token 获取
5
+ Author-email: Jupiter <jupiter3019@163.com>
6
+ License: MIT
7
+ Keywords: wechat,mcp,fastmcp,公众号
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: fastmcp>=4.0
11
+ Requires-Dist: requests>=2.25
12
+
13
+ # wechat-oa-mcp-direct
14
+
15
+ 微信公众号 MCP 服务器 —— **直连微信官方 API 版**。
16
+
17
+ 基于 FastMCP 4.x 实现,提供公众号草稿创建 / 发布 / 删除、永久素材删除、access_token 获取共 5 个 MCP 工具。所有请求直连 `api.weixin.qq.com`,**不经过任何第三方中转服务器**,AppID / AppSecret 只在部署机与微信之间传输。
18
+
19
+ > 与 PyPI 上的 `wechat-oa-mcp 0.1.0` 的区别:旧版会把你的 AppSecret 转发到作者的服务器(106.15.125.133)中转,且依赖已废弃的 MCP SDK v1 API,无法在新环境启动。本包修复了上述问题(含 41001 access_token missing 修复)。
20
+
21
+ ---
22
+
23
+ > ## ⚠️ 部署前必读:IP 白名单(最容易踩的坑)
24
+ >
25
+ > 部署好后首次调用工具,如果返回 **`40164 invalid ip ... not in whitelist`** —— **不是代码问题**,是这台部署机的公网出口 IP 还没有加进公众号的 IP 白名单。
26
+ >
27
+ > - **每台部署机器都要把「它自己的出口 IP」加进白名单**(不同机器的 IP 不一样);
28
+ > - **换网络、换服务器、IP 变化后都要重新加**,否则之前能用的机器会突然报 40164;
29
+ > - 获取出口 IP 和配置方法见下文「2.2 配置 IP 白名单」。
30
+
31
+ ---
32
+
33
+ ## 1. 安装
34
+
35
+ ### Windows CMD 一键安装(最简便,无需虚拟环境)
36
+
37
+ 解压 zip 后,在 CMD 中进入解压出来的目录,一条命令安装:
38
+
39
+ ```cmd
40
+ cd wechat-oa-mcp-direct
41
+ pip install .
42
+ ```
43
+
44
+ 安装完成后验证:
45
+
46
+ ```cmd
47
+ wechat-oa-mcp --help
48
+ ```
49
+
50
+ ### macOS / Linux,或需要环境隔离时
51
+
52
+ ```bash
53
+ cd wechat-oa-mcp-direct
54
+ python3 -m venv .venv
55
+ source .venv/bin/activate # Windows 虚拟环境: .venv\Scripts\activate
56
+ pip install .
57
+ ```
58
+
59
+ 要求:Python >= 3.10(Windows 安装 Python 时记得勾选 **"Add Python to PATH"**,否则 CMD 里 `pip` 会提示找不到命令)。
60
+
61
+ ## 2. 前置配置(部署机必须完成)
62
+
63
+ ### 2.1 获取公众号凭证(AppID / AppSecret 在哪里)
64
+
65
+ 1. 用管理员微信扫码登录微信公众平台:https://mp.weixin.qq.com(需要先注册一个公众号,订阅号/服务号均可);
66
+ 2. 左侧菜单点 **「设置与开发」→「基本配置」**;
67
+ 3. 页面上的 **开发者ID(AppID)** 就是 AppID;
68
+ 4. **开发者密码(AppSecret)** 点击「重置」后生成——**只会完整显示这一次**,请立刻复制保存;之后忘记只能再点「重置」(需管理员扫码确认),且重置后旧 AppSecret 立即失效。
69
+
70
+ ### 2.2 配置 IP 白名单(关键,编辑位置在这里)
71
+
72
+ 1. 登录微信公众平台:https://mp.weixin.qq.com;
73
+ 2. 左侧菜单点 **「设置与开发」→「开发接口管理」**;
74
+ 3. 找到 **「IP白名单」** 栏目,点右侧的 **「修改」**;
75
+ 4. 输入 **部署本服务器那台机器的公网出口 IP**(可填多个,用逗号分隔),点 **「确认修改」** 保存。
76
+
77
+ > 如何获取部署机的出口 IP:在部署机的 CMD / 终端执行
78
+ > ```cmd
79
+ > curl "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=你的AppID&secret=你的AppSecret"
80
+ > ```
81
+ > 若返回 `40164 invalid ip <你的IP> not in whitelist`,尖括号里的 IP 就是要填进白名单的那个;填好后再执行一次,直到返回 `access_token` 即配置成功。
82
+ >
83
+ > ⚠️ 出口 IP 可能随网络环境变化(如家庭宽带重拨、换服务器),**换网络后必须重新加白名单**。
84
+
85
+ ## 3. 启动服务器
86
+
87
+ ```bash
88
+ # 默认 SSE 协议,端口 8000(交互式终端)
89
+ wechat-oa-mcp
90
+ # 或
91
+ python -m wechat_oa_mcp
92
+
93
+ # 指定端口 / 协议 / 调试模式
94
+ wechat-oa-mcp --port 8123
95
+ wechat-oa-mcp --transport stdio # stdio 模式(客户端自行拉起时自动生效)
96
+ wechat-oa-mcp --debug
97
+ ```
98
+
99
+ > ⚠️ **后台 / 服务化部署(nohup、systemd、launchd 等)必须显式加 `--transport sse`**:
100
+ > ```bash
101
+ > nohup wechat-oa-mcp --transport sse --port 8000 > mcp.log 2>&1 &
102
+ > ```
103
+ > 因为非终端环境下程序无法区分「后台运行」与「被 MCP 客户端以管道拉起」,未显式指定协议时默认走 stdio(供 npx inspector / 客户端拉起使用),此时不会监听端口。
104
+
105
+ 启动后 SSE 端点:`http://localhost:8000/sse`
106
+
107
+ ## 4. 接入 MCP 客户端
108
+
109
+ ### SSE 方式(需服务器保持运行)
110
+
111
+ ```json
112
+ {
113
+ "mcpServers": {
114
+ "wechat_oa_mcp": {
115
+ "type": "sse",
116
+ "url": "http://localhost:8000/sse"
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ ### stdio 方式(客户端自动拉起,推荐)
123
+
124
+ ```json
125
+ {
126
+ "mcpServers": {
127
+ "wechat_oa_mcp": {
128
+ "type": "stdio",
129
+ "command": "wechat-oa-mcp",
130
+ "args": ["--transport", "stdio"]
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ > 若 `command` 用虚拟环境安装,请填 venv 内的绝对路径,如 `/path/to/.venv/bin/wechat-oa-mcp`。
137
+
138
+ ## 5. 可用工具
139
+
140
+ | 工具 | 必填参数 | 说明 |
141
+ |---|---|---|
142
+ | `WeChat_get_access_token` | AppID, AppSecret | 获取 access_token(有效期 7200 秒) |
143
+ | `WeChat_create_draft` | access_token, image_url, title, content | 下载封面图→上传永久素材→创建图文草稿;可选 author / digest / content_source_url / need_open_comment |
144
+ | `WeChat_publish_draft` | access_token, draft_media_id | 发布草稿(异步任务,返回 publish_id) |
145
+ | `WeChat_del_draft` | access_token, media_id | 删除草稿 |
146
+ | `WeChat_del_material` | access_token, media_id | 删除永久素材 |
147
+
148
+ **调用顺序**:先 `WeChat_get_access_token` 拿 token,再调其余工具(token 2 小时内有效)。
149
+
150
+ **注意**:`WeChat_create_draft` 会真实上传图片并在公众号后台创建草稿,属真实写操作,建议先在测试号或测试内容上验证。
151
+
152
+ ## 6. 常见错误
153
+
154
+ | 错误码 | 含义 | 处理 |
155
+ |---|---|---|
156
+ | 40164 | IP 不在白名单 | 把部署机出口 IP 加入公众号 IP 白名单 |
157
+ | 41001 | access_token missing / invalid | 确认已先调用 get_access_token 且 token 未过期 |
158
+ | 40007 | invalid media_id | media_id 不存在或已删除 |
159
+ | 45009 | 接口调用超限 | 微信接口有频率限制,稍后重试 |
160
+
161
+ ## 7. 验证安装
162
+
163
+ ```bash
164
+ # 检查命令可用
165
+ wechat-oa-mcp --help
166
+
167
+ # 无副作用验证(不创建任何资源):
168
+ # 用 MCP Inspector 或客户端调用 WeChat_get_access_token,成功即说明配置就绪
169
+ npx @modelcontextprotocol/inspector python -m wechat_oa_mcp
170
+ ```
171
+
172
+ ## 免责声明
173
+
174
+ 本工具仅限研究用途,禁止用于商业目的。使用者需自行遵守微信公众平台服务条款。
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ wechat_oa_mcp/__init__.py
4
+ wechat_oa_mcp/__main__.py
5
+ wechat_oa_mcp/cli.py
6
+ wechat_oa_mcp/server.py
7
+ wechat_oa_mcp_direct.egg-info/PKG-INFO
8
+ wechat_oa_mcp_direct.egg-info/SOURCES.txt
9
+ wechat_oa_mcp_direct.egg-info/dependency_links.txt
10
+ wechat_oa_mcp_direct.egg-info/entry_points.txt
11
+ wechat_oa_mcp_direct.egg-info/requires.txt
12
+ wechat_oa_mcp_direct.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ wechat-oa-mcp = wechat_oa_mcp.cli:main
3
+ wechat-oa-mcp-direct = wechat_oa_mcp.cli:main
@@ -0,0 +1,2 @@
1
+ fastmcp>=4.0
2
+ requests>=2.25