# 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,确保图片在任何设备上均可正常显示。