# doc-service 接口说明
> 面向 AI Agent 的文档托管服务调用指南。Agent 可将生成的报告、分析结果、图片发布为公开可访问的链接,再将链接返回给用户。
---
## 1. 服务概述
`doc-service` 是一个轻量级文档与文件托管服务,支持上传 **HTML**、**Markdown** 文档以及**图片**文件,系统返回一个 8 位短链接(key),通过该链接可直接在浏览器访问。
**支持的内容类型:**
| 类型 | 说明 |
|------|------|
| HTML 文档 | 原样存储,浏览器直接渲染 |
| Markdown 文档 | 服务端自动渲染为带样式 HTML |
| 图片 | JPG、PNG、GIF、WEBP、SVG,inline 展示,可嵌入其他页面 |
> ⚠️ 不支持音频、视频、PDF 等其他文件类型。
**典型工作流(Agent 调用):**
```
Agent 生成内容(HTML / Markdown / 图片文件)
↓
POST 对应上传接口(携带 token)
↓
获取返回值中的 viewUrl
↓
将 viewUrl 告知用户,用户直接在浏览器打开 / 嵌入页面
```
---
## 2. 服务地址
**公网地址:** `https://www.doc-service.cn`
所有接口均以此地址为前缀,例如:
```
https://www.doc-service.cn/doc-service/api/md-page/uploadContent
https://www.doc-service.cn/doc-service/api/file/upload
https://www.doc-service.cn/doc-service/view/file/{fileKey}
```
> 上传接口返回的 `viewUrl` 字段已包含完整可访问链接,无需手动拼接。
---
## 3. 鉴权说明
| 操作 | 是否需要鉴权 | Header |
|------|------------|--------|
| 上传(upload) | **需要** | `Authorization: Bearer {token}` |
| 删除(delete) | **需要** | `Authorization: Bearer {token}` |
| 访问(view) | **不需要** | 无,任何人可直接访问 |
**获取 token:** 发邮件至 zhuyngle@163.com 申请,注明用途,管理员将生成并提供专属 token。
---
## 4. 接口清单
### HTML 文档
| 接口 | 方法 | 路径 | 鉴权 |
|------|------|------|------|
| 上传 HTML 字符串 | POST | `/doc-service/api/html-page/uploadContent` | Bearer token |
| 上传 HTML 文件 | POST | `/doc-service/api/html-page/uploadFile` | Bearer token |
| 访问 HTML 页面 | GET | `/doc-service/view/html-page/{pageKey}` | 无需 |
| 删除 HTML 页面 | POST | `/doc-service/api/html-page/delete` | Bearer token |
### Markdown 文档
| 接口 | 方法 | 路径 | 鉴权 |
|------|------|------|------|
| 上传 MD 字符串 | POST | `/doc-service/api/md-page/uploadContent` | Bearer token |
| 上传 MD 文件 | POST | `/doc-service/api/md-page/uploadFile` | Bearer token |
| 访问 MD 渲染页 | GET | `/doc-service/view/md-page/{pageKey}` | 无需 |
| 访问 MD 源码 | GET | `/doc-service/view/md-source/{pageKey}` | 无需 |
| 删除 MD 页面 | POST | `/doc-service/api/md-page/delete` | Bearer token |
### 图片文件
| 接口 | 方法 | 路径 | 鉴权 |
|------|------|------|------|
| 上传图片 | POST | `/doc-service/api/file/upload` | Bearer token |
| 访问图片 | GET | `/doc-service/view/file/{fileKey}` | 无需 |
| 删除图片 | POST | `/doc-service/api/file/delete` | Bearer token |
---
## 5. 接口参数详情
### 5.1 HTML / Markdown — uploadContent(上传内容字符串)
**Query 参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `title` | String | 否 | 文档标题,默认 `未命名文档_时间戳` |
| `expireHours` | Integer | 否 | 有效期小时数,默认 `8760`(365 天) |
**请求体:** 文档内容字符串,`Content-Type: text/plain`
**返回值:**
```json
{
"code": 200,
"data": {
"success": true,
"pageKey": "x5HVJdk7",
"viewUrl": "https://www.doc-service.cn/doc-service/view/html-page/x5HVJdk7",
"title": "我的报告",
"fileSize": 1024,
"pageType": "html"
}
}
```
---
### 5.2 HTML / Markdown — uploadFile(上传文件)
**Multipart 参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `file` | MultipartFile | **是** | 文件内容 |
| `title` | String | 否 | 默认使用原始文件名 |
| `expireHours` | Integer | 否 | 默认 8760 |
---
### 5.3 HTML / Markdown — delete(软删除)
**请求体(JSON):**
```json
{"pageKey": "x5HVJdk7"}
```
`Content-Type: application/json`
---
### 5.4 图片上传 — /api/file/upload
**路径:** `POST /doc-service/api/file/upload`
**Multipart 参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `file` | MultipartFile | **是** | 图片文件,最大 16MB |
| `expireHours` | Integer | 否 | 有效期小时数,默认 8760(365 天) |
**支持格式:**
| 格式 | MIME | 浏览器行为 |
|------|------|-----------|
| JPG / JPEG | image/jpeg | inline 渲染,可用 `
` 嵌入 |
| PNG | image/png | inline 渲染,可用 `
` 嵌入 |
| GIF | image/gif | inline 渲染 |
| WEBP | image/webp | inline 渲染 |
| SVG | image/svg+xml | inline 渲染 |
**返回值:**
```json
{
"code": 200,
"data": {
"success": true,
"fileKey": "kABgwDvz",
"viewUrl": "https://www.doc-service.cn/doc-service/view/file/kABgwDvz",
"embedUrl": "https://www.doc-service.cn/doc-service/view/file/kABgwDvz",
"fileName": "photo.png",
"mimeType": "image/png",
"fileCategory": "image",
"fileSize": 7876
}
}
```
**字段说明:**
- `viewUrl`:访问链接
- `embedUrl`:可直接用于 `
`
---
### 5.5 图片访问 — /view/file/{fileKey}(无需 token)
```
GET /doc-service/view/file/{fileKey}
```
- 浏览器直接渲染(inline)
- 不存在:`404`;已过期:`410 Gone`
---
### 5.6 图片删除 — /api/file/delete
**请求体(JSON):**
```json
{"pageKey": "kABgwDvz"}
```
---
## 6. 完整调用示例(curl)
### 上传 Markdown
```bash
curl -X POST "https://www.doc-service.cn/doc-service/api/md-page/uploadContent?title=Analysis" \
-H "Authorization: Bearer " \
-H "Content-Type: text/plain" \
--data-raw "# 分析报告
## 摘要
- 要点 1
- 要点 2
"
```
### 上传 HTML 文件
```bash
curl -X POST "https://www.doc-service.cn/doc-service/api/html-page/uploadFile" \
-H "Authorization: Bearer " \
-F "file=@/path/to/report.html" \
-F "title=My Report"
```
### 上传图片
```bash
curl -X POST "https://www.doc-service.cn/doc-service/api/file/upload" \
-H "Authorization: Bearer " \
-F "file=@/path/to/photo.png"
```
返回后取 `data.embedUrl`,嵌入其他页面:
```html
```
### 删除
```bash
curl -X POST "https://www.doc-service.cn/doc-service/api/file/delete" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{"pageKey": "kABgwDvz"}'
```
---
## 7. Agent 使用注意事项
1. **文档优先用 Markdown**:`/md-page/` 接口自动渲染为带样式的 HTML 页面,效果更好。
2. **图片可直接嵌入**:上传图片后用 `embedUrl` 放入 `
` 即可跨域展示。
3. **title 含中文须 URL 编码**:query 参数中的中文需 URL encode,HTTP 库通常自动处理。
4. **delete 用 JSON body**:删除接口 body 为 `{"pageKey": "..."}` + `Content-Type: application/json`。
5. **保存 key**:上传后记录 `data.pageKey`(文档)或 `data.fileKey`(图片),用于后续删除。
6. **视图接口完全公开**:`/view/` 路径无需任何 header,可直接分享或嵌入。
7. **有效期**:默认 1 年(8760 小时);到期后访问返回 `410 Gone`。
8. **图片大小限制**:单文件最大 16MB(数据库 MEDIUMBLOB 上限)。
---
## 8. 本地 doc-service MCP 搭建指南
通过 Python 在本地运行 MCP Server,Claude Code 可通过 MCP 协议直接调用上传接口,无需手动拼接 curl 命令。
### 8.1 依赖安装
```bash
pip install mcp
```
> 需要 Python 3.11+
### 8.2 server.py 完整代码
将以下代码保存为本地任意路径,例如 `~/doc-service-mcp/server.py`:
```python
"""
doc-service MCP Server
为 AI Agent 提供文档托管能力:上传 HTML/Markdown 文档并获取公开访问链接。
服务地址:https://www.doc-service.cn/doc-service
"""
import base64
import json
import re
import subprocess
import tempfile
import uuid
import urllib.request
from pathlib import Path
from mcp.server.fastmcp import FastMCP
# ─── 常量配置 ──────────────────────────────────────────────────────────────────
BASE_URL = "https://www.doc-service.cn/doc-service"
TOKEN_FILE = Path.home() / ".doc-service-token"
DEFAULT_USER_AGENT = "doc-service-mcp/1.0"
REQUEST_TIMEOUT_SECONDS = 30
CURL_META_SEP = "---CURL_META_SEP---"
# ─── Token 管理 ────────────────────────────────────────────────────────────────
def read_token() -> str | None:
"""从 ~/.doc-service-token 读取 token,文件不存在返回 None"""
if TOKEN_FILE.exists():
return TOKEN_FILE.read_text().strip() or None
return None
def write_token(token: str) -> None:
"""将 token 写入 ~/.doc-service-token"""
TOKEN_FILE.write_text(token.strip())
def _get_token() -> str:
"""获取 token,未配置则抛出异常提示用户"""
token = read_token()
if not token:
raise RuntimeError(
"未配置 token,请先调用 set_token 工具设置。"
"申请 token 请发邮件至 zhuyngle@163.com。"
)
return token
# ─── HTTP 请求层 ───────────────────────────────────────────────────────────────
def _exec_curl(args: list[str]) -> str:
"""
执行 curl 子进程。
MCP stdio 环境中使用 curl 比 httpx 更稳定,能正确处理 TLS/代理/重定向。
"""
try:
result = subprocess.run(
["curl"] + args,
capture_output=True,
timeout=REQUEST_TIMEOUT_SECONDS,
)
except subprocess.TimeoutExpired:
raise RuntimeError("curl 执行超时")
except FileNotFoundError:
raise RuntimeError("curl 未找到,请确保系统已安装 curl")
if result.returncode != 0:
raise RuntimeError(f"curl 失败: {result.stderr.decode(errors='replace')}")
return result.stdout.decode(errors="replace")
def _parse_curl_response(raw: str) -> str:
"""
解析 curl 响应体与状态码。
curl 使用 -w "\\n{SEP}%{http_code}\\n" 在末尾追加状态码。
"""
sep_pos = raw.rfind(CURL_META_SEP)
if sep_pos == -1:
raise RuntimeError(f"curl 响应格式异常: {raw[:200]}")
body = raw[:sep_pos].rstrip("\n")
status_str = raw[sep_pos + len(CURL_META_SEP):].strip()
try:
status_code = int(status_str)
except ValueError:
raise RuntimeError(f"无法解析状态码: {status_str!r}")
if status_code == 404:
raise RuntimeError("页面不存在(404)")
if status_code == 410:
raise RuntimeError("页面已过期(410 Gone)")
if not (200 <= status_code < 300):
raise RuntimeError(f"请求失败 HTTP {status_code}: {body[:300]}")
return body
def http_request(
method: str,
url: str,
headers: dict[str, str] | None = None,
body: bytes | None = None,
) -> str:
"""
通用 HTTP 请求封装,支持 GET / POST / DELETE。
请求体写入临时文件,通过 --data-binary @file 传给 curl,避免命令行长度限制。
"""
all_headers = {"User-Agent": DEFAULT_USER_AGENT}
if headers:
all_headers.update(headers)
args = [
"-s",
"-X", method.upper(),
"-w", f"\n{CURL_META_SEP}%{{http_code}}\n",
]
for k, v in all_headers.items():
args += ["-H", f"{k}: {v}"]
tmp_path = None
try:
if body is not None:
tmp_path = Path(tempfile.gettempdir()) / f"doc-mcp-{uuid.uuid4().hex}.bin"
tmp_path.write_bytes(body)
args += ["--data-binary", f"@{tmp_path}"]
args.append(url)
raw = _exec_curl(args)
return _parse_curl_response(raw)
finally:
if tmp_path and tmp_path.exists():
tmp_path.unlink()
def _build_multipart(
fields: dict[str, str],
file_field: str,
file_name: str,
file_content: bytes,
) -> tuple[str, bytes]:
"""
手动构建 multipart/form-data,减少外部依赖。
返回 (boundary, body_bytes)。
"""
boundary = f"----FormBoundary{uuid.uuid4().hex[:12]}"
parts = bytearray()
# 普通字段
for name, value in fields.items():
parts += (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="{name}"\r\n\r\n'
f"{value}\r\n"
).encode()
# 文件字段
parts += (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="{file_field}"; filename="{file_name}"\r\n'
f"Content-Type: application/octet-stream\r\n\r\n"
).encode()
parts += file_content
parts += f"\r\n--{boundary}--\r\n".encode()
return boundary, bytes(parts)
# ─── API 函数层 ────────────────────────────────────────────────────────────────
def upload_content(
page_type: str,
content: str,
title: str | None = None,
expire_hours: int | None = None,
) -> dict:
"""上传文档内容字符串(HTML 或 MD),返回上传结果 dict"""
token = _get_token()
params = []
if title:
from urllib.parse import quote
params.append(f"title={quote(title)}")
if expire_hours is not None:
params.append(f"expireHours={expire_hours}")
query = ("?" + "&".join(params)) if params else ""
url = f"{BASE_URL}/api/{page_type}-page/uploadContent{query}"
body = content.encode("utf-8")
raw = http_request(
"POST", url,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "text/plain",
},
body=body,
)
return json.loads(raw)
def upload_file(
page_type: str,
file_path: str,
title: str | None = None,
expire_hours: int | None = None,
) -> dict:
"""上传本地文件(HTML 或 MD),返回上传结果 dict"""
token = _get_token()
path = Path(file_path)
if not path.exists():
raise FileNotFoundError(f"文件不存在: {file_path}")
file_content = path.read_bytes()
file_name = path.name
fields: dict[str, str] = {}
if title:
fields["title"] = title
if expire_hours is not None:
fields["expireHours"] = str(expire_hours)
boundary, body = _build_multipart(fields, "file", file_name, file_content)
url = f"{BASE_URL}/api/{page_type}-page/uploadFile"
raw = http_request(
"POST", url,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": f"multipart/form-data; boundary={boundary}",
},
body=body,
)
return json.loads(raw)
def _delete_page(page_key: str, page_type: str) -> dict:
"""软删除文档页面,pageKey 通过 JSON body 传递"""
token = _get_token()
url = f"{BASE_URL}/api/{page_type}-page/delete"
body = json.dumps({"pageKey": page_key}).encode("utf-8")
raw = http_request(
"POST", url,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
body=body,
)
return json.loads(raw)
# ─── 资源文件上传(用于 HTML 预处理)──────────────────────────────────────────────
_MIME_MAP = {
"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png",
"gif": "image/gif", "webp": "image/webp", "svg": "image/svg+xml",
"mp3": "audio/mpeg", "wav": "audio/wav", "ogg": "audio/ogg", "m4a": "audio/mp4",
"mp4": "video/mp4", "webm": "video/webm",
}
_MIME_REV = {v: k for k, v in _MIME_MAP.items()}
_DATA_URI_RE = re.compile(r'data:[^"\')\s]+;base64,[A-Za-z0-9+/=]+')
_SRC_RE = re.compile(
r'''(?:src|href|srcset)\s*=\s*["']([^"']+)["']'''
r'''|url\(\s*["']?([^"')]+?)["']?\s*\)''',
re.IGNORECASE,
)
def _mime_from_data_uri(data_uri: str) -> str:
m = re.match(r"data:([^;]+);base64,", data_uri)
return m.group(1) if m else "application/octet-stream"
def _mime_from_url(url: str) -> str:
ext = url.split("?")[0].rsplit(".", 1)[-1].lower()
return _MIME_MAP.get(ext, "application/octet-stream")
def _ext_from_mime(mime: str) -> str:
return _MIME_REV.get(mime, "bin")
def _upload_binary_file(file_path: str, mime_type: str, token: str) -> str:
"""上传二进制文件到 /api/file/upload,返回 viewUrl"""
result = subprocess.run(
[
"curl", "-s", "-X", "POST",
f"{BASE_URL}/api/file/upload",
"-H", f"Authorization: Bearer {token}",
"-F", f"file=@{file_path};type={mime_type}",
],
capture_output=True, text=True, timeout=60,
)
if result.returncode != 0:
raise RuntimeError(f"curl 失败: {result.stderr}")
data = json.loads(result.stdout)
if data.get("code") != 200:
raise RuntimeError(f"上传失败: {data.get('message')}")
return data["data"]["viewUrl"]
def _upload_html_file_direct(file_path: str, title: str, token: str) -> str:
"""上传 HTML 文件到 /api/html-page/uploadFile,返回 viewUrl"""
from urllib.parse import quote
title_enc = quote(title)
result = subprocess.run(
[
"curl", "-s", "-X", "POST",
f"{BASE_URL}/api/html-page/uploadFile?title={title_enc}&expireHours=8760",
"-H", f"Authorization: Bearer {token}",
"-F", f"file=@{file_path};type=text/html",
],
capture_output=True, text=True, timeout=120,
)
if result.returncode != 0:
raise RuntimeError(f"curl 失败: {result.stderr}")
data = json.loads(result.stdout)
if data.get("code") != 200:
raise RuntimeError(f"上传 HTML 失败: {data.get('message')}")
return data["data"]["viewUrl"]
def _process_data_uri(data_uri: str, token: str, cache: dict) -> str:
if data_uri in cache:
return cache[data_uri]
mime = _mime_from_data_uri(data_uri)
if not any(mime.startswith(p) for p in ("image/", "audio/", "video/")):
return data_uri
b64_data = re.sub(r"^data:[^;]+;base64,", "", data_uri)
raw = base64.b64decode(b64_data)
ext = _ext_from_mime(mime)
tmp = Path(tempfile.gettempdir()) / f"mcp-res-{uuid.uuid4().hex[:8]}.{ext}"
tmp.write_bytes(raw)
try:
url = _upload_binary_file(str(tmp), mime, token)
cache[data_uri] = url
return url
except Exception:
return data_uri
finally:
tmp.unlink(missing_ok=True)
def _process_external_url(url: str, token: str, cache: dict) -> str:
if url in cache:
return cache[url]
if "doc-service.cn" in url:
return url
mime = _mime_from_url(url)
if not any(mime.startswith(p) for p in ("image/", "audio/", "video/")):
return url
ext = _ext_from_mime(mime)
try:
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=20) as resp:
raw = resp.read()
ct = resp.headers.get("Content-Type", mime).split(";")[0].strip()
if ct and ct != "application/octet-stream":
mime = ct
ext = _ext_from_mime(mime)
except Exception:
return url
tmp = Path(tempfile.gettempdir()) / f"mcp-res-{uuid.uuid4().hex[:8]}.{ext}"
tmp.write_bytes(raw)
try:
new_url = _upload_binary_file(str(tmp), mime, token)
cache[url] = new_url
return new_url
except Exception:
return url
finally:
tmp.unlink(missing_ok=True)
def _extract_and_replace_resources(html: str, token: str) -> str:
"""扫描 HTML,提取 base64 资源和外链资源,上传后替换链接。"""
cache: dict = {}
def replace_data_uri(m):
return _process_data_uri(m.group(0), token, cache)
html = _DATA_URI_RE.sub(replace_data_uri, html)
def replace_src(m):
url = (m.group(1) or m.group(2) or "").strip()
if not url or url.startswith("#") or url.startswith("data:"):
return m.group(0)
if url.startswith("http://") or url.startswith("https://"):
new_url = _process_external_url(url, token, cache)
return m.group(0).replace(url, new_url)
return m.group(0)
html = _SRC_RE.sub(replace_src, html)
return html
# ─── MCP 工具注册 ──────────────────────────────────────────────────────────────
mcp = FastMCP("doc-service")
@mcp.tool()
def set_token(token: str) -> str:
"""
保存 doc-service API token 到本地文件 (~/.doc-service-token)。
token 申请:发邮件至 zhuyngle@163.com。
设置一次后长期有效,无需重复配置。
"""
try:
write_token(token)
return f"Token 已保存至 {TOKEN_FILE}"
except Exception as e:
return f"操作失败: {e}"
@mcp.tool()
def upload_html_content(
content: str,
title: str = "",
expireHours: int = 8760,
) -> str:
"""
上传 HTML 内容字符串,返回公开访问链接(viewUrl)。
Args:
content: 完整的 HTML 内容字符串
title: 文档标题(可选,默认自动生成)
expireHours: 有效期小时数(可选,默认 8760 = 365天)
Returns:
上传成功返回 viewUrl,失败返回错误信息
"""
try:
result = upload_content("html", content, title or None, expireHours)
data = result.get("data", {})
view_url = data.get("viewUrl", "")
page_key = data.get("pageKey", "")
return f"上传成功\nviewUrl: {view_url}\npageKey: {page_key}"
except Exception as e:
return f"操作失败: {e}"
@mcp.tool()
def upload_html_file(
filePath: str,
title: str = "",
expireHours: int = 8760,
) -> str:
"""
上传本地 HTML 文件,返回公开访问链接(viewUrl)。
Args:
filePath: 本地 HTML 文件的绝对路径
title: 文档标题(可选,默认使用文件名)
expireHours: 有效期小时数(可选,默认 8760 = 365天)
Returns:
上传成功返回 viewUrl,失败返回错误信息
"""
try:
result = upload_file("html", filePath, title or None, expireHours)
data = result.get("data", {})
view_url = data.get("viewUrl", "")
page_key = data.get("pageKey", "")
return f"上传成功\nviewUrl: {view_url}\npageKey: {page_key}"
except Exception as e:
return f"操作失败: {e}"
@mcp.tool()
def upload_md_content(
content: str,
title: str = "",
expireHours: int = 8760,
) -> str:
"""
上传 Markdown 内容字符串,返回公开访问链接(viewUrl)。
服务端自动将 Markdown 渲染为带样式的 HTML 页面。
Args:
content: Markdown 格式的文档内容
title: 文档标题(可选,默认自动生成)
expireHours: 有效期小时数(可选,默认 8760 = 365天)
Returns:
上传成功返回 viewUrl,失败返回错误信息
"""
try:
result = upload_content("md", content, title or None, expireHours)
data = result.get("data", {})
view_url = data.get("viewUrl", "")
page_key = data.get("pageKey", "")
return f"上传成功\nviewUrl: {view_url}\npageKey: {page_key}"
except Exception as e:
return f"操作失败: {e}"
@mcp.tool()
def upload_md_file(
filePath: str,
title: str = "",
expireHours: int = 8760,
) -> str:
"""
上传本地 Markdown 文件,返回公开访问链接(viewUrl)。
Args:
filePath: 本地 Markdown 文件的绝对路径
title: 文档标题(可选,默认使用文件名)
expireHours: 有效期小时数(可选,默认 8760 = 365天)
Returns:
上传成功返回 viewUrl,失败返回错误信息
"""
try:
result = upload_file("md", filePath, title or None, expireHours)
data = result.get("data", {})
view_url = data.get("viewUrl", "")
page_key = data.get("pageKey", "")
return f"上传成功\nviewUrl: {view_url}\npageKey: {page_key}"
except Exception as e:
return f"操作失败: {e}"
@mcp.tool()
def upload_html_with_resources(
filePath: str,
title: str = "",
expireHours: int = 8760,
) -> str:
"""
上传本地 HTML 文件,自动提取并上传其中的图片/音频/视频资源,替换链接后再上传 HTML。
与 upload_html_file 的区别:本工具会先扫描 HTML 中的 base64 内嵌资源和外链资源,
逐一上传到 doc-service 文件存储并替换为公网链接,确保 HTML 在任何环境下都能正确加载资源。
Args:
filePath: 本地 HTML 文件的绝对路径
title: 文档标题(可选,默认使用文件名)
expireHours: 有效期小时数(可选,默认 8760 = 365天)
Returns:
上传成功返回 viewUrl,失败返回错误信息
"""
try:
token = _get_token()
path = Path(filePath)
if not path.exists():
return f"操作失败: 文件不存在 {filePath}"
html = path.read_text(encoding="utf-8")
html_replaced = _extract_and_replace_resources(html, token)
# 写入临时文件再上传
tmp = Path(tempfile.gettempdir()) / f"mcp-html-{uuid.uuid4().hex[:8]}.html"
tmp.write_text(html_replaced, encoding="utf-8")
try:
view_url = _upload_html_file_direct(str(tmp), title or path.stem, token)
finally:
tmp.unlink(missing_ok=True)
return f"上传成功\nviewUrl: {view_url}"
except Exception as e:
return f"操作失败: {e}"
@mcp.tool()
def delete_page(pageKey: str, pageType: str) -> str:
"""
删除已上传的文档页面(软删除,内容保留在数据库)。
Args:
pageKey: 上传时返回的 8 位页面唯一标识
pageType: 页面类型,填 "html" 或 "md"
Returns:
删除成功返回确认信息,失败返回错误信息
"""
try:
if pageType not in ("html", "md"):
return "操作失败: pageType 必须为 'html' 或 'md'"
_delete_page(pageKey, pageType)
return f"删除成功,pageKey={pageKey}"
except Exception as e:
return f"操作失败: {e}"
# ─── 入口点 ────────────────────────────────────────────────────────────────────
def main():
mcp.run()
if __name__ == "__main__":
main()
```
### 8.3 注册到 Claude Code
在项目根目录的 `.claude/mcp.json`(若不存在则新建)中添加:
```json
{
"mcpServers": {
"doc-service": {
"type": "stdio",
"command": "python3.11",
"args": ["/path/to/your/server.py"],
"env": {}
}
}
}
```
将 `/path/to/your/server.py` 替换为实际路径。
### 8.4 设置 token
重启 Claude Code 后,在对话中调用:
```
mcp__doc-service__set_token
参数: token = "your_token_here"
```
token 会保存到 `~/.doc-service-token`,后续无需重复设置。
### 8.5 可用 MCP 工具
| 工具名 | 说明 |
|--------|------|
| `set_token` | 保存 API token |
| `upload_html_content` | 上传 HTML 字符串 |
| `upload_html_file` | 上传本地 HTML 文件 |
| `upload_html_with_resources` | 上传 HTML(自动提取并上传内嵌图片) |
| `upload_md_content` | 上传 Markdown 字符串 |
| `upload_md_file` | 上传本地 Markdown 文件 |
| `delete_page` | 删除文档页面 |
### 8.6 典型使用场景
**场景一:Agent 生成 Markdown 报告后直接上传**
Claude 在生成分析报告后,直接调用 `upload_md_content`,将报告内容作为参数传入,获得可分享链接。
**场景二:上传含图片的 HTML 报告**
使用 `upload_html_with_resources`,工具会自动将 HTML 中所有 `base64` 内嵌图片提取出来,分别上传为图片文件并替换链接,最终上传完整 HTML,确保图片在任何设备上均可正常显示。