From 5d12b884a1c2cb5fec27d78b7424123dcf508608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=B0=E5=AE=81?= Date: Thu, 3 Sep 2026 16:03:44 +0800 Subject: [PATCH] feat(hiagent): restructure into knowledge-scoped MCP server with capability tools --- server/mcp_server_hiagent/.python-version | 1 - server/mcp_server_hiagent/README.md | 223 ------ server/mcp_server_hiagent/README_zh.md | 198 ----- server/mcp_server_hiagent/mcp.json | 8 - .../versions/v3_1_0/__init__.py | 14 - .../versions/v3_1_0/tools/__init__.py | 12 - .../versions/v3_1_0/tools/knowledge.py | 165 ----- .../tests/tools/test_knowledge.py | 150 ---- server/mcp_server_hiagent_knowledge/README.md | 349 +++++++++ .../mcp_server_hiagent_knowledge/README_zh.md | 331 +++++++++ server/mcp_server_hiagent_knowledge/mcp.json | 20 + .../pyproject.toml | 8 +- .../mcp_server_hiagent_knowledge}/__init__.py | 0 .../src/mcp_server_hiagent_knowledge}/main.py | 4 +- .../versions/__init__.py | 2 +- .../versions/v3_1_0/__init__.py | 14 + .../versions/v3_1_0/client.py | 10 +- .../versions/v3_1_0/config.py | 6 + .../versions/v3_1_0/server.py | 32 +- .../versions/v3_1_0/signer.py | 0 .../versions/v3_1_0/tools/__init__.py | 12 + .../versions/v3_1_0/tools/_common.py | 9 - .../versions/v3_1_0/tools/dataset.py | 15 +- .../versions/v3_1_0/tools/knowledge.py | 689 ++++++++++++++++++ .../tests/test_client.py | 4 +- .../tests/test_config.py | 19 +- .../tests/test_server.py | 11 +- .../tests/test_signer.py | 2 +- .../tests/test_versions.py | 2 +- .../tests/tools/test_dataset.py | 28 +- .../tests/tools/test_knowledge.py | 434 +++++++++++ .../uv.lock | 2 +- 32 files changed, 1941 insertions(+), 833 deletions(-) delete mode 100644 server/mcp_server_hiagent/.python-version delete mode 100644 server/mcp_server_hiagent/README.md delete mode 100644 server/mcp_server_hiagent/README_zh.md delete mode 100644 server/mcp_server_hiagent/mcp.json delete mode 100644 server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/__init__.py delete mode 100644 server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/tools/__init__.py delete mode 100644 server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/tools/knowledge.py delete mode 100644 server/mcp_server_hiagent/tests/tools/test_knowledge.py create mode 100644 server/mcp_server_hiagent_knowledge/README.md create mode 100644 server/mcp_server_hiagent_knowledge/README_zh.md create mode 100644 server/mcp_server_hiagent_knowledge/mcp.json rename server/{mcp_server_hiagent => mcp_server_hiagent_knowledge}/pyproject.toml (59%) rename server/{mcp_server_hiagent/src/mcp_server_hiagent => mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge}/__init__.py (100%) rename server/{mcp_server_hiagent/src/mcp_server_hiagent => mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge}/main.py (95%) rename server/{mcp_server_hiagent/src/mcp_server_hiagent => mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge}/versions/__init__.py (95%) create mode 100644 server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/__init__.py rename server/{mcp_server_hiagent/src/mcp_server_hiagent => mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge}/versions/v3_1_0/client.py (91%) rename server/{mcp_server_hiagent/src/mcp_server_hiagent => mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge}/versions/v3_1_0/config.py (91%) rename server/{mcp_server_hiagent/src/mcp_server_hiagent => mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge}/versions/v3_1_0/server.py (50%) rename server/{mcp_server_hiagent/src/mcp_server_hiagent => mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge}/versions/v3_1_0/signer.py (100%) create mode 100644 server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/tools/__init__.py rename server/{mcp_server_hiagent/src/mcp_server_hiagent => mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge}/versions/v3_1_0/tools/_common.py (63%) rename server/{mcp_server_hiagent/src/mcp_server_hiagent => mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge}/versions/v3_1_0/tools/dataset.py (82%) create mode 100644 server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/tools/knowledge.py rename server/{mcp_server_hiagent => mcp_server_hiagent_knowledge}/tests/test_client.py (95%) rename server/{mcp_server_hiagent => mcp_server_hiagent_knowledge}/tests/test_config.py (74%) rename server/{mcp_server_hiagent => mcp_server_hiagent_knowledge}/tests/test_server.py (78%) rename server/{mcp_server_hiagent => mcp_server_hiagent_knowledge}/tests/test_signer.py (95%) rename server/{mcp_server_hiagent => mcp_server_hiagent_knowledge}/tests/test_versions.py (95%) rename server/{mcp_server_hiagent => mcp_server_hiagent_knowledge}/tests/tools/test_dataset.py (68%) create mode 100644 server/mcp_server_hiagent_knowledge/tests/tools/test_knowledge.py rename server/{mcp_server_hiagent => mcp_server_hiagent_knowledge}/uv.lock (99%) diff --git a/server/mcp_server_hiagent/.python-version b/server/mcp_server_hiagent/.python-version deleted file mode 100644 index 2c073331..00000000 --- a/server/mcp_server_hiagent/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.11 diff --git a/server/mcp_server_hiagent/README.md b/server/mcp_server_hiagent/README.md deleted file mode 100644 index 7f46b9d8..00000000 --- a/server/mcp_server_hiagent/README.md +++ /dev/null @@ -1,223 +0,0 @@ -# HiAgent MCP Server - -This MCP server wraps HiAgent Platform OpenAPI capabilities as MCP tools. It currently provides knowledge-engine tools — listing knowledge bases (datasets) in a workspace, inspecting a dataset, and calling the HiAgent knowledge engine to retrieve knowledge chunks — and will keep adding more HiAgent OpenAPI capabilities over time. - -## Features - -- List knowledge bases (datasets) in a workspace -- Inspect a single dataset, including its default retrieval parameters -- Search knowledge in one or more datasets via the knowledge engine (`knowledge_search`) -- Report MCP server and OpenAPI configuration state - -## Setup - -### Prerequisites - -- Python 3.11 or higher -- API credentials (AK/SK) - -### Installation - -Run directly from the repository with uvx (recommended): - -```bash -uvx --from "git+https://github.com/volcengine/mcp-server#subdirectory=server/mcp_server_hiagent" mcp-server-hiagent -``` - -Or with uv, from the compatibility path: - -```bash -cd mcp-server/server/mcp_server_hiagent -uv run mcp-server-hiagent -``` - -### Configuration - -The server requires the following environment variables: - -- `HIAGENT_TOP_HOST`: HiAgent Platform API (volc-top) gateway address, including scheme and port -- `HIAGENT_ACCESS_KEY_ID`: Your HiAgent access key id -- `HIAGENT_SECRET_ACCESS_KEY`: Your HiAgent secret access key - -Optional environment variables: - -- `HIAGENT_VERSION`: HiAgent OpenAPI compatibility version to use. Defaults to the latest registered version (currently `v3.1.0`). Can also be set per-run with the `--hiagent-version` CLI flag, which takes precedence. Selects a self-contained implementation under `versions/`; supported values: `v3.1.0` -- `HIAGENT_ACCOUNT_ID`: Main account id sent as the `X-Account-Id` query parameter, defaults to `1000000000` -- `HIAGENT_REGION`: Region used in AK/SK V4 signing (not a network address), defaults to `cn-north-1` -- `FASTMCP_CHECK_FOR_UPDATES`: Set to `off` to skip FastMCP's startup update check, which otherwise makes an outbound request and can fail startup in restricted networks -- `MCP_SERVER_HOST`: Bind host for the FastMCP server, streamable-http only (default: `127.0.0.1`) -- `MCP_SERVER_PORT`: Bind port for the FastMCP server, streamable-http only (default: `8000`) -- `STREAMABLE_HTTP_PATH`: Streamable HTTP endpoint path (default: `/mcp`) - -## Usage - -### Running the Server - -The server can be run with either stdio transport (for MCP integration, e.g. the HiAgent STDIO plugin) or streamable-http transport: - -```bash -python -m mcp_server_hiagent.main --transport stdio -``` - -Or: - -```bash -python -m mcp_server_hiagent.main --transport streamable-http -``` - -Select a specific HiAgent OpenAPI version explicitly with `--hiagent-version` -(overrides the `HIAGENT_VERSION` environment variable; defaults to the latest -registered version): - -```bash -python -m mcp_server_hiagent.main --hiagent-version v3.1.0 -``` - -### Available Tools - -#### health_check - -Report the MCP server and OpenAPI configuration state. - -```python -health_check() -``` - -#### list_datasets - -List knowledge bases (datasets) in a workspace, so callers can obtain the `DatasetIDs` required by the knowledge engine. - -```python -list_datasets( - workspace_id="workspace_id", - page_number=1, - page_size=20, -) -``` - -Parameters: -- `workspace_id` (required): the workspace id to list datasets for. -- `page_number` (optional): page number (default: 1). -- `page_size` (optional): page size (default: 20). - -#### get_dataset - -Get information about a single dataset, including its default retrieval parameters. - -```python -get_dataset( - workspace_id="workspace_id", - dataset_id="dataset_id", -) -``` - -Parameters: -- `workspace_id` (required): the workspace id the dataset belongs to. -- `dataset_id` (required): the id of the dataset to inspect. - -#### call_knowledge_engine_tool - -Call the HiAgent knowledge engine over one or more datasets. Only `tool_name="knowledge_search"` is supported at present. - -```python -call_knowledge_engine_tool( - workspace_id="workspace_id", - dataset_ids=["dataset_id"], - tool_name="knowledge_search", - queries=["How to reset my password?"], - top_k=3, - score_threshold=0.2, -) -``` - -Parameters: -- `workspace_id` (required): the workspace id the datasets belong to. -- `dataset_ids` (required): list of dataset ids to search, at least one. -- `tool_name` (optional): sub-tool name, defaults to `knowledge_search`. Only `knowledge_search` is supported at present; other known sub-tools (`list_knowledge_chunks`, `grep_chunks`, `get_doc_info`, `wiki_search`, `wiki_read_page`, `wiki_read_source_doc`) are recognized but rejected. -- `queries` (optional): list of query strings (required for `knowledge_search`). -- `top_k` (optional): maximum number of results to return. -- `score_threshold` (optional): relevance score threshold (0~1). -- `rerank_id` (optional): rerank model id. -- `knowledge_run_mode` (optional): run mode, one of `quick` / `smart_search` / `wiki_search`. - -## Best Practices & Test Prompts - -Recommended usage pattern and, for each exposed tool, a natural-language prompt you can give an MCP-enabled agent to exercise it plus the expected result. These prompts double as a manual smoke test after wiring the server into a client. - -**Recommended flow:** `health_check` (confirm config) → `list_datasets` (discover `DatasetIDs`) → optionally `get_dataset` (read default retrieval params) → `call_knowledge_engine_tool` (retrieve). `WorkspaceID` is not discoverable via this server — take it from the HiAgent console URL (`.../workspace//...`). - -#### health_check - -- **Best practice:** call it first, before any credentialed tool, to confirm the server sees your AK/SK and top host. It never calls the OpenAPI and never echoes secrets — only booleans. -- **Test prompt:** "Check whether the HiAgent MCP server is healthy and properly configured." -- **Expected result:** `status="ok"`, `auth="aksk"`, and `configured=true` with each `*_configured` flag true when env vars are set; no credential values are returned. - -#### list_datasets - -- **Best practice:** use it to discover the `DatasetIDs` required by `call_knowledge_engine_tool`; page with `page_number`/`page_size` (1–100) instead of requesting everything at once. `dataset` == knowledge base. -- **Test prompt:** "List the knowledge bases in workspace ``." -- **Expected result:** a paged list of datasets, each with its id and name, that you can feed into the knowledge engine. - -#### get_dataset - -- **Best practice:** call it when you want a dataset's default retrieval parameters (e.g. `RetrievalTopK`, `RetrievalScoreThreshold`) so your `call_knowledge_engine_tool` arguments match how the base was configured. -- **Test prompt:** "Show the details and default retrieval settings of dataset `` in workspace ``." -- **Expected result:** the dataset's metadata including its default retrieval parameters. - -#### call_knowledge_engine_tool - -- **Best practice:** pass 1–5 short, self-contained `queries` (not a whole conversation); start with a small `top_k` (e.g. 3) and a modest `score_threshold` (e.g. 0.2), then tune. Only `tool_name="knowledge_search"` is supported in this version. -- **Test prompt:** "Search datasets `[]` in workspace `` for \"How do I reset my password?\" and return the top 3 chunks." -- **Expected result:** a `Result.KnowledgeSearch.Hits[]` payload where each hit carries `DatasetID` / `DocumentID` / `SegmentID` / `Content`; an unsupported `tool_name` is rejected with a clear error, and invalid arguments (empty `queries`, `score_threshold` outside 0–1) raise a validation error. - -## MCP Integration - -To add this server to your MCP configuration, add the following to your MCP settings file: - -```json -{ - "mcpServers": { - "hiagent": { - "command": "uvx", - "args": [ - "--from", - "git+https://github.com/volcengine/mcp-server#subdirectory=server/mcp_server_hiagent", - "mcp-server-hiagent" - ], - "env": { - "HIAGENT_TOP_HOST": "http://your-top-host:30040", - "HIAGENT_ACCESS_KEY_ID": "your-access-key", - "HIAGENT_SECRET_ACCESS_KEY": "your-secret-key", - "HIAGENT_ACCOUNT_ID": "1000000000", - "HIAGENT_REGION": "cn-north-1", - "FASTMCP_CHECK_FOR_UPDATES": "off" - } - } - } -} -``` - -This uses the STDIO transport (the default), which the HiAgent MCP plugin launches locally and injects credentials into via its environment-variable table. - -## Troubleshooting - -### Common Issues - -1. **Authentication Errors** - - Verify your AK/SK credentials are correct - - Check that you have the necessary permissions for the workspace and datasets - -2. **Startup Failure in Restricted Networks** - - Set `FASTMCP_CHECK_FOR_UPDATES=off` to skip FastMCP's outbound update check - -3. **Empty or Denied Results** - - Verify the `workspace_id` and `dataset_ids` are correct - - Confirm `HIAGENT_TOP_HOST` points to the HiAgent Platform API (volc-top) gateway, not the web or Agent API address - -### Logging - -The server uses Python's logging module with INFO level by default. You can see detailed logs in the console when running the server. - -## License - -volcengine/mcp-server is licensed under the [MIT License](https://github.com/volcengine/mcp-server/blob/main/LICENSE). diff --git a/server/mcp_server_hiagent/README_zh.md b/server/mcp_server_hiagent/README_zh.md deleted file mode 100644 index 7be3a473..00000000 --- a/server/mcp_server_hiagent/README_zh.md +++ /dev/null @@ -1,198 +0,0 @@ -# HiAgent MCP Server - -## 产品描述 - -HiAgent MCP Server 是一个模型上下文协议(Model Context Protocol)服务器,将 HiAgent 平台 OpenAPI 的能力封装为标准 MCP 工具,供 MCP 客户端(如 Claude Desktop、Cursor,以及 HiAgent 平台的 MCP 插件)使用。本 Server 持续接入 HiAgent 平台的各类 OpenAPI 能力,当前已提供知识引擎相关工具:列出指定 workspace 下的知识库、查看知识库详情,并调用知识引擎在指定知识库中检索知识片段;后续将陆续扩展更多能力。 - -## 分类 - -其他 - -## 功能 - -- 列出指定 workspace 下的知识库列表 -- 查看单个知识库的详细信息(含默认检索参数) -- 调用知识引擎在指定知识库中检索知识片段(`knowledge_search`) -- 查看 MCP Server 与 OpenAPI 的配置状态 - -## 使用指南 - -### 前置准备 - -- Python 3.11+ -- UV -- API credentials (AK/SK) - -### 安装 - -克隆仓库: - -```bash -git clone git@github.com:volcengine/mcp-server.git -``` - -### 使用方法 - -启动服务器: - -#### UV - -```bash -cd mcp-server/server/mcp_server_hiagent -uv run mcp-server-hiagent - -# 使用 streamable-http 模式启动(默认为 stdio) -uv run mcp-server-hiagent -t streamable-http - -# 显式指定 HiAgent OpenAPI 版本(覆盖 HIAGENT_VERSION 环境变量;不填默认用最新) -uv run mcp-server-hiagent --hiagent-version v3.1.0 -``` - -使用客户端与服务器交互: - -``` -Trae | Cursor | Claude Desktop | Cline | HiAgent MCP 插件 | ... -``` - -## 配置 - -### 环境变量 - -以下环境变量可用于配置 MCP 服务器: - -| 环境变量 | 描述 | 默认值 | -|---|---|---| -| `HIAGENT_TOP_HOST` | HiAgent Platform API(volc-top)网关地址,含 scheme 与端口 | - | -| `HIAGENT_ACCESS_KEY_ID` | HiAgent 账号 AccessKey ID | - | -| `HIAGENT_SECRET_ACCESS_KEY` | HiAgent 账号 SecretAccessKey | - | -| `HIAGENT_ACCOUNT_ID` | 作为 `X-Account-Id` 查询参数发送的主账号 ID | `1000000000` | -| `HIAGENT_VERSION` | 使用的 HiAgent OpenAPI 兼容版本,对应 `versions/` 下的自包含实现;不填默认使用最新已注册版本(当前为 `v3.1.0`)。也可用 `--hiagent-version` 命令行参数按次指定,且优先级更高;当前支持 `v3.1.0` | 最新(`v3.1.0`) | -| `HIAGENT_REGION` | 用于 AK/SK V4 签名的 Region(非网络地址) | `cn-north-1` | -| `FASTMCP_CHECK_FOR_UPDATES` | 设为 `off`,否则 FastMCP 启动时的联网版本检查在受限网络下可能导致启动失败 | - | -| `MCP_SERVER_HOST` | MCP server 绑定 host(streamable-http) | `127.0.0.1` | -| `MCP_SERVER_PORT` | MCP server 监听端口(streamable-http) | `8000` | - -## 可用工具 - -HiAgent MCP Server 提供以下功能: - -- `health_check`: 返回 MCP server 与 OpenAPI 的配置状态 -- `list_datasets`: 列出指定 workspace 下的知识库列表 -- `get_dataset`: 获取单个知识库的详细信息 -- `call_knowledge_engine_tool`: 调用知识引擎在指定知识库中检索 - -#### health_check - -```python -health_check() -``` - -#### list_datasets - -```python -list_datasets( - workspace_id="workspace_id", - page_number=1, - page_size=20, -) -``` - -Parameters: -- `workspace_id` (必须): 要列出知识库的 workspace ID -- `page_number` (可选): 页码(默认值:1) -- `page_size` (可选): 每页数量(默认值:20) - -#### get_dataset - -```python -get_dataset( - workspace_id="workspace_id", - dataset_id="dataset_id", -) -``` - -Parameters: -- `workspace_id` (必须): 知识库所属的 workspace ID -- `dataset_id` (必须): 要获取信息的知识库 ID - -#### call_knowledge_engine_tool - -```python -call_knowledge_engine_tool( - workspace_id="workspace_id", - dataset_ids=["dataset_id"], - tool_name="knowledge_search", - queries=["如何重置密码?"], - top_k=3, - score_threshold=0.2, -) -``` - -Parameters: -- `workspace_id` (必须): 知识库所属的 workspace ID -- `dataset_ids` (必须): 要检索的知识库 ID 列表,至少 1 个 -- `tool_name` (可选): 子工具名称,默认 `knowledge_search`;当前仅支持 `knowledge_search` -- `queries` (可选): 检索查询词列表(`knowledge_search` 必填) -- `top_k` (可选): 返回的最大结果数 -- `score_threshold` (可选): 相关性分数阈值(0~1) -- `rerank_id` (可选): 重排模型 ID -- `knowledge_run_mode` (可选): 运行模式,枚举 `quick` / `smart_search` / `wiki_search` - -## 最佳实践与测试 Prompt - -推荐的使用顺序,以及每个透出方法的自然语言测试 Prompt 与期望结果——这些 Prompt 也可作为接入 MCP 客户端后的手工冒烟测试。 - -**推荐流程:** `health_check`(确认配置)→ `list_datasets`(获取 `DatasetIDs`)→ 可选 `get_dataset`(读默认检索参数)→ `call_knowledge_engine_tool`(检索)。`WorkspaceID` 无法通过本 Server 列举,需从 HiAgent 控制台网页 URL(`.../workspace//...`)获取。 - -#### health_check - -- **最佳实践:** 在任何需要凭证的工具之前先调用它,确认 Server 已读到 AK/SK 与 top host。它不调用 OpenAPI、不回显任何凭证,只返回布尔值。 -- **测试 Prompt:** “检查 HiAgent MCP Server 是否健康、配置是否齐备。” -- **期望结果:** `status="ok"`、`auth="aksk"`,环境变量齐备时 `configured=true` 且各 `*_configured` 为 true;不返回任何凭证明文。 - -#### list_datasets - -- **最佳实践:** 用它获取 `call_knowledge_engine_tool` 所需的 `DatasetIDs`;用 `page_number`/`page_size`(1~100)分页,不要一次性全量拉取。dataset 即知识库。 -- **测试 Prompt:** “列出 workspace `` 下的知识库。” -- **期望结果:** 分页的知识库列表,每项含 id 与名称,可用于后续知识引擎调用。 - -#### get_dataset - -- **最佳实践:** 当需要某知识库的默认检索参数(如 `RetrievalTopK`、`RetrievalScoreThreshold`)时调用,使 `call_knowledge_engine_tool` 的入参与该库配置保持一致。 -- **测试 Prompt:** “展示 workspace `` 下知识库 `` 的详情与默认检索设置。” -- **期望结果:** 该知识库的元数据,含默认检索参数。 - -#### call_knowledge_engine_tool - -- **最佳实践:** 传入 1~5 条简短、可独立理解的 `queries`(不要传整段对话);`top_k` 从较小值(如 3)起步、`score_threshold` 取适中值(如 0.2)再调优。本版本仅支持 `tool_name="knowledge_search"`。 -- **测试 Prompt:** “在 workspace `` 的知识库 `[]` 中检索「如何重置密码?」,返回相关度最高的 3 个切片。” -- **期望结果:** `Result.KnowledgeSearch.Hits[]`,每个 hit 含 `DatasetID` / `DocumentID` / `SegmentID` / `Content`;不支持的 `tool_name` 返回明确错误,非法入参(`queries` 为空、`score_threshold` 越界)触发校验错误。 - -### uvx 启动 - -```json -{ - "mcpServers": { - "hiagent": { - "command": "uvx", - "args": [ - "--from", - "git+https://github.com/volcengine/mcp-server#subdirectory=server/mcp_server_hiagent", - "mcp-server-hiagent" - ], - "env": { - "HIAGENT_TOP_HOST": "http://your-top-host:30040", - "HIAGENT_ACCESS_KEY_ID": "your-access-key", - "HIAGENT_SECRET_ACCESS_KEY": "your-secret-key", - "HIAGENT_ACCOUNT_ID": "1000000000", - "HIAGENT_REGION": "cn-north-1", - "FASTMCP_CHECK_FOR_UPDATES": "off" - } - } - } -} -``` - -## 证书 - -volcengine/mcp-server is licensed under the [MIT License](https://github.com/volcengine/mcp-server/blob/main/LICENSE). diff --git a/server/mcp_server_hiagent/mcp.json b/server/mcp_server_hiagent/mcp.json deleted file mode 100644 index e22d468b..00000000 --- a/server/mcp_server_hiagent/mcp.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "mcpServers": { - "hiagent": { - "url": "http://127.0.0.1:8000/mcp" - } - } -} - diff --git a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/__init__.py b/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/__init__.py deleted file mode 100644 index fc6bd427..00000000 --- a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -"""HiAgent OpenAPI compatibility implementation for HiAgent version v3.1.0. - -Each supported HiAgent version is a self-contained sub-package under -``mcp_server_hiagent.versions``. If a future HiAgent OpenAPI changes request or -response shapes, add a new ``vX_Y_Z`` package and register it in -``mcp_server_hiagent.versions`` without touching this one. -""" - -from __future__ import annotations - -from mcp_server_hiagent.versions.v3_1_0.config import load_server_config -from mcp_server_hiagent.versions.v3_1_0.server import create_mcp_server - -__all__ = ["create_mcp_server", "load_server_config"] diff --git a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/tools/__init__.py b/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/tools/__init__.py deleted file mode 100644 index 55d0f170..00000000 --- a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/tools/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Business-domain tool registration for HiAgent MCP Server.""" - -from mcp_server_hiagent.versions.v3_1_0.tools._common import OpenAPIClient -from mcp_server_hiagent.versions.v3_1_0.tools.dataset import register_dataset_tools -from mcp_server_hiagent.versions.v3_1_0.tools.knowledge import register_knowledge_tools - - -__all__ = [ - "OpenAPIClient", - "register_dataset_tools", - "register_knowledge_tools", -] diff --git a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/tools/knowledge.py b/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/tools/knowledge.py deleted file mode 100644 index f4bb1c64..00000000 --- a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/tools/knowledge.py +++ /dev/null @@ -1,165 +0,0 @@ -"""HiAgent Knowledge Engine OpenAPI tool. - -``CallKnowledgeEngineTool`` is a dispatcher: ``ToolName`` selects a sub-tool and -the request carries a same-named parameter object (oneof). This module fully -implements the verified ``knowledge_search`` sub-tool; the remaining sub-tools -are recognized but not yet supported (their argument schemas are pending IDL -confirmation) and are rejected with a clear error instead of issuing an invalid -request. -""" - -from __future__ import annotations - -from collections.abc import Sequence - -from fastmcp import FastMCP - -from mcp_server_hiagent.versions.v3_1_0.tools._common import ( - OPENAPI_SERVICE, - OPENAPI_VERSION, - OpenAPIClient, -) - - -# Sub-tools known to exist on CallKnowledgeEngineTool (from the real IDL). -KNOWN_TOOL_NAMES = ( - "knowledge_search", - "list_knowledge_chunks", - "grep_chunks", - "get_doc_info", - "wiki_search", - "wiki_read_page", - "wiki_read_source_doc", -) - -# Sub-tools whose argument object is fully implemented in this MCP. -SUPPORTED_TOOL_NAMES = ("knowledge_search",) - -# Valid values for the optional KnowledgeRunMode field. -KNOWLEDGE_RUN_MODES = ("quick", "smart_search", "wiki_search") - - -def knowledge_search( - client: OpenAPIClient, - *, - workspace_id: str, - dataset_ids: Sequence[str], - queries: Sequence[str], - top_k: int | None = None, - score_threshold: float | None = None, - rerank_id: str | None = None, - knowledge_run_mode: str | None = None, -) -> dict[str, object]: - """Call CallKnowledgeEngineTool with ToolName=knowledge_search.""" - - if not workspace_id: - raise ValueError("workspace_id is required") - if not dataset_ids: - raise ValueError("dataset_ids must contain at least one dataset id") - if not queries: - raise ValueError("queries must contain at least one query") - if score_threshold is not None and not 0 <= score_threshold <= 1: - raise ValueError("score_threshold must be between 0 and 1") - if knowledge_run_mode is not None and knowledge_run_mode not in KNOWLEDGE_RUN_MODES: - raise ValueError( - f"knowledge_run_mode must be one of {KNOWLEDGE_RUN_MODES}" - ) - - search: dict[str, object] = {"Queries": list(queries)} - if top_k is not None: - search["TopK"] = top_k - if score_threshold is not None: - search["ScoreThreshold"] = score_threshold - if rerank_id: - search["RerankID"] = rerank_id - - body: dict[str, object] = { - "WorkspaceID": workspace_id, - "DatasetIDs": list(dataset_ids), - "ToolName": "knowledge_search", - "KnowledgeSearch": search, - } - if knowledge_run_mode is not None: - body["KnowledgeRunMode"] = knowledge_run_mode - - return client.call( - action="CallKnowledgeEngineTool", - version=OPENAPI_VERSION, - service=OPENAPI_SERVICE, - body=body, - ) - - -def call_knowledge_engine_tool( - client: OpenAPIClient, - *, - workspace_id: str, - dataset_ids: Sequence[str], - tool_name: str, - queries: Sequence[str] | None = None, - top_k: int | None = None, - score_threshold: float | None = None, - rerank_id: str | None = None, - knowledge_run_mode: str | None = None, -) -> dict[str, object]: - """Dispatch a HiAgent knowledge engine tool call by ``tool_name``.""" - - if not tool_name: - raise ValueError("tool_name is required") - if tool_name not in KNOWN_TOOL_NAMES: - raise ValueError( - f"unknown tool_name {tool_name!r}; known tools: {KNOWN_TOOL_NAMES}" - ) - if tool_name not in SUPPORTED_TOOL_NAMES: - raise ValueError( - f"tool_name {tool_name!r} is not supported yet; " - f"currently supported: {SUPPORTED_TOOL_NAMES}" - ) - - # Only knowledge_search is supported at present. - return knowledge_search( - client, - workspace_id=workspace_id, - dataset_ids=dataset_ids, - queries=queries or [], - top_k=top_k, - score_threshold=score_threshold, - rerank_id=rerank_id, - knowledge_run_mode=knowledge_run_mode, - ) - - -def register_knowledge_tools(mcp: FastMCP, client: OpenAPIClient) -> None: - """Register the knowledge engine tool on a FastMCP server.""" - - @mcp.tool(name="call_knowledge_engine_tool") - def call_knowledge_engine_tool_tool( - workspace_id: str, - dataset_ids: list[str], - tool_name: str = "knowledge_search", - queries: list[str] | None = None, - top_k: int | None = None, - score_threshold: float | None = None, - rerank_id: str | None = None, - knowledge_run_mode: str | None = None, - ) -> dict[str, object]: - """Call the HiAgent knowledge engine over one or more datasets. - - Only ``tool_name="knowledge_search"`` is supported at present; it - retrieves knowledge chunks for the given ``queries``. Other known - sub-tools (list_knowledge_chunks, grep_chunks, get_doc_info, - wiki_search, wiki_read_page, wiki_read_source_doc) are not yet - supported and will be rejected. - """ - - return call_knowledge_engine_tool( - client, - workspace_id=workspace_id, - dataset_ids=dataset_ids, - tool_name=tool_name, - queries=queries, - top_k=top_k, - score_threshold=score_threshold, - rerank_id=rerank_id, - knowledge_run_mode=knowledge_run_mode, - ) diff --git a/server/mcp_server_hiagent/tests/tools/test_knowledge.py b/server/mcp_server_hiagent/tests/tools/test_knowledge.py deleted file mode 100644 index 9eb13d3e..00000000 --- a/server/mcp_server_hiagent/tests/tools/test_knowledge.py +++ /dev/null @@ -1,150 +0,0 @@ -from __future__ import annotations - -from typing import Any - -import pytest - -from mcp_server_hiagent.versions.v3_1_0.tools.knowledge import ( - SUPPORTED_TOOL_NAMES, - call_knowledge_engine_tool, - knowledge_search, -) - - -class RecordingClient: - def __init__(self) -> None: - self.calls: list[dict[str, Any]] = [] - - def call(self, **kwargs: Any) -> dict[str, object]: - self.calls.append(kwargs) - return { - "ResponseMetadata": {"Action": kwargs["action"]}, - "Result": {"ToolName": "knowledge_search", "KnowledgeSearch": {"Hits": []}}, - } - - -def test_knowledge_search_builds_oneof_request() -> None: - client = RecordingClient() - - knowledge_search( - client, - workspace_id="ws-1", - dataset_ids=["ds-1", "ds-2"], - queries=["hello"], - top_k=3, - score_threshold=0.2, - rerank_id="rk-1", - ) - - assert client.calls[0] == { - "action": "CallKnowledgeEngineTool", - "version": "2023-08-01", - "service": "app", - "body": { - "WorkspaceID": "ws-1", - "DatasetIDs": ["ds-1", "ds-2"], - "ToolName": "knowledge_search", - "KnowledgeSearch": { - "Queries": ["hello"], - "TopK": 3, - "ScoreThreshold": 0.2, - "RerankID": "rk-1", - }, - }, - } - - -def test_knowledge_search_omits_optional_fields() -> None: - client = RecordingClient() - - knowledge_search(client, workspace_id="ws-1", dataset_ids=["ds-1"], queries=["q"]) - - assert client.calls[0]["body"]["KnowledgeSearch"] == {"Queries": ["q"]} - assert "KnowledgeRunMode" not in client.calls[0]["body"] - - -def test_knowledge_search_run_mode() -> None: - client = RecordingClient() - - knowledge_search( - client, - workspace_id="ws-1", - dataset_ids=["ds-1"], - queries=["q"], - knowledge_run_mode="smart_search", - ) - - assert client.calls[0]["body"]["KnowledgeRunMode"] == "smart_search" - - -@pytest.mark.parametrize( - "kwargs", - [ - {"workspace_id": "", "dataset_ids": ["ds-1"], "queries": ["q"]}, - {"workspace_id": "ws-1", "dataset_ids": [], "queries": ["q"]}, - {"workspace_id": "ws-1", "dataset_ids": ["ds-1"], "queries": []}, - ], -) -def test_knowledge_search_required_fields(kwargs: dict[str, Any]) -> None: - with pytest.raises(ValueError): - knowledge_search(RecordingClient(), **kwargs) - - -@pytest.mark.parametrize("bad", [-0.1, 1.1]) -def test_knowledge_search_score_threshold_range(bad: float) -> None: - with pytest.raises(ValueError): - knowledge_search( - RecordingClient(), - workspace_id="ws-1", - dataset_ids=["ds-1"], - queries=["q"], - score_threshold=bad, - ) - - -def test_knowledge_search_invalid_run_mode() -> None: - with pytest.raises(ValueError): - knowledge_search( - RecordingClient(), - workspace_id="ws-1", - dataset_ids=["ds-1"], - queries=["q"], - knowledge_run_mode="nope", - ) - - -def test_dispatch_defaults_to_knowledge_search() -> None: - client = RecordingClient() - - call_knowledge_engine_tool( - client, - workspace_id="ws-1", - dataset_ids=["ds-1"], - tool_name="knowledge_search", - queries=["q"], - ) - - assert client.calls[0]["body"]["ToolName"] == "knowledge_search" - - -def test_dispatch_rejects_unknown_tool() -> None: - with pytest.raises(ValueError): - call_knowledge_engine_tool( - RecordingClient(), - workspace_id="ws-1", - dataset_ids=["ds-1"], - tool_name="does_not_exist", - queries=["q"], - ) - - -def test_dispatch_rejects_known_but_unsupported_tool() -> None: - # wiki_search is a known sub-tool but not yet supported. - assert "wiki_search" not in SUPPORTED_TOOL_NAMES - with pytest.raises(ValueError): - call_knowledge_engine_tool( - RecordingClient(), - workspace_id="ws-1", - dataset_ids=["ds-1"], - tool_name="wiki_search", - ) diff --git a/server/mcp_server_hiagent_knowledge/README.md b/server/mcp_server_hiagent_knowledge/README.md new file mode 100644 index 00000000..2bc68b5f --- /dev/null +++ b/server/mcp_server_hiagent_knowledge/README.md @@ -0,0 +1,349 @@ +# HiAgent Knowledge MCP Server + +This MCP server exposes a **knowledge base (knowledge engine)** as MCP tools. Each tool is named after the user-facing **capability** it provides rather than a raw OpenAPI action. It provides knowledge base discovery and the full knowledge-engine tool set (semantic search, regex grep, document metadata/chunks, and Wiki search/read). + +## Features + +- List knowledge bases (datasets) in a workspace, and inspect a single dataset +- Search knowledge across datasets by relevance (`search_knowledge`) +- Match chunks by RE2 regex (`grep_knowledge_chunks`) +- Read documents' metadata in batch (`list_document_infos`) and a document's chunks in order (`list_document_chunks`) +- Search generated Wiki pages (`search_wiki`), read a page (`read_wiki_page`), and trace its sources — by page slug (`read_wiki_source_chunk`) or by a referenced source document's resource id (`read_wiki_source_doc`) +- Report MCP server and OpenAPI configuration state + +### Capability-based tool naming + +The HiAgent OpenAPI exposes the knowledge engine through a single `CallKnowledgeEngineTool` action whose `ToolName` field selects a sub-tool and whose request carries a same-named PascalCase parameter object. This server maps each `(ToolName, parameter object)` combination to its own capability-named MCP tool with a flat argument schema: + +| MCP tool | OpenAPI action | `ToolName` | Parameter object | Capability | +|---|---|---|---|---| +| `search_knowledge` | `CallKnowledgeEngineTool` | `knowledge_search` | `KnowledgeSearch` | Relevance search across datasets | +| `grep_knowledge_chunks` | `CallKnowledgeEngineTool` | `grep_chunks` | `GrepChunks` | RE2 regex match over candidate chunks | +| `list_document_infos` | `CallKnowledgeEngineTool` | `list_doc_infos` | `ListDocInfos` | Documents' metadata, batched by dataset | +| `list_document_chunks` | `CallKnowledgeEngineTool` | `list_knowledge_chunks` | `ListKnowledgeChunks` | Sequential read of one document's chunks | +| `search_wiki` | `CallKnowledgeEngineTool` | `wiki_search` | `WikiSearch` | Search generated Wiki pages | +| `read_wiki_page` | `CallKnowledgeEngineTool` | `wiki_read_page` | `WikiReadPage` | Read a Wiki page by slug | +| `read_wiki_source_chunk` | `CallKnowledgeEngineTool` | `wiki_read_source_chunk` | `WikiReadSourceChunk` | Read a Wiki page's referenced source chunks, by page slug | +| `read_wiki_source_doc` | `CallKnowledgeEngineTool` | `list_knowledge_chunks` | `ListKnowledgeChunks` | Read one referenced source document's chunks in order, by resource id | + +> Note: in HiAgent a *dataset* is a *knowledge base*, so `list_datasets` / `get_dataset` are the "list/inspect knowledge base" capabilities. Every knowledge-engine tool also accepts an optional `user_info` (`{"UserID": ..., "UserChannel": ...}`) forwarded verbatim as the OpenAPI `UserInfo` end-user identity. + +> Validation model: this server is a thin adapter. It only checks that the fields it must always send are present (workspace/dataset ids, a pattern/slug/queries where the capability requires one) and relies on argument types from the tool schema. Value ranges, enums and cross-field rules (e.g. `top_k`/`limit`/`overlap` bounds, `grep_type` scope rules) are owned by the backend service and are version-specific, so its `InvalidParameter.*` errors are passed through to the caller rather than duplicated here. + +## Setup + +### Prerequisites + +- Python 3.11 or higher +- API credentials (AK/SK) + +### Installation + +Run directly from the repository with uvx (recommended): + +```bash +uvx --from "git+https://github.com/volcengine/mcp-server#subdirectory=server/mcp_server_hiagent_knowledge" mcp-server-hiagent-knowledge +``` + +Or with uv, from the compatibility path: + +```bash +cd mcp-server/server/mcp_server_hiagent_knowledge +uv run mcp-server-hiagent-knowledge +``` + +### Configuration + +The server requires the following environment variables: + +- `HIAGENT_TOP_HOST`: HiAgent Platform API (volc-top) gateway address, including scheme and port +- `HIAGENT_ACCESS_KEY_ID`: Your HiAgent access key id +- `HIAGENT_SECRET_ACCESS_KEY`: Your HiAgent secret access key + +Optional environment variables: + +- `HIAGENT_VERSION`: HiAgent OpenAPI compatibility version to use. Defaults to the latest registered version (currently `v3.1.0`). Can also be set per-run with the `--hiagent-version` CLI flag, which takes precedence. Selects a self-contained implementation under `versions/`; supported values: `v3.1.0` +- `HIAGENT_ACCOUNT_ID`: Main account id sent as the `X-Account-Id` query parameter, defaults to `1000000000` +- `HIAGENT_REGION`: Region used in AK/SK V4 signing (not a network address), defaults to `cn-north-1` +- `FASTMCP_CHECK_FOR_UPDATES`: Set to `off` to skip FastMCP's startup update check, which otherwise makes an outbound request and can fail startup in restricted networks +- `MCP_SERVER_HOST`: Bind host for the FastMCP server, streamable-http only (default: `127.0.0.1`) +- `MCP_SERVER_PORT`: Bind port for the FastMCP server, streamable-http only (default: `8000`) +- `STREAMABLE_HTTP_PATH`: Streamable HTTP endpoint path (default: `/mcp`) + +## Usage + +### Running the Server + +The server can be run with either stdio transport (for MCP integration, e.g. the HiAgent STDIO plugin) or streamable-http transport: + +```bash +python -m mcp_server_hiagent_knowledge.main --transport stdio +``` + +Or: + +```bash +python -m mcp_server_hiagent_knowledge.main --transport streamable-http +``` + +Select a specific HiAgent OpenAPI version explicitly with `--hiagent-version` +(overrides the `HIAGENT_VERSION` environment variable; defaults to the latest +registered version): + +```bash +python -m mcp_server_hiagent_knowledge.main --hiagent-version v3.1.0 +``` + +### Available Tools + +#### health_check + +Report the MCP server and OpenAPI configuration state. + +```python +health_check() +``` + +#### list_datasets + +List knowledge bases (datasets) in a workspace, so callers can obtain the `DatasetIDs` required by the knowledge engine. + +```python +list_datasets( + workspace_id="workspace_id", + page_number=1, + page_size=20, +) +``` + +Parameters: +- `workspace_id` (required): the workspace id to list datasets for. +- `page_number` (optional): page number (default: 1). +- `page_size` (optional): page size (default: 20). + +#### get_dataset + +Get information about a single dataset, including its default retrieval parameters. + +```python +get_dataset( + workspace_id="workspace_id", + dataset_id="dataset_id", +) +``` + +Parameters: +- `workspace_id` (required): the workspace id the dataset belongs to. +- `dataset_id` (required): the id of the dataset to inspect. + +#### search_knowledge + +Search knowledge across one or more datasets and return the chunks most relevant to your queries. + +```python +search_knowledge( + workspace_id="workspace_id", + dataset_ids=["dataset_id"], + queries=["How to reset my password?"], + top_k=3, + score_threshold=0.2, +) +``` + +Parameters: +- `workspace_id` (required): the workspace id the datasets belong to. +- `dataset_ids` (required): list of dataset ids to search, at least one. +- `queries` (required): list of natural-language query strings, at least one. +- `top_k` (optional): maximum number of results to return. +- `score_threshold` (optional): minimum relevance score to keep. +- `rerank_id` (optional): rerank model id. + +#### grep_knowledge_chunks + +Match knowledge chunks by one RE2 regular expression. Use for exact tokens (error codes, identifiers, fixed phrases) when semantic search is insufficient. Narrows candidates first, then applies the pattern — not an exhaustive full-dataset scan. + +```python +grep_knowledge_chunks( + workspace_id="workspace_id", + dataset_ids=["dataset_id"], + pattern="ERR\\d+", + queries=["error code"], + limit=10, +) +``` + +Parameters: +- `workspace_id` (required): the workspace id the datasets belong to. +- `dataset_ids` (required): dataset ids to search, at least one. +- `pattern` (required): one RE2 regular expression (no backreferences/lookarounds). +- `queries` (optional): queries to narrow the candidate set. +- `limit` (optional): maximum number of matches. +- `grep_type` (optional): scan scope, `dataset_ids` (default) or `resource_ids`. +- `resource_ids` (optional): document/resource ids to restrict the scan to; required when `grep_type` is `resource_ids`. + +#### list_document_infos + +Get metadata for one or more documents, batched by dataset (title, type, size, status, segment count, timestamps). Metadata only — not document content. + +```python +list_document_infos( + workspace_id="workspace_id", + dataset_ids=["dataset_id"], + resource_ids={"dataset_id": ["resource_id_1", "resource_id_2"]}, +) +``` + +Parameters: +- `workspace_id` (required): the workspace id the datasets belong to. +- `dataset_ids` (required): dataset ids involved, at least one. +- `resource_ids` (required): map of dataset id → list of document/resource ids to describe; keys must be within `dataset_ids`. + +#### list_document_chunks + +List the knowledge chunks of a single document/resource in reading order. Unlike `search_knowledge` (relevance-ranked for a query), this walks one resource sequentially — useful for browsing a document's full content. + +```python +list_document_chunks( + workspace_id="workspace_id", + dataset_ids=["dataset_id"], + resource_id="resource_id", + limit=50, +) +``` + +Parameters: +- `workspace_id` (required): the workspace id the datasets belong to. +- `dataset_ids` (required): list of dataset ids the resource belongs to, at least one. +- `resource_id` (required): the document/resource whose chunks to list. +- `limit` (optional): maximum number of chunks per page. +- `cursor_segment_id` (optional): segment id to continue paging from (pass the last returned segment id). + +#### search_wiki + +Search generated Wiki pages for concepts and topic pages. Returns page candidates (with `Slug`) for navigation, not final evidence. + +```python +search_wiki( + workspace_id="workspace_id", + dataset_ids=["dataset_id"], + queries=["reverse acquisition"], + limit=5, +) +``` + +Parameters: +- `workspace_id` (required): the workspace id the datasets belong to. +- `dataset_ids` (required): dataset ids to search, at least one. +- `queries` (required): natural-language query strings, at least one. +- `limit` (optional): maximum number of pages. + +#### read_wiki_page + +Read one generated Wiki page by slug (structure, summary, content). Wiki pages are generated navigation material, not final evidence. + +```python +read_wiki_page( + workspace_id="workspace_id", + dataset_ids=["dataset_id"], + slug="concept/reverse-acquisition", +) +``` + +Parameters: +- `workspace_id` (required): the workspace id the datasets belong to. +- `dataset_ids` (required): dataset ids the page belongs to, at least one. +- `slug` (required): the Wiki page slug (from `search_wiki`). + +#### read_wiki_source_chunk + +Read a Wiki page's referenced source chunks, resolved by the page slug — the final evidence for facts, numbers, quotations and code. To instead read one referenced source document's chunks in order by its resource id, use `read_wiki_source_doc`. + +```python +read_wiki_source_chunk( + workspace_id="workspace_id", + dataset_ids=["dataset_id"], + slug="concept/reverse-acquisition", + limit=5, + overlap=2, +) +``` + +Parameters: +- `workspace_id` (required): the workspace id the datasets belong to. +- `dataset_ids` (required): dataset ids the page belongs to, at least one. +- `slug` (required): the Wiki page slug whose sources to read. +- `limit` (optional): maximum number of source chunks per page. +- `overlap` (optional): number of adjacent segments to expand around each referenced chunk for more context (0 disables expansion). +- `cursor_segment_id` (optional): segment id to continue paging from. + +#### read_wiki_source_doc + +Read one referenced source document's chunks in reading order, by resource id. Companion to `read_wiki_source_chunk`: that resolves a page's referenced chunks by slug; this walks a single referenced source document (`resource_id`, e.g. from a Wiki page's `SourceRefs`) sequentially. + +```python +read_wiki_source_doc( + workspace_id="workspace_id", + dataset_ids=["dataset_id"], + resource_id="resource_id", + limit=50, +) +``` + +Parameters: +- `workspace_id` (required): the workspace id the datasets belong to. +- `dataset_ids` (required): dataset ids the resource belongs to, at least one. +- `resource_id` (required): the referenced source document/resource whose chunks to read. +- `limit` (optional): maximum number of chunks per page. +- `cursor_segment_id` (optional): segment id to continue paging from. + +## MCP Integration + +To add this server to your MCP configuration, add the following to your MCP settings file: + +```json +{ + "mcpServers": { + "hiagent-knowledge": { + "command": "uvx", + "args": [ + "--from", + "git+https://github.com/volcengine/mcp-server#subdirectory=server/mcp_server_hiagent_knowledge", + "mcp-server-hiagent-knowledge" + ], + "env": { + "HIAGENT_TOP_HOST": "http://your-top-host:30040", + "HIAGENT_ACCESS_KEY_ID": "your-access-key", + "HIAGENT_SECRET_ACCESS_KEY": "your-secret-key", + "HIAGENT_ACCOUNT_ID": "1000000000", + "HIAGENT_REGION": "cn-north-1", + "FASTMCP_CHECK_FOR_UPDATES": "off" + } + } + } +} +``` + +This uses the STDIO transport (the default), which the HiAgent MCP plugin launches locally and injects credentials into via its environment-variable table. + +## Troubleshooting + +### Common Issues + +1. **Authentication Errors** + - Verify your AK/SK credentials are correct + - Check that you have the necessary permissions for the workspace and datasets + +2. **Startup Failure in Restricted Networks** + - Set `FASTMCP_CHECK_FOR_UPDATES=off` to skip FastMCP's outbound update check + +3. **Empty or Denied Results** + - Verify the `workspace_id` and `dataset_ids` are correct + - Confirm `HIAGENT_TOP_HOST` points to the HiAgent Platform API (volc-top) gateway, not the web or Agent API address + +### Logging + +The server uses Python's logging module with INFO level by default. You can see detailed logs in the console when running the server. + +## License + +volcengine/mcp-server is licensed under the [MIT License](https://github.com/volcengine/mcp-server/blob/main/LICENSE). diff --git a/server/mcp_server_hiagent_knowledge/README_zh.md b/server/mcp_server_hiagent_knowledge/README_zh.md new file mode 100644 index 00000000..4e849617 --- /dev/null +++ b/server/mcp_server_hiagent_knowledge/README_zh.md @@ -0,0 +1,331 @@ +# HiAgent Knowledge MCP Server + +## 产品描述 + +HiAgent Knowledge MCP Server 是一个模型上下文协议(Model Context Protocol)服务器,将**知识库(知识引擎)**能力封装为标准 MCP 工具,供 MCP 客户端(如 Claude Desktop、Cursor,以及 HiAgent 平台的 MCP 插件)使用。每个工具都按其对用户暴露的**能力**命名,而非直接照搬底层 OpenAPI 的 action 名。当前提供知识库发现(列知识库、查详情)与完整的知识引擎工具集(语义检索、正则匹配、文档元数据/切片、Wiki 检索/阅读)。 + +## 分类 + +其他 + +## 功能 + +- 列出指定 workspace 下的知识库列表,并查看单个知识库详情 +- 在一个或多个知识库中按相关性检索知识片段(`search_knowledge`) +- 按 RE2 正则匹配切片(`grep_knowledge_chunks`) +- 批量读取文档元数据(`list_document_infos`)与按顺序读取文档切片(`list_document_chunks`) +- 搜索生成的 Wiki 页面(`search_wiki`)、读取 Wiki 页面(`read_wiki_page`),并溯源:按页面 slug 读引用切片(`read_wiki_source_chunk`)或按引用源文档的 resource_id 顺序读原文(`read_wiki_source_doc`) +- 查看 MCP Server 与 OpenAPI 的配置状态 + +### 面向能力的工具命名 + +HiAgent OpenAPI 通过单个 `CallKnowledgeEngineTool` action 暴露知识引擎:`ToolName` 字段选择子工具,请求体携带同名 PascalCase 参数对象。本 Server 把每个 `(ToolName, 参数对象)` 组合映射为一个面向能力命名、参数扁平的独立 MCP 工具: + +| MCP 工具 | OpenAPI action | `ToolName` | 参数对象 | 能力 | +|---|---|---|---|---| +| `search_knowledge` | `CallKnowledgeEngineTool` | `knowledge_search` | `KnowledgeSearch` | 跨知识库按相关性检索 | +| `grep_knowledge_chunks` | `CallKnowledgeEngineTool` | `grep_chunks` | `GrepChunks` | 候选切片内 RE2 正则匹配 | +| `list_document_infos` | `CallKnowledgeEngineTool` | `list_doc_infos` | `ListDocInfos` | 按知识库批量查文档元数据 | +| `list_document_chunks` | `CallKnowledgeEngineTool` | `list_knowledge_chunks` | `ListKnowledgeChunks` | 顺序读取单个文档的分片 | +| `search_wiki` | `CallKnowledgeEngineTool` | `wiki_search` | `WikiSearch` | 搜索生成的 Wiki 页面 | +| `read_wiki_page` | `CallKnowledgeEngineTool` | `wiki_read_page` | `WikiReadPage` | 按 slug 读取 Wiki 页面 | +| `read_wiki_source_chunk` | `CallKnowledgeEngineTool` | `wiki_read_source_chunk` | `WikiReadSourceChunk` | 按页面 slug 读取 Wiki 引用的切片 | +| `read_wiki_source_doc` | `CallKnowledgeEngineTool` | `list_knowledge_chunks` | `ListKnowledgeChunks` | 按 resource_id 顺序读取某引用源文档的切片 | + +> 说明:HiAgent 中 dataset 即知识库,故 `list_datasets` / `get_dataset` 是「列/查知识库」能力。每个知识引擎工具还接受可选的 `user_info`(`{"UserID": ..., "UserChannel": ...}`),原样透传为 OpenAPI 的 `UserInfo` 终端用户身份。 + +> 校验模型:本 Server 是薄适配层,只校验「必须发送的字段是否存在」(workspace/dataset ID、能力要求的 pattern/slug/queries 等)并依赖工具 schema 的参数类型;数值范围、枚举、跨字段规则(如 `top_k`/`limit`/`overlap` 的上下限、`grep_type` 的范围约束)由后端服务负责且随版本变化,因此其 `InvalidParameter.*` 报错会原样透传给调用方,不在本层重复校验。 + +## 使用指南 + +### 前置准备 + +- Python 3.11+ +- UV +- API credentials (AK/SK) + +### 安装 + +克隆仓库: + +```bash +git clone git@github.com:volcengine/mcp-server.git +``` + +### 使用方法 + +启动服务器: + +#### UV + +```bash +cd mcp-server/server/mcp_server_hiagent_knowledge +uv run mcp-server-hiagent-knowledge + +# 使用 streamable-http 模式启动(默认为 stdio) +uv run mcp-server-hiagent-knowledge -t streamable-http + +# 显式指定 HiAgent OpenAPI 版本(覆盖 HIAGENT_VERSION 环境变量;不填默认用最新) +uv run mcp-server-hiagent-knowledge --hiagent-version v3.1.0 +``` + +使用客户端与服务器交互: + +``` +Trae | Cursor | Claude Desktop | Cline | HiAgent MCP 插件 | ... +``` + +## 配置 + +### 环境变量 + +以下环境变量可用于配置 MCP 服务器: + +| 环境变量 | 描述 | 默认值 | +|---|---|---| +| `HIAGENT_TOP_HOST` | HiAgent Platform API(volc-top)网关地址,含 scheme 与端口 | - | +| `HIAGENT_ACCESS_KEY_ID` | HiAgent 账号 AccessKey ID | - | +| `HIAGENT_SECRET_ACCESS_KEY` | HiAgent 账号 SecretAccessKey | - | +| `HIAGENT_ACCOUNT_ID` | 作为 `X-Account-Id` 查询参数发送的主账号 ID | `1000000000` | +| `HIAGENT_VERSION` | 使用的 HiAgent OpenAPI 兼容版本,对应 `versions/` 下的自包含实现;不填默认使用最新已注册版本(当前为 `v3.1.0`)。也可用 `--hiagent-version` 命令行参数按次指定,且优先级更高;当前支持 `v3.1.0` | 最新(`v3.1.0`) | +| `HIAGENT_REGION` | 用于 AK/SK V4 签名的 Region(非网络地址) | `cn-north-1` | +| `FASTMCP_CHECK_FOR_UPDATES` | 设为 `off`,否则 FastMCP 启动时的联网版本检查在受限网络下可能导致启动失败 | - | +| `MCP_SERVER_HOST` | MCP server 绑定 host(streamable-http) | `127.0.0.1` | +| `MCP_SERVER_PORT` | MCP server 监听端口(streamable-http) | `8000` | + +## 可用工具 + +HiAgent Knowledge MCP Server 提供以下功能: + +- `health_check`: 返回 MCP server 与 OpenAPI 的配置状态 +- `list_datasets`: 列出指定 workspace 下的知识库列表 +- `get_dataset`: 获取单个知识库的详细信息 +- `search_knowledge`: 在一个或多个知识库中按相关性检索知识片段 +- `grep_knowledge_chunks`: 按 RE2 正则匹配知识切片 +- `list_document_infos`: 按知识库批量获取文档元数据 +- `list_document_chunks`: 按顺序列出单个文档/资源的知识分片 +- `search_wiki`: 搜索生成的 Wiki 页面 +- `read_wiki_page`: 按 slug 读取 Wiki 页面 +- `read_wiki_source_chunk`: 读取 Wiki 页引用的原始文档切片 +- `read_wiki_source_doc`: 按 resource_id 顺序读取某引用源文档的切片 + +#### health_check + +```python +health_check() +``` + +#### list_datasets + +```python +list_datasets( + workspace_id="workspace_id", + page_number=1, + page_size=20, +) +``` + +Parameters: +- `workspace_id` (必须): 要列出知识库的 workspace ID +- `page_number` (可选): 页码(默认值:1) +- `page_size` (可选): 每页数量(默认值:20) + +#### get_dataset + +```python +get_dataset( + workspace_id="workspace_id", + dataset_id="dataset_id", +) +``` + +Parameters: +- `workspace_id` (必须): 知识库所属的 workspace ID +- `dataset_id` (必须): 要获取信息的知识库 ID + +#### search_knowledge + +```python +search_knowledge( + workspace_id="workspace_id", + dataset_ids=["dataset_id"], + queries=["如何重置密码?"], + top_k=3, + score_threshold=0.2, +) +``` + +Parameters: +- `workspace_id` (必须): 知识库所属的 workspace ID +- `dataset_ids` (必须): 要检索的知识库 ID 列表,至少 1 个 +- `queries` (必须): 检索查询词列表,至少 1 个 +- `top_k` (可选): 返回的最大结果数 +- `score_threshold` (可选): 保留结果的最小相关性分数 +- `rerank_id` (可选): 重排模型 ID + +#### grep_knowledge_chunks + +按一条 RE2 正则匹配知识切片;用于错误码、标识符、固定短语等精确定位。先召回候选再匹配,不是全库扫描。 + +```python +grep_knowledge_chunks( + workspace_id="workspace_id", + dataset_ids=["dataset_id"], + pattern="ERR\\d+", + queries=["错误码"], + limit=10, +) +``` + +Parameters: +- `workspace_id` (必须): 知识库所属的 workspace ID +- `dataset_ids` (必须): 要检索的知识库 ID 列表,至少 1 个 +- `pattern` (必须): 一条 RE2 正则(不支持反向引用/前后瞻) +- `queries` (可选): 用于缩小候选集的查询词 +- `limit` (可选): 命中上限 +- `grep_type` (可选): 扫描范围,`dataset_ids`(默认)或 `resource_ids` +- `resource_ids` (可选): 限定扫描的文档/资源 ID 列表;当 `grep_type` 为 `resource_ids` 时必填 + +#### list_document_infos + +按知识库批量获取一个或多个文档的元数据(标题、类型、大小、状态、分段数、时间戳)。仅元数据,非文档内容。 + +```python +list_document_infos( + workspace_id="workspace_id", + dataset_ids=["dataset_id"], + resource_ids={"dataset_id": ["resource_id_1", "resource_id_2"]}, +) +``` + +Parameters: +- `workspace_id` (必须): 知识库所属的 workspace ID +- `dataset_ids` (必须): 涉及的知识库 ID 列表,至少 1 个 +- `resource_ids` (必须): 知识库 ID → 该库下文档/资源 ID 列表的映射;key 必须在 `dataset_ids` 内 + +#### list_document_chunks + +按阅读顺序列出单个文档/资源的知识分片;与 `search_knowledge`(按查询相关性排序)不同,本工具顺序遍历一个资源,适合浏览文档全文。 + +```python +list_document_chunks( + workspace_id="workspace_id", + dataset_ids=["dataset_id"], + resource_id="resource_id", + limit=50, +) +``` + +Parameters: +- `workspace_id` (必须): 知识库所属的 workspace ID +- `dataset_ids` (必须): 资源所属的知识库 ID 列表,至少 1 个 +- `resource_id` (必须): 要列出分片的文档/资源 ID +- `limit` (可选): 每页返回的最大分片数 +- `cursor_segment_id` (可选): 续页游标,传入上一页返回的最后一个 segment ID + +#### search_wiki + +搜索生成的 Wiki 页面,返回页面候选(含 `Slug`)用于导航,不作为最终证据。 + +```python +search_wiki( + workspace_id="workspace_id", + dataset_ids=["dataset_id"], + queries=["反向购买"], + limit=5, +) +``` + +Parameters: +- `workspace_id` (必须): 知识库所属的 workspace ID +- `dataset_ids` (必须): 要检索的知识库 ID 列表,至少 1 个 +- `queries` (必须): 检索查询词列表,至少 1 个 +- `limit` (可选): 返回页面上限 + +#### read_wiki_page + +按 slug 读取单个 Wiki 页面(结构、摘要、内容)。Wiki 页面是生成的导航材料,非最终证据。 + +```python +read_wiki_page( + workspace_id="workspace_id", + dataset_ids=["dataset_id"], + slug="concept/reverse-acquisition", +) +``` + +Parameters: +- `workspace_id` (必须): 知识库所属的 workspace ID +- `dataset_ids` (必须): 页面所属的知识库 ID 列表,至少 1 个 +- `slug` (必须): Wiki 页面 slug(来自 `search_wiki`) + +#### read_wiki_source_chunk + +按页面 slug 读取 Wiki 页引用的切片——事实、数字、引文、代码的最终证据来源。若想按引用源文档的 resource_id 顺序读原文,改用 `read_wiki_source_doc`。 + +```python +read_wiki_source_chunk( + workspace_id="workspace_id", + dataset_ids=["dataset_id"], + slug="concept/reverse-acquisition", + limit=5, + overlap=2, +) +``` + +Parameters: +- `workspace_id` (必须): 知识库所属的 workspace ID +- `dataset_ids` (必须): 页面所属的知识库 ID 列表,至少 1 个 +- `slug` (必须): 要读取来源的 Wiki 页面 slug +- `limit` (可选): 每页返回的最大源切片数 +- `overlap` (可选): 每个引用切片前后各扩展的邻近分段数,用于补充上下文(0 表示不扩展) +- `cursor_segment_id` (可选): 续页游标 + +#### read_wiki_source_doc + +按 resource_id 顺序读取某个被引用源文档的切片。与 `read_wiki_source_chunk` 互补:后者按页面 slug 读引用切片;本工具顺序遍历单个被引用源文档(`resource_id`,如取自 Wiki 页面的 `SourceRefs`)。 + +```python +read_wiki_source_doc( + workspace_id="workspace_id", + dataset_ids=["dataset_id"], + resource_id="resource_id", + limit=50, +) +``` + +Parameters: +- `workspace_id` (必须): 知识库所属的 workspace ID +- `dataset_ids` (必须): 资源所属的知识库 ID 列表,至少 1 个 +- `resource_id` (必须): 要读取的被引用源文档/资源 ID +- `limit` (可选): 每页返回的最大切片数 +- `cursor_segment_id` (可选): 续页游标 + +### uvx 启动 + +```json +{ + "mcpServers": { + "hiagent-knowledge": { + "command": "uvx", + "args": [ + "--from", + "git+https://github.com/volcengine/mcp-server#subdirectory=server/mcp_server_hiagent_knowledge", + "mcp-server-hiagent-knowledge" + ], + "env": { + "HIAGENT_TOP_HOST": "http://your-top-host:30040", + "HIAGENT_ACCESS_KEY_ID": "your-access-key", + "HIAGENT_SECRET_ACCESS_KEY": "your-secret-key", + "HIAGENT_ACCOUNT_ID": "1000000000", + "HIAGENT_REGION": "cn-north-1", + "FASTMCP_CHECK_FOR_UPDATES": "off" + } + } + } +} +``` + +## 证书 + +volcengine/mcp-server is licensed under the [MIT License](https://github.com/volcengine/mcp-server/blob/main/LICENSE). diff --git a/server/mcp_server_hiagent_knowledge/mcp.json b/server/mcp_server_hiagent_knowledge/mcp.json new file mode 100644 index 00000000..d59edbb0 --- /dev/null +++ b/server/mcp_server_hiagent_knowledge/mcp.json @@ -0,0 +1,20 @@ +{ + "mcpServers": { + "hiagent-knowledge": { + "command": "uvx", + "args": [ + "--from", + "git+https://github.com/volcengine/mcp-server#subdirectory=server/mcp_server_hiagent_knowledge", + "mcp-server-hiagent-knowledge" + ], + "env": { + "HIAGENT_TOP_HOST": "http://your-top-host:30040", + "HIAGENT_ACCESS_KEY_ID": "your-access-key", + "HIAGENT_SECRET_ACCESS_KEY": "your-secret-key", + "HIAGENT_ACCOUNT_ID": "1000000000", + "HIAGENT_REGION": "cn-north-1", + "FASTMCP_CHECK_FOR_UPDATES": "off" + } + } + } +} diff --git a/server/mcp_server_hiagent/pyproject.toml b/server/mcp_server_hiagent_knowledge/pyproject.toml similarity index 59% rename from server/mcp_server_hiagent/pyproject.toml rename to server/mcp_server_hiagent_knowledge/pyproject.toml index 817992af..d345408a 100644 --- a/server/mcp_server_hiagent/pyproject.toml +++ b/server/mcp_server_hiagent_knowledge/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "mcp-server-hiagent" +name = "mcp-server-hiagent-knowledge" version = "0.1.0" -description = "MCP server for HiAgent OpenAPI" +description = "MCP server for the HiAgent Knowledge (knowledge base) engine" readme = "README.md" requires-python = ">=3.11" license = {text = "MIT"} @@ -11,14 +11,14 @@ dependencies = [ ] [project.scripts] -mcp-server-hiagent = "mcp_server_hiagent.main:main" +mcp-server-hiagent-knowledge = "mcp_server_hiagent_knowledge.main:main" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["src/mcp_server_hiagent"] +packages = ["src/mcp_server_hiagent_knowledge"] [dependency-groups] dev = [ diff --git a/server/mcp_server_hiagent/src/mcp_server_hiagent/__init__.py b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/__init__.py similarity index 100% rename from server/mcp_server_hiagent/src/mcp_server_hiagent/__init__.py rename to server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/__init__.py diff --git a/server/mcp_server_hiagent/src/mcp_server_hiagent/main.py b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/main.py similarity index 95% rename from server/mcp_server_hiagent/src/mcp_server_hiagent/main.py rename to server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/main.py index e7a4c0e0..fdc252bf 100644 --- a/server/mcp_server_hiagent/src/mcp_server_hiagent/main.py +++ b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/main.py @@ -6,7 +6,7 @@ import logging import os -from mcp_server_hiagent.versions import ( +from mcp_server_hiagent_knowledge.versions import ( DEFAULT_VERSION, SUPPORTED_VERSIONS, load_version_module, @@ -79,7 +79,7 @@ def main() -> None: host=server_config.host, port=server_config.port, path=server_config.streamable_http_path, - stateless_http=os.getenv("STATELESS_HTTP", "true").lower() == "true", + stateless_http=server_config.stateless_http, ) diff --git a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/__init__.py b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/__init__.py similarity index 95% rename from server/mcp_server_hiagent/src/mcp_server_hiagent/versions/__init__.py rename to server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/__init__.py index 412f7ba2..1775bcc2 100644 --- a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/__init__.py +++ b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/__init__.py @@ -47,4 +47,4 @@ def load_version_module(version: str) -> ModuleType: f"unsupported HIAGENT_VERSION {version!r}; " f"supported versions: {', '.join(SUPPORTED_VERSIONS)}" ) - return import_module(f"mcp_server_hiagent.versions.{package}") + return import_module(f"mcp_server_hiagent_knowledge.versions.{package}") diff --git a/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/__init__.py b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/__init__.py new file mode 100644 index 00000000..ee39ee13 --- /dev/null +++ b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/__init__.py @@ -0,0 +1,14 @@ +"""HiAgent OpenAPI compatibility implementation for HiAgent version v3.1.0. + +Each supported HiAgent version is a self-contained sub-package under +``mcp_server_hiagent_knowledge.versions``. If a future HiAgent OpenAPI changes request or +response shapes, add a new ``vX_Y_Z`` package and register it in +``mcp_server_hiagent_knowledge.versions`` without touching this one. +""" + +from __future__ import annotations + +from mcp_server_hiagent_knowledge.versions.v3_1_0.config import load_server_config +from mcp_server_hiagent_knowledge.versions.v3_1_0.server import create_mcp_server + +__all__ = ["create_mcp_server", "load_server_config"] diff --git a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/client.py b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/client.py similarity index 91% rename from server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/client.py rename to server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/client.py index 286e6172..a5ee5aec 100644 --- a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/client.py +++ b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/client.py @@ -8,11 +8,11 @@ from collections.abc import Mapping from dataclasses import dataclass -from mcp_server_hiagent.versions.v3_1_0.config import HiAgentConfig -from mcp_server_hiagent.versions.v3_1_0.signer import sign_openapi_request - - -DEFAULT_OPENAPI_VERSION = "2023-08-01" +from mcp_server_hiagent_knowledge.versions.v3_1_0.config import HiAgentConfig +from mcp_server_hiagent_knowledge.versions.v3_1_0.signer import sign_openapi_request +from mcp_server_hiagent_knowledge.versions.v3_1_0.tools._common import ( + OPENAPI_VERSION as DEFAULT_OPENAPI_VERSION, +) @dataclass(frozen=True) diff --git a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/config.py b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/config.py similarity index 91% rename from server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/config.py rename to server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/config.py index a8e41259..6429c6b7 100644 --- a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/config.py +++ b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/config.py @@ -12,6 +12,7 @@ DEFAULT_HOST = "127.0.0.1" DEFAULT_PORT = 8000 DEFAULT_STREAMABLE_HTTP_PATH = "/mcp" +DEFAULT_STATELESS_HTTP = True @dataclass(frozen=True) @@ -42,6 +43,7 @@ class ServerConfig: host: str = DEFAULT_HOST port: int = DEFAULT_PORT streamable_http_path: str = DEFAULT_STREAMABLE_HTTP_PATH + stateless_http: bool = DEFAULT_STATELESS_HTTP def _clean_top_host(value: str) -> str: @@ -80,4 +82,8 @@ def load_server_config() -> ServerConfig: os.getenv("STREAMABLE_HTTP_PATH", DEFAULT_STREAMABLE_HTTP_PATH).strip() or DEFAULT_STREAMABLE_HTTP_PATH ), + stateless_http=( + os.getenv("STATELESS_HTTP", str(DEFAULT_STATELESS_HTTP)).strip().lower() + == "true" + ), ) diff --git a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/server.py b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/server.py similarity index 50% rename from server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/server.py rename to server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/server.py index d68b96e6..aa6c1adb 100644 --- a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/server.py +++ b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/server.py @@ -4,9 +4,9 @@ from fastmcp import FastMCP -from mcp_server_hiagent.versions.v3_1_0.client import HiAgentOpenAPIClient -from mcp_server_hiagent.versions.v3_1_0.config import load_hiagent_config -from mcp_server_hiagent.versions.v3_1_0.tools import ( +from mcp_server_hiagent_knowledge.versions.v3_1_0.client import HiAgentOpenAPIClient +from mcp_server_hiagent_knowledge.versions.v3_1_0.config import load_hiagent_config +from mcp_server_hiagent_knowledge.versions.v3_1_0.tools import ( OpenAPIClient, register_dataset_tools, register_knowledge_tools, @@ -14,7 +14,7 @@ def create_mcp_server(client: OpenAPIClient | None = None) -> FastMCP: - """Create the HiAgent MCP server. + """Create the HiAgent Knowledge MCP server. Credentials are loaded from environment variables at startup (single identity). Passing ``client`` injects a fixed OpenAPI client (used by @@ -25,23 +25,27 @@ def create_mcp_server(client: OpenAPIClient | None = None) -> FastMCP: openapi_client = client or HiAgentOpenAPIClient(hiagent_config) mcp = FastMCP( - name="hiagent-mcp-server", + name="hiagent-knowledge-mcp-server", instructions=( - "HiAgent MCP Server wraps HiAgent Platform OpenAPI capabilities as " - "MCP tools. It currently provides knowledge-engine tools (dataset " - "listing and knowledge search) and will keep adding more HiAgent " - "OpenAPI capabilities. It supports stdio and streamable-http " - "transports and AK/SK authentication only. Credentials are provided " - "via environment variables (HIAGENT_TOP_HOST, HIAGENT_ACCESS_KEY_ID, " - "HIAGENT_SECRET_ACCESS_KEY)." + "HiAgent Knowledge MCP Server exposes a knowledge base (knowledge " + "engine) as MCP tools. Each tool is named after the user-facing " + "capability it provides. It offers knowledge base discovery " + "(list_datasets, get_dataset) and the full " + "knowledge-engine tool set (search_knowledge, grep_knowledge_chunks, " + "list_document_infos, list_document_chunks, search_wiki, " + "read_wiki_page, read_wiki_source_chunk, read_wiki_source_doc). " + "It supports stdio and " + "streamable-http transports and AK/SK authentication only. " + "Credentials are provided via environment variables " + "(HIAGENT_TOP_HOST, HIAGENT_ACCESS_KEY_ID, HIAGENT_SECRET_ACCESS_KEY)." ), ) @mcp.tool() def health_check() -> dict[str, object]: """ - Check whether the HiAgent MCP server is running and whether required - HiAgent OpenAPI configuration is present. + 检查 HiAgent Knowledge MCP Server 是否运行、以及必需的知识库 OpenAPI 配置是否齐备。 + 仅返回状态与各项是否已配置的布尔值,不回显任何凭证明文。 """ return { diff --git a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/signer.py b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/signer.py similarity index 100% rename from server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/signer.py rename to server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/signer.py diff --git a/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/tools/__init__.py b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/tools/__init__.py new file mode 100644 index 00000000..cf480600 --- /dev/null +++ b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/tools/__init__.py @@ -0,0 +1,12 @@ +"""Business-domain tool registration for HiAgent MCP Server.""" + +from mcp_server_hiagent_knowledge.versions.v3_1_0.tools._common import OpenAPIClient +from mcp_server_hiagent_knowledge.versions.v3_1_0.tools.dataset import register_dataset_tools +from mcp_server_hiagent_knowledge.versions.v3_1_0.tools.knowledge import register_knowledge_tools + + +__all__ = [ + "OpenAPIClient", + "register_dataset_tools", + "register_knowledge_tools", +] diff --git a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/tools/_common.py b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/tools/_common.py similarity index 63% rename from server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/tools/_common.py rename to server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/tools/_common.py index 5fc17e49..66689bc9 100644 --- a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/tools/_common.py +++ b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/tools/_common.py @@ -22,12 +22,3 @@ def call( service: str | None = None, timeout_seconds: float = 30, ) -> dict[str, object]: ... - - -def validate_pagination(page_number: int, page_size: int) -> None: - """Validate the common HiAgent list pagination contract.""" - - if page_number < 1: - raise ValueError("page_number must be at least 1") - if not 1 <= page_size <= 100: - raise ValueError("page_size must be between 1 and 100") diff --git a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/tools/dataset.py b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/tools/dataset.py similarity index 82% rename from server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/tools/dataset.py rename to server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/tools/dataset.py index d3d112d2..66c3bb6a 100644 --- a/server/mcp_server_hiagent/src/mcp_server_hiagent/versions/v3_1_0/tools/dataset.py +++ b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/tools/dataset.py @@ -1,19 +1,18 @@ """HiAgent Dataset (knowledge base) OpenAPI tools. These are dependency tools for the knowledge engine: they let callers discover -the ``DatasetIDs`` (and default retrieval parameters) required by -``call_knowledge_engine_tool``. +the ``DatasetIDs`` (and default retrieval parameters) required by the +knowledge-engine capability tools (``search_knowledge`` / ``list_document_chunks``). """ from __future__ import annotations from fastmcp import FastMCP -from mcp_server_hiagent.versions.v3_1_0.tools._common import ( +from mcp_server_hiagent_knowledge.versions.v3_1_0.tools._common import ( OPENAPI_SERVICE, OPENAPI_VERSION, OpenAPIClient, - validate_pagination, ) @@ -28,14 +27,14 @@ def list_datasets( if not workspace_id: raise ValueError("workspace_id is required") - validate_pagination(page_number, page_size) return client.call( action="ListDatasets", version=OPENAPI_VERSION, service=OPENAPI_SERVICE, body={ "WorkspaceID": workspace_id, - "ListOpt": {"PageNumber": page_number, "PageSize": page_size}, + "PageNumber": page_number, + "PageSize": page_size, }, ) @@ -69,7 +68,7 @@ def list_datasets_tool( page_number: int = 1, page_size: int = 20, ) -> dict[str, object]: - """List HiAgent datasets (knowledge bases) in a workspace.""" + """列出某个 workspace 下的 HiAgent 知识库(dataset),供调用方获取知识引擎所需的 DatasetIDs。""" return list_datasets( client, @@ -83,7 +82,7 @@ def get_dataset_tool( workspace_id: str, dataset_id: str, ) -> dict[str, object]: - """Get one HiAgent dataset, including default retrieval parameters.""" + """获取单个 HiAgent 知识库的详情,含其默认检索参数。""" return get_dataset( client, diff --git a/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/tools/knowledge.py b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/tools/knowledge.py new file mode 100644 index 00000000..d4d4d1d7 --- /dev/null +++ b/server/mcp_server_hiagent_knowledge/src/mcp_server_hiagent_knowledge/versions/v3_1_0/tools/knowledge.py @@ -0,0 +1,689 @@ +"""HiAgent Knowledge Engine OpenAPI tools. + +The HiAgent OpenAPI exposes knowledge-engine capabilities through a single +``CallKnowledgeEngineTool`` action whose ``ToolName`` field selects a sub-tool +and whose request carries a same-named PascalCase parameter object (e.g. +``KnowledgeSearch``, ``GrepChunks``). + +This module maps each ``(ToolName, parameter-object)`` combination to a +**separate MCP tool named after the user-facing capability**, with a flat +argument schema; the handler assembles the request body with field names kept +strictly identical to the OpenAPI contract. + +Capability tool -> ToolName -> parameter object + search_knowledge -> knowledge_search -> KnowledgeSearch + grep_knowledge_chunks -> grep_chunks -> GrepChunks + list_document_infos -> list_doc_infos -> ListDocInfos + list_document_chunks -> list_knowledge_chunks -> ListKnowledgeChunks + search_wiki -> wiki_search -> WikiSearch + read_wiki_page -> wiki_read_page -> WikiReadPage + read_wiki_source_chunk -> wiki_read_source_chunk -> WikiReadSourceChunk + read_wiki_source_doc -> list_knowledge_chunks -> ListKnowledgeChunks + +The two Wiki source read-back capabilities are complementary: +``read_wiki_source_chunk`` resolves a Wiki page's referenced chunks by ``slug`` +(wiki_read_source_chunk), while ``read_wiki_source_doc`` reads one referenced +source document's chunks in order by ``resource_id`` (dispatched through +list_knowledge_chunks). + +Every sub-tool also accepts an optional ``user_info`` (the OpenAPI ``UserInfo`` +end-user identity, ``{"UserID": ..., "UserChannel": ...}``) forwarded verbatim. +""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence + +from fastmcp import FastMCP + +from mcp_server_hiagent_knowledge.versions.v3_1_0.tools._common import ( + OPENAPI_SERVICE, + OPENAPI_VERSION, + OpenAPIClient, +) + + +# The single OpenAPI action every knowledge-engine capability dispatches through. +KNOWLEDGE_ENGINE_ACTION = "CallKnowledgeEngineTool" + +# All knowledge-engine sub-tools, keyed by the OpenAPI ``ToolName`` value. +KNOWN_TOOL_NAMES = ( + "knowledge_search", + "grep_chunks", + "list_doc_infos", + "list_knowledge_chunks", + "wiki_search", + "wiki_read_page", + "wiki_read_source_chunk", +) + + +def _call_knowledge_engine( + client: OpenAPIClient, + *, + workspace_id: str, + dataset_ids: Sequence[str], + tool_name: str, + parameter_field: str | None = None, + parameter_object: dict[str, object] | None = None, + user_info: Mapping[str, object] | None = None, +) -> dict[str, object]: + """Assemble and send a ``CallKnowledgeEngineTool`` request. + + Shared by every capability handler. ``parameter_object`` is placed under the + PascalCase ``parameter_field`` (e.g. ``KnowledgeSearch``); callers pass the + already-built object so field names stay strictly aligned with the OpenAPI + contract. ``user_info`` is forwarded verbatim as the top-level ``UserInfo`` + end-user identity when provided. + + Argument *values* are not validated here: field ranges, enums and + cross-field rules are the backend service's responsibility and are + version-specific, so the server's ``InvalidParameter.*`` errors are passed + through to the caller rather than duplicated locally. Only the presence of + the fields this server must always send (``WorkspaceID`` / ``DatasetIDs`` / + a known ``ToolName``) is asserted. + """ + + if not workspace_id: + raise ValueError("workspace_id is required") + if not dataset_ids: + raise ValueError("dataset_ids must contain at least one dataset id") + if tool_name not in KNOWN_TOOL_NAMES: + raise ValueError( + f"unknown tool_name {tool_name!r}; known tools: {KNOWN_TOOL_NAMES}" + ) + + body: dict[str, object] = { + "WorkspaceID": workspace_id, + "DatasetIDs": list(dataset_ids), + "ToolName": tool_name, + } + if parameter_field is not None and parameter_object is not None: + body[parameter_field] = parameter_object + if user_info: + body["UserInfo"] = dict(user_info) + + return client.call( + action=KNOWLEDGE_ENGINE_ACTION, + version=OPENAPI_VERSION, + service=OPENAPI_SERVICE, + body=body, + ) + + +# --- capability handlers ---------------------------------------------------- + +def search_knowledge( + client: OpenAPIClient, + *, + workspace_id: str, + dataset_ids: Sequence[str], + queries: Sequence[str], + top_k: int | None = None, + score_threshold: float | None = None, + rerank_id: str | None = None, + user_info: Mapping[str, object] | None = None, +) -> dict[str, object]: + """Semantic knowledge retrieval (CallKnowledgeEngineTool/knowledge_search).""" + + if not queries: + raise ValueError("queries must contain at least one query") + + search: dict[str, object] = {"Queries": list(queries)} + if top_k is not None: + search["TopK"] = top_k + if score_threshold is not None: + search["ScoreThreshold"] = score_threshold + if rerank_id: + search["RerankID"] = rerank_id + + return _call_knowledge_engine( + client, + workspace_id=workspace_id, + dataset_ids=dataset_ids, + tool_name="knowledge_search", + parameter_field="KnowledgeSearch", + parameter_object=search, + user_info=user_info, + ) + + +def grep_knowledge_chunks( + client: OpenAPIClient, + *, + workspace_id: str, + dataset_ids: Sequence[str], + pattern: str, + queries: Sequence[str] | None = None, + limit: int | None = None, + grep_type: str | None = None, + resource_ids: Sequence[str] | None = None, + user_info: Mapping[str, object] | None = None, +) -> dict[str, object]: + """Match knowledge chunks by RE2 regex (CallKnowledgeEngineTool/grep_chunks). + + Narrows candidates through retrieval, then applies ``pattern``. Not an + exhaustive full-dataset scan. ``grep_type`` selects the scan scope + (``dataset_ids`` default, or ``resource_ids`` to restrict to specific + documents named in ``resource_ids``); scope rules are enforced by the + OpenAPI layer. + """ + + if not pattern: + raise ValueError("pattern is required") + + grep: dict[str, object] = {"Pattern": pattern} + if queries: + grep["Queries"] = list(queries) + if limit is not None: + grep["Limit"] = limit + if grep_type: + grep["GrepType"] = grep_type + if resource_ids: + grep["ResourceIDs"] = list(resource_ids) + + return _call_knowledge_engine( + client, + workspace_id=workspace_id, + dataset_ids=dataset_ids, + tool_name="grep_chunks", + parameter_field="GrepChunks", + parameter_object=grep, + user_info=user_info, + ) + + +def list_document_infos( + client: OpenAPIClient, + *, + workspace_id: str, + dataset_ids: Sequence[str], + resource_ids: Mapping[str, Sequence[str]], + user_info: Mapping[str, object] | None = None, +) -> dict[str, object]: + """Read metadata for documents, batched by dataset + (CallKnowledgeEngineTool/list_doc_infos). + + ``resource_ids`` maps each dataset id to the resource ids to describe; its + keys must be within ``dataset_ids``. Returns each document's title/type/size/ + status/segment count/timestamps. Metadata only, not document content. + """ + + if not resource_ids: + raise ValueError("resource_ids must contain at least one dataset entry") + normalized: dict[str, list[str]] = {} + for dataset_id, ids in resource_ids.items(): + if not dataset_id: + raise ValueError("resource_ids keys (dataset ids) must be non-empty") + id_list = list(ids) + if not id_list: + raise ValueError( + f"resource_ids[{dataset_id!r}] must contain at least one resource id" + ) + normalized[dataset_id] = id_list + + return _call_knowledge_engine( + client, + workspace_id=workspace_id, + dataset_ids=dataset_ids, + tool_name="list_doc_infos", + parameter_field="ListDocInfos", + parameter_object={"ResourceIDs": normalized}, + user_info=user_info, + ) + + +def list_document_chunks( + client: OpenAPIClient, + *, + workspace_id: str, + dataset_ids: Sequence[str], + resource_id: str, + limit: int | None = None, + cursor_segment_id: str | None = None, + user_info: Mapping[str, object] | None = None, +) -> dict[str, object]: + """List one document's chunks in reading order + (CallKnowledgeEngineTool/list_knowledge_chunks). + + Walks a single ``resource_id`` sequentially, paging with + ``cursor_segment_id``. + """ + + if not resource_id: + raise ValueError("resource_id is required") + + chunks: dict[str, object] = {"ResourceID": resource_id} + if limit is not None: + chunks["Limit"] = limit + if cursor_segment_id: + chunks["CursorSegmentID"] = cursor_segment_id + + return _call_knowledge_engine( + client, + workspace_id=workspace_id, + dataset_ids=dataset_ids, + tool_name="list_knowledge_chunks", + parameter_field="ListKnowledgeChunks", + parameter_object=chunks, + user_info=user_info, + ) + + +def search_wiki( + client: OpenAPIClient, + *, + workspace_id: str, + dataset_ids: Sequence[str], + queries: Sequence[str], + limit: int | None = None, + user_info: Mapping[str, object] | None = None, +) -> dict[str, object]: + """Search generated Wiki pages (CallKnowledgeEngineTool/wiki_search). + + Returns Wiki page candidates (with ``Slug``) for navigation, not final + evidence. Read a page with ``read_wiki_page`` / ``read_wiki_source_chunk``. + """ + + if not queries: + raise ValueError("queries must contain at least one query") + + wiki: dict[str, object] = {"Queries": list(queries)} + if limit is not None: + wiki["Limit"] = limit + + return _call_knowledge_engine( + client, + workspace_id=workspace_id, + dataset_ids=dataset_ids, + tool_name="wiki_search", + parameter_field="WikiSearch", + parameter_object=wiki, + user_info=user_info, + ) + + +def read_wiki_page( + client: OpenAPIClient, + *, + workspace_id: str, + dataset_ids: Sequence[str], + slug: str, + user_info: Mapping[str, object] | None = None, +) -> dict[str, object]: + """Read one generated Wiki page by slug + (CallKnowledgeEngineTool/wiki_read_page). + + Wiki pages are generated navigation material; read the original sources with + ``read_wiki_source_chunk`` before answering with facts. + """ + + if not slug: + raise ValueError("slug is required") + + return _call_knowledge_engine( + client, + workspace_id=workspace_id, + dataset_ids=dataset_ids, + tool_name="wiki_read_page", + parameter_field="WikiReadPage", + parameter_object={"Slug": slug}, + user_info=user_info, + ) + + +def read_wiki_source_chunk( + client: OpenAPIClient, + *, + workspace_id: str, + dataset_ids: Sequence[str], + slug: str, + limit: int | None = None, + overlap: int | None = None, + cursor_segment_id: str | None = None, + user_info: Mapping[str, object] | None = None, +) -> dict[str, object]: + """Read a Wiki page's referenced source chunks, resolved by page slug + (CallKnowledgeEngineTool/wiki_read_source_chunk). + + Reads the chunks a Wiki page references (its ``chunk_refs``) given the page + ``slug`` — the final evidence for facts/numbers/quotes. ``overlap`` expands + each referenced chunk with adjacent segments for more context. Page forward + with ``cursor_segment_id``. To instead read one referenced source document's + chunks in order by its resource id, use ``read_wiki_source_doc``. + """ + + if not slug: + raise ValueError("slug is required") + + src: dict[str, object] = {"Slug": slug} + if limit is not None: + src["Limit"] = limit + if overlap is not None: + src["Overlap"] = overlap + if cursor_segment_id: + src["CursorSegmentID"] = cursor_segment_id + + return _call_knowledge_engine( + client, + workspace_id=workspace_id, + dataset_ids=dataset_ids, + tool_name="wiki_read_source_chunk", + parameter_field="WikiReadSourceChunk", + parameter_object=src, + user_info=user_info, + ) + + +def read_wiki_source_doc( + client: OpenAPIClient, + *, + workspace_id: str, + dataset_ids: Sequence[str], + resource_id: str, + limit: int | None = None, + cursor_segment_id: str | None = None, + user_info: Mapping[str, object] | None = None, +) -> dict[str, object]: + """Read one Wiki source document's chunks in order, by resource id + (dispatched through CallKnowledgeEngineTool/list_knowledge_chunks). + + Companion to ``read_wiki_source_chunk``: where that resolves a page's referenced + chunks by ``slug``, this reads a single referenced source document + (``resource_id``, e.g. taken from a Wiki page's ``SourceRefs``) chunk by + chunk in reading order. It likewise dispatches through ``list_knowledge_chunks`` + and returns ``ListKnowledgeChunks``. Page forward with ``cursor_segment_id``. + + Reading a source document by ``resource_id`` is exactly ``list_document_chunks``; + this thin wrapper reuses that implementation under a Wiki-tracing capability + name so the paging logic lives in one place. + """ + + return list_document_chunks( + client, + workspace_id=workspace_id, + dataset_ids=dataset_ids, + resource_id=resource_id, + limit=limit, + cursor_segment_id=cursor_segment_id, + user_info=user_info, + ) + + +def register_knowledge_tools(mcp: FastMCP, client: OpenAPIClient) -> None: + """Register the knowledge-engine capability tools on a FastMCP server. + + Each capability of the ``CallKnowledgeEngineTool`` action is exposed as a + distinct, capability-named MCP tool rather than one generic tool keyed by a + raw OpenAPI ``ToolName``. + """ + + @mcp.tool(name="search_knowledge") + def search_knowledge_tool( + workspace_id: str, + dataset_ids: list[str], + queries: list[str], + top_k: int | None = None, + score_threshold: float | None = None, + rerank_id: str | None = None, + user_info: dict[str, object] | None = None, + ) -> dict[str, object]: + """语义检索:适合概念、解释、概览、改写类问题("是什么"/"为什么"/"怎么做"/"总结"/"对比"), + 即答案措辞可能与提问不同的场景。返回候选分段用于导航——把它们当作"在哪找", + 而非最终证据;回答事实前建议用 ``list_document_chunks`` 深读原文。 + + 用 ``list_datasets`` 获取知识库 id。常与 ``grep_knowledge_chunks``(精确锚定)配合以扩大召回。 + + 参数: + - workspace_id:知识库所属的 workspace。 + - dataset_ids:要检索的知识库 id 列表,至少 1 个。 + - queries:1~5 个简短、独立的自然语言问题或概念描述;不要传原始对话或长段落。 + - top_k:返回的候选分段数;简单事实问题 5~10,跨文档分析/归因/对比建议 20~30。 + - score_threshold:最低相关性分数(0~1);想提高召回就设低,结果噪音多再调高。 + - rerank_id:可选的重排模型 id。 + - user_info:可选的终端用户身份,{"UserID": ..., "UserChannel": ...}。 + """ + + return search_knowledge( + client, + workspace_id=workspace_id, + dataset_ids=dataset_ids, + queries=queries, + top_k=top_k, + score_threshold=score_threshold, + rerank_id=rerank_id, + user_info=user_info, + ) + + @mcp.tool(name="grep_knowledge_chunks") + def grep_knowledge_chunks_tool( + workspace_id: str, + dataset_ids: list[str], + pattern: str, + queries: list[str] | None = None, + limit: int | None = None, + grep_type: str | None = None, + resource_ids: list[str] | None = None, + user_info: dict[str, object] | None = None, + ) -> dict[str, object]: + """用一条 RE2 正则定位精确文本——适合标识符、固定短语、字段/API 名、版本号、错误码等 + 对措辞敏感的场景。先缩小召回候选再套用正则,不保证穷举整个数据集。返回候选分段(在哪找), + 不是最终证据;回答前建议用 ``list_document_chunks`` 深读原文。 + + 技巧:把近义/别名打包进 ONE 条并列正则 ``alias_a|alias_b|alias_c``,不要多次调用; + 当 ``pattern`` 含正则运算符时,在 ``queries`` 里给不含运算符的可检索词以先召回候选。 + 常在 ``search_knowledge`` 之前用于实体锚定。 + + 参数: + - workspace_id:知识库所属的 workspace。 + - dataset_ids:要检索的知识库 id 列表,至少 1 个。 + - pattern:一条 RE2 正则(用 ``|`` 组合;不支持反向引用/前后瞻)。 + - queries:可选,1~5 个字面检索词,套用正则前先召回候选分段。 + - limit:返回的候选匹配数上限。 + - grep_type:扫描范围,``dataset_ids``(默认)或 ``resource_ids``。 + - resource_ids:限定扫描的文档/资源 id 列表;当 ``grep_type`` 为 ``resource_ids`` 时必填。 + - user_info:可选的终端用户身份,{"UserID": ..., "UserChannel": ...}。 + """ + + return grep_knowledge_chunks( + client, + workspace_id=workspace_id, + dataset_ids=dataset_ids, + pattern=pattern, + queries=queries, + limit=limit, + grep_type=grep_type, + resource_ids=resource_ids, + user_info=user_info, + ) + + @mcp.tool(name="list_document_infos") + def list_document_infos_tool( + workspace_id: str, + dataset_ids: list[str], + resource_ids: dict[str, list[str]], + user_info: dict[str, object] | None = None, + ) -> dict[str, object]: + """批量读取文档元数据(标题、类型、大小、状态、分段数、时间戳)。这是用于识别或对比文档的 + "目录",不能作为正文事实、规则、数字或引文的证据;要看正文请用 ``list_document_chunks`` 深读。 + + 参数: + - workspace_id:知识库所属的 workspace。 + - dataset_ids:涉及的知识库 id 列表,至少 1 个。 + - resource_ids:知识库 id -> 该库下文档/资源 id 列表 的映射(key 必须在 ``dataset_ids`` 内), + 如 {"dataset-1": ["resource-1", "resource-2"]};同一库的 id 合并为一次批量调用。 + - user_info:可选的终端用户身份,{"UserID": ..., "UserChannel": ...}。 + """ + + return list_document_infos( + client, + workspace_id=workspace_id, + dataset_ids=dataset_ids, + resource_ids=resource_ids, + user_info=user_info, + ) + + @mcp.tool(name="list_document_chunks") + def list_document_chunks_tool( + workspace_id: str, + dataset_ids: list[str], + resource_id: str, + limit: int | None = None, + cursor_segment_id: str | None = None, + user_info: dict[str, object] | None = None, + ) -> dict[str, object]: + """按阅读顺序深读单个文档的原始分段——当答案依赖完整上下文而非孤立片段时, + 推荐在 ``search_knowledge`` / ``grep_knowledge_chunks`` 命中相关文档后用它接力深读。 + 搜索工具告诉你"在哪",本工具告诉你"文档到底写了什么"。 + + 参数: + - workspace_id:知识库所属的 workspace。 + - dataset_ids:资源所属的知识库 id 列表,至少 1 个。 + - resource_id:要深读的文档/资源(来自前一步 search/grep 结果)。 + - limit:每页最多读取的有序分段数。 + - cursor_segment_id:续读游标;从头读时不要传。只能复用**同一文档**上一次调用返回的游标, + 不要跨文档、跨工具串用游标。 + - user_info:可选的终端用户身份,{"UserID": ..., "UserChannel": ...}。 + """ + + return list_document_chunks( + client, + workspace_id=workspace_id, + dataset_ids=dataset_ids, + resource_id=resource_id, + limit=limit, + cursor_segment_id=cursor_segment_id, + user_info=user_info, + ) + + @mcp.tool(name="search_wiki") + def search_wiki_tool( + workspace_id: str, + dataset_ids: list[str], + queries: list[str], + limit: int | None = None, + user_info: dict[str, object] | None = None, + ) -> dict[str, object]: + """用 BM25 关键词匹配找到入口 Wiki 页。返回页面候选(含 ``Slug``)仅用于 + 导航——结果是摘要,不是证据,不能只凭搜索结果作答;建议用 + ``read_wiki_page`` 读取选中页面的正文。 + + 参数: + - workspace_id:知识库所属的 workspace。 + - dataset_ids:要检索的知识库 id 列表,至少 1 个。 + - queries:1~5 个简短关键词查询;保留有区分度的实体、产品名、缩写与精确 + 术语;别名或不同表述建议拆成不同查询。 + - limit:返回的 Wiki 页面候选数上限。 + - user_info:可选的终端用户身份,{"UserID": ..., "UserChannel": ...}。 + """ + + return search_wiki( + client, + workspace_id=workspace_id, + dataset_ids=dataset_ids, + queries=queries, + limit=limit, + user_info=user_info, + ) + + @mcp.tool(name="read_wiki_page") + def read_wiki_page_tool( + workspace_id: str, + dataset_ids: list[str], + slug: str, + user_info: dict[str, object] | None = None, + ) -> dict[str, object]: + """读取单个 Wiki 页面的完整内容——阅读 Wiki 材料的主力工具,适用于 + ``search_wiki`` 找到的页面、从其他页面链接过来的页面,或特殊的 + ``index`` 页(建议先读 ``index`` 获取知识库整体概览)。返回结果还会带出 + 相关页面(in/out 链接),可顺着链接跳 1~2 跳补充上下文。 + + Wiki 页面是生成的导航材料,非最终证据;需要精确事实、数字、规则、引文或 + 代码时,建议用 ``read_wiki_source_chunk``(按本页 slug 溯源引用切片)或 + ``read_wiki_source_doc``(按被引用源文档的 resource id 顺序读原文)回溯原始出处。 + + 参数: + - workspace_id:知识库所属的 workspace。 + - dataset_ids:页面所属的知识库 id 列表,至少 1 个。 + - slug:Wiki 页面 slug(来自 ``search_wiki``、相关页面或 ``index``)。 + - user_info:可选的终端用户身份,{"UserID": ..., "UserChannel": ...}。 + """ + + return read_wiki_page( + client, + workspace_id=workspace_id, + dataset_ids=dataset_ids, + slug=slug, + user_info=user_info, + ) + + @mcp.tool(name="read_wiki_source_chunk") + def read_wiki_source_chunk_tool( + workspace_id: str, + dataset_ids: list[str], + slug: str, + limit: int | None = None, + overlap: int | None = None, + cursor_segment_id: str | None = None, + user_info: dict[str, object] | None = None, + ) -> dict[str, object]: + """按页面 ``slug`` 解析、依 ``chunk_refs`` 顺序读取某个 Wiki 页面所引用的 + 源切片——获取该 Wiki 页面所依赖证据(事实、数字、引文、代码)的推荐方式。 + 设 ``overlap`` > 0 可在每个引用切片前后补充邻近上下文。若想按 resource id + 顺序通读整篇被引用源文档,建议改用 ``read_wiki_source_doc``。 + + 参数: + - workspace_id:知识库所属的 workspace。 + - dataset_ids:页面所属的知识库 id 列表,至少 1 个。 + - slug:要读取来源的 Wiki 页面 slug。 + - limit:每页消费的引用锚点数上限(是锚点数,不是最终返回切片数)。 + - overlap:每个锚点前后扩展的邻近分段数(0 表示仅锚点本身)。 + - cursor_segment_id:续页游标;首次读取请省略。仅可复用同一页面上一次调用 + 返回的游标。 + - user_info:可选的终端用户身份,{"UserID": ..., "UserChannel": ...}。 + """ + + return read_wiki_source_chunk( + client, + workspace_id=workspace_id, + dataset_ids=dataset_ids, + slug=slug, + limit=limit, + overlap=overlap, + cursor_segment_id=cursor_segment_id, + user_info=user_info, + ) + + @mcp.tool(name="read_wiki_source_doc") + def read_wiki_source_doc_tool( + workspace_id: str, + dataset_ids: list[str], + resource_id: str, + limit: int | None = None, + cursor_segment_id: str | None = None, + user_info: dict[str, object] | None = None, + ) -> dict[str, object]: + """按 resource id 顺序读取整篇被引用源文档的切片——当你想按原文顺序通读 + (而非只读某页面所引用的切片)时,直接读取源文档的推荐方式,例如取 Wiki + 页面 ``SourceRefs`` 中的某个 ``resource_id``。与 ``read_wiki_source_chunk`` + 互补(后者按页面 slug 解析引用切片)。 + + 参数: + - workspace_id:知识库所属的 workspace。 + - dataset_ids:资源所属的知识库 id 列表,至少 1 个。 + - resource_id:要读取的被引用源文档/资源 id(例如取自 Wiki 页面的 SourceRefs)。 + - limit:每页顺序返回的切片数上限。 + - cursor_segment_id:续页游标;首次读取请省略。仅可复用同一文档上一次调用 + 返回的游标。 + - user_info:可选的终端用户身份,{"UserID": ..., "UserChannel": ...}。 + """ + + return read_wiki_source_doc( + client, + workspace_id=workspace_id, + dataset_ids=dataset_ids, + resource_id=resource_id, + limit=limit, + cursor_segment_id=cursor_segment_id, + user_info=user_info, + ) diff --git a/server/mcp_server_hiagent/tests/test_client.py b/server/mcp_server_hiagent_knowledge/tests/test_client.py similarity index 95% rename from server/mcp_server_hiagent/tests/test_client.py rename to server/mcp_server_hiagent_knowledge/tests/test_client.py index 51c61efe..4d2fd6b1 100644 --- a/server/mcp_server_hiagent/tests/test_client.py +++ b/server/mcp_server_hiagent_knowledge/tests/test_client.py @@ -7,8 +7,8 @@ import pytest -from mcp_server_hiagent.versions.v3_1_0.client import HiAgentOpenAPIClient, OpenAPIError -from mcp_server_hiagent.versions.v3_1_0.config import HiAgentConfig +from mcp_server_hiagent_knowledge.versions.v3_1_0.client import HiAgentOpenAPIClient, OpenAPIError +from mcp_server_hiagent_knowledge.versions.v3_1_0.config import HiAgentConfig class _Response: diff --git a/server/mcp_server_hiagent/tests/test_config.py b/server/mcp_server_hiagent_knowledge/tests/test_config.py similarity index 74% rename from server/mcp_server_hiagent/tests/test_config.py rename to server/mcp_server_hiagent_knowledge/tests/test_config.py index f692a7c4..c883be7a 100644 --- a/server/mcp_server_hiagent/tests/test_config.py +++ b/server/mcp_server_hiagent_knowledge/tests/test_config.py @@ -2,7 +2,7 @@ import pytest -from mcp_server_hiagent.versions.v3_1_0.config import ( +from mcp_server_hiagent_knowledge.versions.v3_1_0.config import ( DEFAULT_ACCOUNT_ID, DEFAULT_REGION, load_hiagent_config, @@ -47,3 +47,20 @@ def test_load_server_config_rejects_invalid_port(monkeypatch: pytest.MonkeyPatch with pytest.raises(ValueError, match="MCP_SERVER_PORT"): load_server_config() + +def test_load_server_config_stateless_http_defaults_true( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("STATELESS_HTTP", raising=False) + + assert load_server_config().stateless_http is True + + +def test_load_server_config_stateless_http_env_override( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("STATELESS_HTTP", "false") + + assert load_server_config().stateless_http is False + + diff --git a/server/mcp_server_hiagent/tests/test_server.py b/server/mcp_server_hiagent_knowledge/tests/test_server.py similarity index 78% rename from server/mcp_server_hiagent/tests/test_server.py rename to server/mcp_server_hiagent_knowledge/tests/test_server.py index 75f651bd..691ac2f8 100644 --- a/server/mcp_server_hiagent/tests/test_server.py +++ b/server/mcp_server_hiagent_knowledge/tests/test_server.py @@ -3,7 +3,7 @@ import asyncio from typing import Any -from mcp_server_hiagent.versions.v3_1_0.server import create_mcp_server +from mcp_server_hiagent_knowledge.versions.v3_1_0.server import create_mcp_server class RecordingClient: @@ -22,9 +22,16 @@ def test_server_registers_knowledge_engine_tools() -> None: assert names == { "health_check", - "call_knowledge_engine_tool", "list_datasets", "get_dataset", + "search_knowledge", + "grep_knowledge_chunks", + "list_document_infos", + "list_document_chunks", + "search_wiki", + "read_wiki_page", + "read_wiki_source_chunk", + "read_wiki_source_doc", } diff --git a/server/mcp_server_hiagent/tests/test_signer.py b/server/mcp_server_hiagent_knowledge/tests/test_signer.py similarity index 95% rename from server/mcp_server_hiagent/tests/test_signer.py rename to server/mcp_server_hiagent_knowledge/tests/test_signer.py index 69c4802d..b36acc4b 100644 --- a/server/mcp_server_hiagent/tests/test_signer.py +++ b/server/mcp_server_hiagent_knowledge/tests/test_signer.py @@ -4,7 +4,7 @@ import pytest -from mcp_server_hiagent.versions.v3_1_0.signer import sign_openapi_request +from mcp_server_hiagent_knowledge.versions.v3_1_0.signer import sign_openapi_request def test_sign_openapi_request_includes_region_in_credential_scope() -> None: diff --git a/server/mcp_server_hiagent/tests/test_versions.py b/server/mcp_server_hiagent_knowledge/tests/test_versions.py similarity index 95% rename from server/mcp_server_hiagent/tests/test_versions.py rename to server/mcp_server_hiagent_knowledge/tests/test_versions.py index b0045667..a12dd73b 100644 --- a/server/mcp_server_hiagent/tests/test_versions.py +++ b/server/mcp_server_hiagent_knowledge/tests/test_versions.py @@ -2,7 +2,7 @@ import pytest -from mcp_server_hiagent.versions import ( +from mcp_server_hiagent_knowledge.versions import ( DEFAULT_VERSION, SUPPORTED_VERSIONS, VERSION_PACKAGES, diff --git a/server/mcp_server_hiagent/tests/tools/test_dataset.py b/server/mcp_server_hiagent_knowledge/tests/tools/test_dataset.py similarity index 68% rename from server/mcp_server_hiagent/tests/tools/test_dataset.py rename to server/mcp_server_hiagent_knowledge/tests/tools/test_dataset.py index 66656a7e..ad8a658a 100644 --- a/server/mcp_server_hiagent/tests/tools/test_dataset.py +++ b/server/mcp_server_hiagent_knowledge/tests/tools/test_dataset.py @@ -4,7 +4,7 @@ import pytest -from mcp_server_hiagent.versions.v3_1_0.tools.dataset import get_dataset, list_datasets +from mcp_server_hiagent_knowledge.versions.v3_1_0.tools.dataset import get_dataset, list_datasets class RecordingClient: @@ -27,7 +27,8 @@ def test_list_datasets_builds_workspace_request() -> None: "service": "app", "body": { "WorkspaceID": "ws-1", - "ListOpt": {"PageNumber": 2, "PageSize": 10}, + "PageNumber": 2, + "PageSize": 10, }, } @@ -37,7 +38,8 @@ def test_list_datasets_defaults() -> None: list_datasets(client, workspace_id="ws-1") - assert client.calls[0]["body"]["ListOpt"] == {"PageNumber": 1, "PageSize": 20} + assert client.calls[0]["body"]["PageNumber"] == 1 + assert client.calls[0]["body"]["PageSize"] == 20 def test_list_datasets_requires_workspace() -> None: @@ -49,14 +51,18 @@ def test_list_datasets_requires_workspace() -> None: "page_number,page_size", [(0, 20), (1, 0), (1, 101)], ) -def test_list_datasets_validates_pagination(page_number: int, page_size: int) -> None: - with pytest.raises(ValueError): - list_datasets( - RecordingClient(), - workspace_id="ws-1", - page_number=page_number, - page_size=page_size, - ) +def test_list_datasets_passes_pagination_through(page_number: int, page_size: int) -> None: + # Pagination bounds are enforced by the OpenAPI layer, not here; whatever + # the caller passes is forwarded verbatim. + client = RecordingClient() + list_datasets( + client, + workspace_id="ws-1", + page_number=page_number, + page_size=page_size, + ) + assert client.calls[0]["body"]["PageNumber"] == page_number + assert client.calls[0]["body"]["PageSize"] == page_size def test_get_dataset_builds_request() -> None: diff --git a/server/mcp_server_hiagent_knowledge/tests/tools/test_knowledge.py b/server/mcp_server_hiagent_knowledge/tests/tools/test_knowledge.py new file mode 100644 index 00000000..201601c3 --- /dev/null +++ b/server/mcp_server_hiagent_knowledge/tests/tools/test_knowledge.py @@ -0,0 +1,434 @@ +from __future__ import annotations + +from typing import Any + +import pytest + +from mcp_server_hiagent_knowledge.versions.v3_1_0.tools.knowledge import ( + KNOWN_TOOL_NAMES, + list_document_infos, + grep_knowledge_chunks, + list_document_chunks, + read_wiki_page, + read_wiki_source_chunk, + read_wiki_source_doc, + search_knowledge, + search_wiki, +) + + +class RecordingClient: + def __init__(self) -> None: + self.calls: list[dict[str, Any]] = [] + + def call(self, **kwargs: Any) -> dict[str, object]: + self.calls.append(kwargs) + return {"ResponseMetadata": {"Action": kwargs["action"]}, "Result": {}} + + +# --- search_knowledge (knowledge_search) ------------------------------------ + + +def test_search_knowledge_builds_oneof_request() -> None: + client = RecordingClient() + search_knowledge( + client, + workspace_id="ws-1", + dataset_ids=["ds-1", "ds-2"], + queries=["hello"], + top_k=3, + score_threshold=0.2, + rerank_id="rk-1", + ) + assert client.calls[0]["body"] == { + "WorkspaceID": "ws-1", + "DatasetIDs": ["ds-1", "ds-2"], + "ToolName": "knowledge_search", + "KnowledgeSearch": { + "Queries": ["hello"], + "TopK": 3, + "ScoreThreshold": 0.2, + "RerankID": "rk-1", + }, + } + + +def test_search_knowledge_omits_optional_fields() -> None: + client = RecordingClient() + search_knowledge(client, workspace_id="ws-1", dataset_ids=["ds-1"], queries=["q"]) + assert client.calls[0]["body"]["KnowledgeSearch"] == {"Queries": ["q"]} + assert "KnowledgeRunMode" not in client.calls[0]["body"] + + +@pytest.mark.parametrize( + "kwargs", + [ + {"workspace_id": "", "dataset_ids": ["ds-1"], "queries": ["q"]}, + {"workspace_id": "ws-1", "dataset_ids": [], "queries": ["q"]}, + {"workspace_id": "ws-1", "dataset_ids": ["ds-1"], "queries": []}, + ], +) +def test_search_knowledge_required_fields(kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError): + search_knowledge(RecordingClient(), **kwargs) + + +def test_search_knowledge_passes_score_threshold_through() -> None: + # Value ranges are validated by the OpenAPI layer, not here: any value the + # caller provides is forwarded verbatim. + client = RecordingClient() + search_knowledge( + client, + workspace_id="ws-1", + dataset_ids=["ds-1"], + queries=["q"], + score_threshold=1.5, + ) + assert client.calls[0]["body"]["KnowledgeSearch"]["ScoreThreshold"] == 1.5 + + +# --- grep_knowledge_chunks (grep_chunks) ------------------------------------ + + +def test_grep_builds_oneof_request() -> None: + client = RecordingClient() + grep_knowledge_chunks( + client, + workspace_id="ws-1", + dataset_ids=["ds-1"], + pattern="err\\d+", + queries=["error"], + limit=5, + ) + assert client.calls[0]["body"] == { + "WorkspaceID": "ws-1", + "DatasetIDs": ["ds-1"], + "ToolName": "grep_chunks", + "GrepChunks": {"Pattern": "err\\d+", "Queries": ["error"], "Limit": 5}, + } + + +def test_grep_passes_scope_fields_through() -> None: + client = RecordingClient() + grep_knowledge_chunks( + client, + workspace_id="ws-1", + dataset_ids=["ds-1"], + pattern="p", + grep_type="resource_ids", + resource_ids=["r-1", "r-2"], + ) + assert client.calls[0]["body"]["GrepChunks"] == { + "Pattern": "p", + "GrepType": "resource_ids", + "ResourceIDs": ["r-1", "r-2"], + } + + +def test_grep_omits_optional_fields() -> None: + client = RecordingClient() + grep_knowledge_chunks( + client, workspace_id="ws-1", dataset_ids=["ds-1"], pattern="p" + ) + assert client.calls[0]["body"]["GrepChunks"] == {"Pattern": "p"} + + +@pytest.mark.parametrize( + "kwargs", + [ + {"workspace_id": "", "dataset_ids": ["ds-1"], "pattern": "p"}, + {"workspace_id": "ws-1", "dataset_ids": [], "pattern": "p"}, + {"workspace_id": "ws-1", "dataset_ids": ["ds-1"], "pattern": ""}, + ], +) +def test_grep_required_fields(kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError): + grep_knowledge_chunks(RecordingClient(), **kwargs) + + +# --- list_document_infos (list_doc_infos) ----------------------------------- + + +def test_list_document_infos_request() -> None: + client = RecordingClient() + list_document_infos( + client, + workspace_id="ws-1", + dataset_ids=["ds-1", "ds-2"], + resource_ids={"ds-1": ["r-1", "r-2"], "ds-2": ["r-3"]}, + ) + assert client.calls[0]["body"] == { + "WorkspaceID": "ws-1", + "DatasetIDs": ["ds-1", "ds-2"], + "ToolName": "list_doc_infos", + "ListDocInfos": {"ResourceIDs": {"ds-1": ["r-1", "r-2"], "ds-2": ["r-3"]}}, + } + + +@pytest.mark.parametrize( + "kwargs", + [ + {"workspace_id": "", "dataset_ids": ["ds-1"], "resource_ids": {"ds-1": ["r"]}}, + {"workspace_id": "ws-1", "dataset_ids": [], "resource_ids": {"ds-1": ["r"]}}, + {"workspace_id": "ws-1", "dataset_ids": ["ds-1"], "resource_ids": {}}, + {"workspace_id": "ws-1", "dataset_ids": ["ds-1"], "resource_ids": {"ds-1": []}}, + {"workspace_id": "ws-1", "dataset_ids": ["ds-1"], "resource_ids": {"": ["r"]}}, + ], +) +def test_list_document_infos_required_fields(kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError): + list_document_infos(RecordingClient(), **kwargs) + + +# --- list_document_chunks (list_knowledge_chunks) --------------------------- + + +def test_list_document_chunks_builds_oneof_request() -> None: + client = RecordingClient() + list_document_chunks( + client, + workspace_id="ws-1", + dataset_ids=["ds-1"], + resource_id="res-1", + limit=50, + cursor_segment_id="seg-9", + ) + assert client.calls[0]["body"] == { + "WorkspaceID": "ws-1", + "DatasetIDs": ["ds-1"], + "ToolName": "list_knowledge_chunks", + "ListKnowledgeChunks": { + "ResourceID": "res-1", + "Limit": 50, + "CursorSegmentID": "seg-9", + }, + } + + +def test_list_document_chunks_omits_optional_fields() -> None: + client = RecordingClient() + list_document_chunks( + client, workspace_id="ws-1", dataset_ids=["ds-1"], resource_id="res-1" + ) + assert client.calls[0]["body"]["ListKnowledgeChunks"] == {"ResourceID": "res-1"} + + +@pytest.mark.parametrize( + "kwargs", + [ + {"workspace_id": "", "dataset_ids": ["ds-1"], "resource_id": "r"}, + {"workspace_id": "ws-1", "dataset_ids": [], "resource_id": "r"}, + {"workspace_id": "ws-1", "dataset_ids": ["ds-1"], "resource_id": ""}, + ], +) +def test_list_document_chunks_required_fields(kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError): + list_document_chunks(RecordingClient(), **kwargs) + + +# --- search_wiki (wiki_search) ---------------------------------------------- + + +def test_search_wiki_builds_oneof_request() -> None: + client = RecordingClient() + search_wiki( + client, workspace_id="ws-1", dataset_ids=["ds-1"], queries=["规定"], limit=5 + ) + assert client.calls[0]["body"] == { + "WorkspaceID": "ws-1", + "DatasetIDs": ["ds-1"], + "ToolName": "wiki_search", + "WikiSearch": {"Queries": ["规定"], "Limit": 5}, + } + + +def test_search_wiki_omits_optional_fields() -> None: + client = RecordingClient() + search_wiki(client, workspace_id="ws-1", dataset_ids=["ds-1"], queries=["q"]) + assert client.calls[0]["body"]["WikiSearch"] == {"Queries": ["q"]} + + +@pytest.mark.parametrize( + "kwargs", + [ + {"workspace_id": "", "dataset_ids": ["ds-1"], "queries": ["q"]}, + {"workspace_id": "ws-1", "dataset_ids": [], "queries": ["q"]}, + {"workspace_id": "ws-1", "dataset_ids": ["ds-1"], "queries": []}, + ], +) +def test_search_wiki_required_fields(kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError): + search_wiki(RecordingClient(), **kwargs) + + +# --- read_wiki_page (wiki_read_page) ---------------------------------------- + + +def test_read_wiki_page_request() -> None: + client = RecordingClient() + read_wiki_page( + client, workspace_id="ws-1", dataset_ids=["ds-1"], slug="concept/foo" + ) + assert client.calls[0]["body"] == { + "WorkspaceID": "ws-1", + "DatasetIDs": ["ds-1"], + "ToolName": "wiki_read_page", + "WikiReadPage": {"Slug": "concept/foo"}, + } + + +@pytest.mark.parametrize( + "kwargs", + [ + {"workspace_id": "", "dataset_ids": ["ds-1"], "slug": "s"}, + {"workspace_id": "ws-1", "dataset_ids": [], "slug": "s"}, + {"workspace_id": "ws-1", "dataset_ids": ["ds-1"], "slug": ""}, + ], +) +def test_read_wiki_page_required_fields(kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError): + read_wiki_page(RecordingClient(), **kwargs) + + +# --- read_wiki_source_chunk (wiki_read_source_chunk) ------------------------------ + + +def test_read_wiki_source_chunk_builds_oneof_request() -> None: + client = RecordingClient() + read_wiki_source_chunk( + client, + workspace_id="ws-1", + dataset_ids=["ds-1"], + slug="concept/foo", + limit=3, + overlap=2, + cursor_segment_id="seg-2", + ) + assert client.calls[0]["body"] == { + "WorkspaceID": "ws-1", + "DatasetIDs": ["ds-1"], + "ToolName": "wiki_read_source_chunk", + "WikiReadSourceChunk": { + "Slug": "concept/foo", + "Limit": 3, + "Overlap": 2, + "CursorSegmentID": "seg-2", + }, + } + + +def test_read_wiki_source_chunk_omits_optional_fields() -> None: + client = RecordingClient() + read_wiki_source_chunk( + client, workspace_id="ws-1", dataset_ids=["ds-1"], slug="s" + ) + assert client.calls[0]["body"]["WikiReadSourceChunk"] == {"Slug": "s"} + + +def test_read_wiki_source_chunk_allows_zero_overlap() -> None: + client = RecordingClient() + read_wiki_source_chunk( + client, workspace_id="ws-1", dataset_ids=["ds-1"], slug="s", overlap=0 + ) + assert client.calls[0]["body"]["WikiReadSourceChunk"] == {"Slug": "s", "Overlap": 0} + + +@pytest.mark.parametrize( + "kwargs", + [ + {"workspace_id": "", "dataset_ids": ["ds-1"], "slug": "s"}, + {"workspace_id": "ws-1", "dataset_ids": [], "slug": "s"}, + {"workspace_id": "ws-1", "dataset_ids": ["ds-1"], "slug": ""}, + ], +) +def test_read_wiki_source_chunk_required_fields(kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError): + read_wiki_source_chunk(RecordingClient(), **kwargs) + + +# --- read_wiki_source_doc (dispatched via list_knowledge_chunks) ------------ + + +def test_read_wiki_source_doc_builds_request() -> None: + client = RecordingClient() + read_wiki_source_doc( + client, + workspace_id="ws-1", + dataset_ids=["ds-1"], + resource_id="res-1", + limit=5, + cursor_segment_id="seg-2", + ) + assert client.calls[0]["body"] == { + "WorkspaceID": "ws-1", + "DatasetIDs": ["ds-1"], + "ToolName": "list_knowledge_chunks", + "ListKnowledgeChunks": { + "ResourceID": "res-1", + "Limit": 5, + "CursorSegmentID": "seg-2", + }, + } + + +def test_read_wiki_source_doc_omits_optional_fields() -> None: + client = RecordingClient() + read_wiki_source_doc( + client, workspace_id="ws-1", dataset_ids=["ds-1"], resource_id="res-1" + ) + assert client.calls[0]["body"]["ListKnowledgeChunks"] == {"ResourceID": "res-1"} + + +@pytest.mark.parametrize( + "kwargs", + [ + {"workspace_id": "", "dataset_ids": ["ds-1"], "resource_id": "r"}, + {"workspace_id": "ws-1", "dataset_ids": [], "resource_id": "r"}, + {"workspace_id": "ws-1", "dataset_ids": ["ds-1"], "resource_id": ""}, + ], +) +def test_read_wiki_source_doc_required_fields(kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError): + read_wiki_source_doc(RecordingClient(), **kwargs) + + +# --- contract ----------------------------------------------------------------- + + +def test_known_tool_names_cover_the_seven_sub_tools() -> None: + assert set(KNOWN_TOOL_NAMES) == { + "knowledge_search", + "grep_chunks", + "list_doc_infos", + "list_knowledge_chunks", + "wiki_search", + "wiki_read_page", + "wiki_read_source_chunk", + } + + +def test_user_info_passed_through() -> None: + client = RecordingClient() + search_knowledge( + client, + workspace_id="ws-1", + dataset_ids=["ds-1"], + queries=["q"], + user_info={"UserID": "u-1", "UserChannel": "web"}, + ) + assert client.calls[0]["body"]["UserInfo"] == {"UserID": "u-1", "UserChannel": "web"} + + +def test_user_info_omitted_when_absent() -> None: + client = RecordingClient() + read_wiki_page(client, workspace_id="ws-1", dataset_ids=["ds-1"], slug="s") + assert "UserInfo" not in client.calls[0]["body"] + + +def test_all_calls_use_action_version_service() -> None: + client = RecordingClient() + search_knowledge(client, workspace_id="ws-1", dataset_ids=["ds-1"], queries=["q"]) + call = client.calls[0] + assert call["action"] == "CallKnowledgeEngineTool" + assert call["version"] == "2023-08-01" + assert call["service"] == "app" diff --git a/server/mcp_server_hiagent/uv.lock b/server/mcp_server_hiagent_knowledge/uv.lock similarity index 99% rename from server/mcp_server_hiagent/uv.lock rename to server/mcp_server_hiagent_knowledge/uv.lock index d593afe1..bfd7d298 100644 --- a/server/mcp_server_hiagent/uv.lock +++ b/server/mcp_server_hiagent_knowledge/uv.lock @@ -680,7 +680,7 @@ wheels = [ ] [[package]] -name = "mcp-server-hiagent" +name = "mcp-server-hiagent-knowledge" version = "0.1.0" source = { editable = "." } dependencies = [