Metadata-Version: 2.5
Name: cloud-asset-mcp
Version: 2.1.0
Summary: 精简版只读 MCP Server：查询资产的位置、使用方、状态和配件
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27.0
Requires-Dist: mcp>=2.0.0
Description-Content-Type: text/markdown

# cloud-asset-mcp

把数据中心固定资产平台 **cloud-asset** 的查询能力，以 [MCP](https://modelcontextprotocol.io) 工具的形式提供给 AI 客户端。

只读 · 单工具 · stdio / Streamable HTTP 双传输 · 无状态

![Python](https://img.shields.io/badge/python-3.11%2B-3776AB?logo=python&logoColor=white)
![MCP](https://img.shields.io/badge/MCP-compatible-6B4FBB)
![License](https://img.shields.io/badge/license-internal-lightgrey)

---

## 概述

AI 客户端接入本服务后，用户可以在对话里直接问「ABC123 这台机器在哪、谁在用、什么配置」，
模型调用唯一工具 `get_asset(keyword)`，服务端完成「检索 → 定位 → 取详情 → 摘字段」，
只返回 LLM 真正需要的四类信息：**当前位置、使用方、状态、配件配置**。

服务端是**纯只读**的，不提供任何写操作，也不发起工单。

## 功能

| 能力 | 说明 |
|---|---|
| 多类型检索 | 同时覆盖服务器、网络设备、配件三类资产 |
| 多字段匹配 | 支持 SN、资产序列号（serialNo）、资产编码（assetCode）三种关键词；15 位以上纯数字自动按 assetId 直查 |
| 精简返回 | 从几十个字段的详情中只摘四类信息，规避凭据字段外泄，同时显著降低 token 消耗 |
| 结构化错误 | 业务失败返回 `{ok: false, error: {code, message, hint}}`，模型可据 `hint` 自行修正参数，不会中断对话 |
| 状态歧义提示 | 命中「基础态/工单虚拟态」歧义状态码时明确标注不确定，不臆造结论 |
| 双传输 | 默认 stdio（客户端拉起子进程）；`--http` 切换 Streamable HTTP（多客户端 / 远程托管） |
| 多用户托管 | HTTP 模式下按请求读取 `X-Asset-Token` / `Authorization`，各用户使用自己的凭据 |
| 凭据保护 | 日志强制走 stderr，并自动对 Bearer / JWT 打码；HTTP 客户端禁用系统代理与重定向 |

## 环境要求

- Python ≥ 3.11
- 可访问 cloud-asset 后端的网络

## 快速开始

### 1. 安装

```bash
# 从 PyPI 安装（发布后）
uvx cloud-asset-mcp --help

# 或从源码安装
git clone <仓库地址> cloud-asset-mcp
cd cloud-asset-mcp
uv sync
```

### 2. 配置

两个环境变量。本地调试可复制样例文件：

```bash
cp .env.example .env
```

```dotenv
ASSET_BASE_URL=https://asset.yourcompany.com
ASSET_TOKEN=<浏览器 Cookie 中的 Admin-Token 裸 JWT>
```

`ASSET_TOKEN` 获取方式：登录资产管理平台 → 开发者工具 → Application / Cookies → 复制 `Admin-Token` 的值。
Token 有 **24 小时绝对有效期**，且 **30 分钟无操作即失效**，过期需重新复制。

优先级：客户端传入的环境变量 > `.env` > 代码默认值。

### 3. 接入客户端

stdio 方式（Claude Desktop / Claude Code / Codex / Cursor / WorkBuddy 通用）：

```json
{
  "mcpServers": {
    "cloud-asset": {
      "type": "stdio",
      "command": "uvx",
      "args": ["cloud-asset-mcp"],
      "env": {
        "ASSET_BASE_URL": "https://asset.yourcompany.com",
        "ASSET_TOKEN": "你的JWT"
      }
    }
  }
}
```

未发布到 PyPI 时，改用虚拟环境解释器的绝对路径：

```json
{
  "mcpServers": {
    "cloud-asset": {
      "command": "/绝对路径/cloud-asset-mcp/.venv/bin/python",
      "args": ["/绝对路径/cloud-asset-mcp/server.py"],
      "env": { "ASSET_BASE_URL": "https://asset.yourcompany.com", "ASSET_TOKEN": "你的JWT" }
    }
  }
}
```

配置完成后重启客户端，在对话中提问即可触发工具调用。各客户端配置文件位置与详细步骤见
[部署指南](docs/部署指南.md)。

## 配置项

| 变量 | 必填 | 默认值 | 说明 |
|---|---|---|---|
| `ASSET_BASE_URL` | 是 | `http://127.0.0.1:9039` | cloud-asset 后端根地址，末尾斜杠自动去除 |
| `ASSET_TOKEN` | stdio 模式必填 | — | 裸 JWT。可带 `Bearer ` 前缀，会自动剥离；HTTP 多用户模式下由请求头提供，可不配 |

命令行参数：

| 参数 | 默认值 | 说明 |
|---|---|---|
| `--http` | 关闭 | 使用 Streamable HTTP 传输，默认 stdio |
| `--host` | `127.0.0.1` | HTTP 监听地址 |
| `--port` | `8765` | HTTP 监听端口 |

## 工具参考

### `get_asset(keyword)`

按关键词查询单台资产，返回位置、使用方、状态与配件配置。

**参数**

| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `keyword` | string | 是 | 设备 SN、资产序列号（serialNo）、资产编码（assetCode），或 15 位以上纯数字 assetId |

**返回（成功）**

```json
{
  "ok": true,
  "asset": {
    "assetId": "900000000000000001",
    "assetType": "server",
    "assetTypeText": "服务器",
    "sn": "ABC123",
    "serialNo": "FW2025010100001",
    "assetCode": "ZC-2024-0001",
    "brandName": "华为",
    "status": { "code": 7, "text": "正常已交付", "note": null },
    "deviceUse": "训练平台",
    "location": {
      "position": "A园区-1号楼-301机房-A01-U12",
      "parkName": "A园区",
      "buildingName": "1号楼",
      "machineRoomName": "301机房",
      "cabinetCode": "A01",
      "cabinetColumnCode": "A",
      "startUnit": 12,
      "height": 2
    },
    "parts": {
      "available": true,
      "rowCount": 2,
      "configured": 12,
      "pending": 2,
      "categories": {
        "gpu": { "label": "GPU", "count": 2, "models": ["A100 80G"], "pending": 0 },
        "power": { "label": "电源", "count": 1, "models": ["1600W"], "pending": 1 }
      }
    }
  }
}
```

字段说明：

- `location.position` 是后端拼好的完整位置串，其余字段按后端返回的粒度可选出现。
- `parts.rowCount` 是配件横表行数，**不等于配件总数**；`configured` 才是已配置数量，`pending` 为「待完善」槽位数。
- 无配件明细时，`parts` 为 `{"available": false, "note": "..."}`。

**返回（失败）**

```json
{
  "ok": false,
  "error": {
    "code": "ASSET_NOT_FOUND",
    "message": "没有找到包含 'ABC999' 的资产。",
    "hint": "已按 serialNo / sn / assetCode 在服务器、网络设备、配件三类里检索过。请确认关键词，或改用资产编码/序列号。"
  }
}
```

**错误码**

| code | 含义 | 处理建议 |
|---|---|---|
| `INVALID_ARGUMENT` | `keyword` 为空 | 补全参数 |
| `ASSET_NOT_FOUND` | 三类资产、三个字段均未命中 | 核对 SN / 资产编码 |
| `TOKEN_MISSING` | 未配置 token，请求头也没带 | 补 `ASSET_TOKEN` 或加请求头 |
| `TOKEN_INVALID` | 非三段式 JWT 或 payload 无法解析 | 重新复制完整 `Admin-Token` |
| `TOKEN_EXPIRED` | 本地解析出 `exp` 已过期 | 重新登录复制新 token |
| `UNAUTHORIZED` | 后端返回 401 | 同上，会话已失效 |
| `FORBIDDEN` | 后端返回 403 | 当前账号无该数据权限 |
| `UPSTREAM_UNAVAILABLE` | 连不上后端 | 检查 `ASSET_BASE_URL` 与网络 |
| `UPSTREAM_TIMEOUT` | 后端 15s 超时 | 稍后重试 |
| `UPSTREAM_ERROR` | 后端业务错误码非 200/401/403 | 看 `message` 中的后端原文 |
| `BAD_RESPONSE` | 响应非 JSON 或缺 `code` 字段 | 确认地址指向资产管理后端 |
| `INTERNAL_ERROR` | 未捕获异常（已记录堆栈） | 反馈维护者 |

> 后端的 HTTP 状态码恒为 200，成败判定依据响应体 `code`。服务端已统一处理该约定。

## 状态码语义（重要）

`status` 字段存在二义性：`3 / 4 / 5 / 8 / 14 ~ 19` 这些码既可能是基础库存态，也可能是被在途工单改写的「虚拟态」。

服务端策略：命中歧义码时照常返回 `text`，同时在 `status.note` 中明确说明不确定原因，建议用户到 Web 端核实。**不查询工单，也不给出猜测结论。**

## 部署

### 本地 / 单用户（stdio）

客户端负责拉起进程，无需常驻服务。日志输出到 stderr，不污染协议通道。

### 远程托管（Streamable HTTP）

```bash
ASSET_BASE_URL=https://asset.yourcompany.com uvx cloud-asset-mcp --http --host 0.0.0.0 --port 8765
```

服务端路径固定为 `/mcp`。客户端配置：

```json
{
  "mcpServers": {
    "cloud-asset": {
      "type": "http",
      "url": "https://mcp.yourcompany.com/mcp",
      "headers": { "X-Asset-Token": "你的JWT" }
    }
  }
}
```

多用户托管时**不要**在服务端配置 `ASSET_TOKEN`，每个用户通过 `X-Asset-Token` 或 `Authorization: Bearer <JWT>` 传递自己的凭据；服务端使用 `ContextVar` 做请求级隔离。

生产部署（systemd + Caddy HTTPS、Docker）所需的 `Dockerfile`、`Caddyfile`、`cloud-asset-mcp.service` 均在 [`deploy/`](deploy) 目录，
完整步骤见 [发布与用户接入指南](docs/发布与用户接入指南.md)。

## 安全设计

1. **只读**：不提供任何写接口，无工单能力。
2. **最小返回**：从详情中白名单摘字段，`bmcPassword` 一类凭据不会外泄，同时节省 token。
3. **日志脱敏**：`RedactingFormatter` 对 `Bearer xxx` 与 `eyJ...` 形式的 JWT 统一打码，日志可安全外发。
4. **stdout 纯净**：日志 handler 固定在 stderr，避免破坏 stdio 协议帧。
5. **不信任网络环境**：`httpx` 设置 `trust_env=False` 与 `follow_redirects=False`，防止 token 经代理或重定向外泄。
6. **凭据不落库**：服务端不存储、不缓存 token；HTTP 模式下 token 仅存活于单次请求上下文。

## 项目结构

```
cloud-asset-mcp/
├── server.py                    # 全部实现：配置 → 认证 → HTTP → 业务 → MCP → 启动
├── fake_backend.py              # 零依赖假后端，无需真实环境即可联调
├── demo.py                      # 直接调用业务逻辑，便于调试
├── verify_mcp.py                # 走真实 MCP 协议验证客户端视角
├── deploy/                      # Dockerfile / Caddyfile / systemd unit
├── docs/                        # 项目详解、部署指南、发布与用户接入指南
├── .env.example                 # 配置样例
└── pyproject.toml               # 打包与命令行入口
```

`server.py` 按 6 节组织，节首有分隔注释：常量 / 配置 / 认证 / HTTP / 业务 / MCP / 启动。

## 文档

| 文档 | 适用读者 | 内容 |
|---|---|---|
| [项目详解](docs/项目详解.md) | 想读懂实现的人 | 从 Python、MCP 基础讲到 `server.py` 逐节精讲，附一次调用的完整时序 |
| [部署指南](docs/部署指南.md) | 要部署的人 | stdio / Streamable HTTP 两种形态、各客户端配置、排错手册 |
| [发布与用户接入指南](docs/发布与用户接入指南.md) | 维护者 | PyPI 发版流程、TestPyPI 试发、systemd + Caddy、Docker 部署、接入验证 |

## 本地开发

无需真实后端和真实 token：

```bash
cp .env.example .env                  # 已指向假后端与演示 JWT

./.venv/bin/python fake_backend.py    # 终端 1：假后端，会打印收到的请求
./.venv/bin/python demo.py ABC123     # 终端 2：直接验证业务逻辑
./.venv/bin/python verify_mcp.py      # 终端 3：走 MCP 协议验证客户端视角
```

假后端内置样例数据：`ABC123`（服务器，缺失配件槽位）、`ABC999`（服务器，命中歧义状态码）、`NET-001`（网络设备，无配件）。

切换真实后端：修改 `.env` 中 `ASSET_BASE_URL` 与 `ASSET_TOKEN` 即可，代码无需改动。

## 发布新版本

```bash
# 1. 更新 pyproject.toml 的 version（语义化版本，不可重复上传同版本号）
# 2. 重新构建
rm -rf dist && uv build
# 3. 上传
uvx twine upload dist/*
```

建议先发 TestPyPI 验证，再发正式索引。完整流程与常见坑见
[发布与用户接入指南](docs/发布与用户接入指南.md)。

## 排错

| 现象 | 原因 | 处理 |
|---|---|---|
| 客户端显示 server 已启动但无工具 | stdout 被 `print` 污染 | 确认日志走 stderr，移除调试输出 |
| 返回 `TOKEN_EXPIRED` | token 超 24h 或 30 分钟未操作 | 重新复制 `Admin-Token` |
| 返回 `UPSTREAM_UNAVAILABLE` | 后端地址错误或未启动 | 校验 `ASSET_BASE_URL`；离线时先跑 `fake_backend.py` |
| 返回 `BAD_RESPONSE` | 地址指向了代理或静态站点 | 确认是 cloud-asset 后端根地址 |
| `uvx cloud-asset-mcp` 找不到包 | 尚未发布到 PyPI | 改用虚拟环境绝对路径配置，或先发版 |
| 查询网络设备报 `UPSTREAM_ERROR` | 后端对不存在的 ID 返回 500 | 换用正确的 SN / 资产编码检索 |

## License

内部项目，未声明开源许可。如需对外开源，请先补充 `LICENSE` 文件并确认代码中不含内部环境信息。
