Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 61 additions & 6 deletions server/mcp_server_askecho_search_infinity/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# 联网搜索API MCP Server
## 版本信息
v0.1.0
v0.2.0
## 产品描述
火山引擎联网搜索API,提供网页与图片搜索能力,帮助大模型获取更准确、更新鲜的外部信息。
火山引擎联网搜索API,提供豆包搜索 Custom 版网页与基础图片搜索能力,帮助大模型获取更准确、更新鲜的外部信息。
## 分类
火山引擎云原生
## 标签
Expand All @@ -16,7 +16,7 @@ v0.1.0
#### 类型
saas
#### 详细描述
根据用户输入问题,返回联网搜索结果,支持网页和图片搜索
根据用户输入问题,返回联网搜索结果,支持网页与基础图片搜索。
#### 调试所需的输入参数:
输入:
```json
Expand All @@ -32,7 +32,7 @@ saas
"type": "string"
},
"Count": {
"description": "返回条数;web 最多 50 条,image 最多 5 条,不传默认 10 条",
"description": "返回条数;web 默认 10 条、最多 50 条,image 默认且最多 5 条",
"type": "number"
},
"SearchType": {
Expand All @@ -46,6 +46,34 @@ saas
"AuthLevel": {
"description": "权威等级过滤,0 为默认,1 为非常权威",
"type": "number"
},
"NeedContent": {
"description": "是否返回网页正文,仅支持 web",
"type": "boolean"
},
"NeedUrl": {
"description": "是否返回网页 URL,仅支持 web",
"type": "boolean"
},
"Sites": {
"description": "仅搜索指定站点,以 | 分隔,最多 20 个,仅支持 web",
"type": "string"
},
"BlockHosts": {
"description": "排除指定站点,以 | 分隔,最多 5 个,仅支持 web",
"type": "string"
},
"Industry": {
"description": "行业搜索,可选 finance、game 或 gov,仅支持 web",
"type": "string"
},
"QueryRewrite": {
"description": "是否启用查询改写",
"type": "boolean"
},
"ContentFormats": {
"description": "网页正文格式,可选 text 或 markdown,仅支持 web",
"type": "string"
}
}
},
Expand All @@ -61,17 +89,17 @@ saas
#### 最容易被唤起的 Prompt示例
联网搜索北京周边游攻略
## 可适配平台
Trae,Cursor,Python
Claude Code, Codex, Trae,Cursor等支持MCP协议的AI工具
## 服务开通链接 (整体产品)
登录火山控制台,开通【联网搜索API】,服务开通链接:https://console.volcengine.com/search-infinity/web-search

API Key 创建链接:https://console.volcengine.com/search-infinity/api-key
## 鉴权方式
- API Key鉴权
- 火山引擎的AKSK鉴权体系
## 安装部署
### 前置准备
- Python 3.12 / 3.13
- 当前不支持 Python 3.14 beta,`mcp` / `pydantic` 依赖链在该版本上仍存在兼容性问题
- UV
**Linux/macOS:**
```bash
Expand Down Expand Up @@ -99,6 +127,33 @@ uv run mcp-server-askecho-search-infinity -t streamable-http
## 部署
### UVX
鉴权信息,火山引擎 AK/SK 与 `ASK_ECHO_SEARCH_INFINITY_API_KEY` 二选一即可

#### 推荐:从 PyPI 或已配置的火山镜像源安装

该方式不依赖 GitHub,`>=0.2.0` 会自动使用可获取的最新发布版本。

```json
{
"mcpServers": {
"mcp-server-askecho-search-infinity": {
"command": "uvx",
"args": [
"--from",
"mcp-server-askecho-search-infinity>=0.2.0",
"mcp-server-askecho-search-infinity"
],
"env": {
"VOLCENGINE_ACCESS_KEY": "",
"VOLCENGINE_SECRET_KEY": "",
"ASK_ECHO_SEARCH_INFINITY_API_KEY": ""
}
}
}
}
```

#### 备用:从 GitHub 源码安装

```json
{
"mcpServers": {
Expand Down
6 changes: 3 additions & 3 deletions server/mcp_server_askecho_search_infinity/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
[project]
name = "mcp-server-askecho-search-infinity"
version = "0.1.0"
version = "0.2.0"
description = "Web Search API MCP Server"
readme = "README.md"
requires-python = ">=3.12,<3.14"
dependencies = [
"mcp>=1.9.4",
"mcp>=1.29.0,<2.0.0",
"aiohttp>=3.9.0",
]

Expand All @@ -14,4 +14,4 @@ mcp-server-askecho-search-infinity = "mcp_server_askecho_search_infinity:main"

[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
build-backend = "setuptools.build_meta"
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

Host = "open.feedcoopapi.com"
ContentType = "application/json"
REQUEST_TIMEOUT_SECONDS = 30


async def web_search_api_key_auth(api_key: str, req: WebSearchRequest, tool_name: str):
Expand All @@ -17,12 +18,10 @@ async def web_search_api_key_auth(api_key: str, req: WebSearchRequest, tool_name
async with session.post(
url=f"https://{Host}/search_api/web_search",
headers=header,
timeout=aiohttp.ClientTimeout(total=3000),
timeout=aiohttp.ClientTimeout(total=REQUEST_TIMEOUT_SECONDS),
data=json.dumps(req.to_payload())
) as response:
# 在上下文内读取所有数据,避免连接关闭问题
response.raise_for_status() # 手动调用
data = await response.json()
return data
return None
return None
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
Region = "cn-beijing"
Host = "mercury.volcengineapi.com"
ContentType = "application/json"
REQUEST_TIMEOUT_SECONDS = 30


async def web_search_volcengine_auth(ak: str, sk: str, req: WebSearchRequest, tool_name: str):
Expand Down Expand Up @@ -122,13 +123,11 @@ async def volcengine_auth_request(method, date, query, header, ak, sk, action, b
method=method,
url=f"https://{request_param['host']}{request_param['path']}",
headers=header,
timeout=aiohttp.ClientTimeout(total=600),
timeout=aiohttp.ClientTimeout(total=REQUEST_TIMEOUT_SECONDS),
params=request_param["query"],
data=request_param["body"]
) as response:
# 在上下文内读取所有数据,避免连接关闭问题
response.raise_for_status() # 手动调用
data = await response.json()
return data
return None
return None
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,10 @@
TIME_RANGE_SHORTCUTS = {"OneDay", "OneWeek", "OneMonth", "OneYear"}
DATE_RANGE_PATTERN = re.compile(r"^(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})$")
SUPPORTED_SEARCH_TYPES = {"web", "image"}


@dataclass
class Error:
message: str
type: str
code: str

def to_dict(self):
return {
"message": self.message,
"type": self.type,
"code": self.code
}


@dataclass
class ResponseError:
error: Error

def to_dict(self):
return {
"error": self.error.to_dict(),
}
SUPPORTED_INDUSTRIES = {"finance", "game", "gov"}
SUPPORTED_CONTENT_FORMATS = {"text", "markdown"}
MAX_SITE_COUNT = 20
MAX_BLOCK_HOST_COUNT = 5


@dataclass
Expand All @@ -38,21 +18,24 @@ class WebSearchRequest:
SearchType: str = "web"
Count: int = 10
Filter: Optional[dict] = None
NeedSummary: Optional[bool] = None
TimeRange: Optional[str] = None
QueryControl: Optional[dict] = None
ContentFormats: Optional[str] = None

def to_payload(self):
payload = {
"Query": self.Query,
"SearchType": self.SearchType,
"Count": self.Count,
}
if self.SearchType == "web":
payload["NeedSummary"] = True
if self.Filter:
payload["Filter"] = self.Filter
if self.TimeRange:
payload["TimeRange"] = self.TimeRange
if self.Filter:
payload["Filter"] = self.Filter
if self.TimeRange:
payload["TimeRange"] = self.TimeRange
if self.QueryControl:
payload["QueryControl"] = self.QueryControl
if self.ContentFormats:
payload["ContentFormats"] = self.ContentFormats
return payload


Expand Down Expand Up @@ -101,39 +84,109 @@ def validate_time_range(time_range: Optional[str]) -> Optional[str]:
return time_range


def validate_host_list(value: Optional[str], field_name: str, max_count: int) -> Optional[str]:
if value is None:
return None

normalized_value = value.strip()
if not normalized_value:
return None

items = [item.strip() for item in normalized_value.split("|")]
if not items or any(not item for item in items):
raise ValueError(f"{field_name} 需为以 | 分隔的非空域名列表。")
if len(items) > max_count:
raise ValueError(f"{field_name} 最多支持 {max_count} 个域名。")
return "|".join(items)


def build_web_search_request(
query: str,
count: int = 10,
count: Optional[int] = None,
search_type: str = "web",
time_range: Optional[str] = None,
auth_level: int = 0,
need_content: Optional[bool] = None,
need_url: Optional[bool] = None,
sites: Optional[str] = None,
block_hosts: Optional[str] = None,
industry: Optional[str] = None,
query_rewrite: Optional[bool] = None,
content_formats: Optional[str] = None,
) -> WebSearchRequest:
normalized_query = (query or "").strip()
if not normalized_query:
raise ValueError("Query 不能为空。")
if len(normalized_query) > 100:
raise ValueError("Query 长度需为 1~100 个字符。")

if search_type not in SUPPORTED_SEARCH_TYPES:
normalized_search_type = (search_type or "").strip().lower()
if normalized_search_type not in SUPPORTED_SEARCH_TYPES:
raise ValueError("SearchType 仅支持 web 或 image。")

if count is None:
count = 10 if normalized_search_type == "web" else 5
if count < 1:
raise ValueError("Count 需大于等于 1。")
max_count = 50 if search_type == "web" else 5
max_count = 50 if normalized_search_type == "web" else 5
if count > max_count:
raise ValueError(f"{search_type} 类型最多返回 {max_count} 条。")
raise ValueError(f"{normalized_search_type} 类型最多返回 {max_count} 条。")

if auth_level not in {0, 1}:
raise ValueError("AuthLevel 仅支持 0 或 1。")

normalized_time_range = validate_time_range(time_range) if search_type == "web" else None
filters = {"AuthInfoLevel": auth_level} if search_type == "web" and auth_level > 0 else None
normalized_industry = industry.strip().lower() if industry else None
if normalized_industry and normalized_industry not in SUPPORTED_INDUSTRIES:
raise ValueError("Industry 仅支持 finance、game 或 gov。")

normalized_content_formats = content_formats.strip().lower() if content_formats else None
if normalized_content_formats and normalized_content_formats not in SUPPORTED_CONTENT_FORMATS:
raise ValueError("ContentFormats 仅支持 text 或 markdown。")

if normalized_search_type == "image":
web_only_args = {
"TimeRange": time_range,
"AuthLevel": auth_level if auth_level else None,
"NeedContent": need_content,
"NeedUrl": need_url,
"Sites": sites,
"BlockHosts": block_hosts,
"Industry": normalized_industry,
"ContentFormats": normalized_content_formats,
}
unsupported_fields = [name for name, value in web_only_args.items() if value is not None]
if unsupported_fields:
raise ValueError(f"{', '.join(unsupported_fields)} 仅支持 web 搜索。")
return WebSearchRequest(
Query=normalized_query,
SearchType=normalized_search_type,
Count=count,
QueryControl={"QueryRewrite": query_rewrite} if query_rewrite is not None else None,
)

filters = {}
if auth_level > 0:
filters["AuthInfoLevel"] = auth_level
if need_content is not None:
filters["NeedContent"] = need_content
if need_url is not None:
filters["NeedUrl"] = need_url

normalized_sites = validate_host_list(sites, "Sites", MAX_SITE_COUNT)
if normalized_sites:
filters["Sites"] = normalized_sites
normalized_block_hosts = validate_host_list(block_hosts, "BlockHosts", MAX_BLOCK_HOST_COUNT)
if normalized_block_hosts:
filters["BlockHosts"] = normalized_block_hosts
if normalized_industry:
filters["Industry"] = normalized_industry

return WebSearchRequest(
Query=normalized_query,
SearchType=search_type,
SearchType=normalized_search_type,
Count=count,
Filter=filters,
NeedSummary=True if search_type == "web" else None,
TimeRange=normalized_time_range,
Filter=filters or None,
TimeRange=validate_time_range(time_range),
QueryControl={"QueryRewrite": query_rewrite} if query_rewrite is not None else None,
ContentFormats=normalized_content_formats,
)
Loading
Loading