# MCP Server for 企微 AI Bot — Reference Implementation

This is a working MCP server that exposes article query tools for the 企微 AI bot.
Deployed at: `/home/agentuser/project/scripts/mcp_server.py`

## Architecture

```
企微 AI 机器人 (DeepSeek)
    ↓ MCP Streamable HTTP (https://mcp.icoach.chat/rpc)
Cloudflare Tunnel
    ↓ localhost:8100
mcp_server.py  ← Mcp-Session-Id header required!
    ↓ SQLite
we-mp-rss database (db.db)
```

**⚠️ 必须用 Streamable HTTP (`/rpc`)，SSE (`/sse`) 在企微上 session 频繁丢失导致插件被移除。**

## Tools Exposed

| Tool | Description | Input |
|------|-------------|-------|
| `get_today_articles` | 今日文章列表 | 无 |
| `get_article_detail` | 文章详情（纯文本前 4000 字） | `article_id` |

### Content Stripping (Critical)

we-mp-rss stores the full HTML page (~3MB per article) in its `articles.content` column.
Taking the first N characters directly returns only `<head>` boilerplate — the AI sees zero article text.

The server uses a `_strip_html()` helper that:
1. Removes `<script>` and `<style>` blocks (re.DOTALL)
2. Removes HTML comments `<!-- ... -->`
3. Strips all HTML tags `<...>`
4. Decodes HTML entities (`&amp;` → `&`, `&#xNNNN` → char)
5. Collapses whitespace

```python
def _strip_html(raw: str) -> str:
    if not raw:
        return ""
    text = re.sub(r'<(script|style)[^>]*>.*?</\1>', '', raw, flags=re.DOTALL | re.IGNORECASE)
    text = re.sub(r'<!--.*?-->', '', text, flags=re.DOTALL)
    text = re.sub(r'<[^>]+>', '', text)
    text = html.unescape(text)
    text = re.sub(r'\s+', ' ', text).strip()
    return text
```

### Restart After Code Changes

```bash
pkill -f mcp_server.py && sleep 1
cd /home/agentuser/project/scripts
~/project/we-mp-rss-main源文件/we-mp-rss-main/venv/bin/python3 mcp_server.py &
# Verify
sleep 2 && curl -s http://localhost:8100/health
```

### Endpoints

| Endpoint | Protocol | 企微插件填写 | 状态 |
|----------|----------|-------------|------|
| `/rpc` | Streamable HTTP | `https://mcp.icoach.chat/rpc` | ✅ 推荐，稳定 |
| `/sse` | SSE | ~~`https://mcp.icoach.chat/sse`~~ | ❌ 企微上不稳定，勿用 |
| `/health` | Plain HTTP | 健康检查 | — |

**企微适用传输协议必须选「Streamable HTTP」，URL 尾缀 `/rpc`。**

## Deployment

```bash
cd /home/agentuser/project/scripts
source ../we-mp-rss-main源文件/we-mp-rss-main/venv/bin/activate
pip install mcp
python mcp_server.py  # runs on port 8100
```

## Dependencies

- `mcp` package (Python MCP SDK)
- `starlette` + `uvicorn` (SSE transport)
- SQLite access to `/home/agentuser/project/we-mp-rss-data/db.db`

## Nginx Config (for Cloudflare Tunnel fallback)

```nginx
location /mcp/sse {
    proxy_pass http://127.0.0.1:8100/sse;
    proxy_http_version 1.1;
    proxy_set_header Connection '';
    proxy_buffering off;
    proxy_cache off;
    chunked_transfer_encoding off;
}
location /mcp/messages/ {
    proxy_pass http://127.0.0.1:8100/messages/;
}
```

## Critical: Mcp-Session-Id Header

**企微使用多台服务器轮询 MCP 请求**（日志可见每次请求来自不同 IP：14.215.x.x, 106.52.x.x, 183.47.x.x），`initialize` 和 `tools/call` 可能被路由到不同节点。没有 `Mcp-Session-Id` 时，`tools/call` 会被企微内部拒绝（「工具名称无法正确匹配」），请求根本不会到达服务端。

**必须在 `initialize` 响应中返回 `Mcp-Session-Id` 头**，并在后续请求中保持不变：

```python
# Streamable HTTP 端点 — handle_rpc()
if method == "initialize":
    session_id = uuid.uuid4().hex
    return JSONResponse(
        _jsonrpc_result(req_id, result),
        headers={"Mcp-Session-Id": session_id}  # ← 关键！
    )

# 后续响应中回传同一个 session_id
def _jsonrpc_response(result_data):
    headers = {"Mcp-Session-Id": session_id} if session_id else {}
    return JSONResponse(result_data, headers=headers)
```

完整实现参考：`/home/agentuser/project/scripts/mcp_server.py`（handle_rpc 函数）。

## Bot System Prompt 格式约束

企微 AI 机器人的「机器人指令」字段**不支持复杂格式**。以下会导致「AI 处理失败」：

| 禁止 | 原因 |
|------|------|
| 代码块（\`\`\`） | 企微解析器崩溃 |
| Markdown 表格（\|...\|） | 同上 |
| Emoji（🔥📊⚡ 等） | 部分不可用 |
| 过长内容（>2000字） | 截断/崩溃 |

**正确格式**：纯文本 + 段落标题用 **粗体**，要点用短横线列表。见 `references/bot-prompt-template.md`。

## Troubleshooting

- **插件频繁消失**：根本原因是 SSE session 丢失 → 改用 `/rpc` (Streamable HTTP)。其次是 `Mcp-Session-Id` 未返回 → 见上方 Critical 节。
- **「工具名称无法正确匹配」**：检查：(1) 插件 URL 是否为 `/rpc`，(2) `initialize` 是否返回 `Mcp-Session-Id` 头，(3) Cloudflare Tunnel 是否正常。
- **AI returns "无法回答" / empty content**: The most common cause is that `_strip_html()` is missing or broken — we-mp-rss content is raw HTML, so without stripping the AI sees `<head>` tags, not text. Verify with `curl` to the `/rpc` endpoint and check the `content` field has readable text, not HTML.
- **「AI 处理失败，请稍后重试」**：90% 是机器人指令格式问题（含代码块/表格），清空指令到最简版再逐步添加排查。
- **Connection timeout from external**: Check Tencent Cloud firewall or Cloudflare Tunnel config
- **pip dependency conflict**: `fastapi` vs `starlette` version mismatch → safe to ignore
