关于 Staan
Staan-MCP 将 AI 客户端连接到 STAAN 搜索 API;STAAN 是拥有自有索引、侧重欧洲市场的搜索服务。它提供可选网页全文的搜索结果,以及带引用的依据型回答工具。
主要功能
- 01搜索多个 Staan 市场
- 02以 Markdown 或 HTML 返回网页全文
- 03限制或排除指定域名
- 04生成带引用的依据型回答
- 05支持 stdio 和 streamable HTTP
适用场景
- 01搜索多个欧洲市场
- 02在搜索结果中获取网页文本
- 03生成附带来源引用的回答
MCP 相关
该 MCP 服务提供 `web_search` 和 `answer` 工具,支持 stdio 与 streamable HTTP 传输。安装版可为 HTTP 添加 OAuth 2.1 访问控制。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
2收录的工具列表 · 更新于 2 周前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| query | string | 是 | 自然语言搜索查询。 |
| market | any | 否 | 用于搜索的语言-国家/地区市场:fr-fr、de-de、en-us、en-gb、en-fr、en-ca、en-au、en-in、en-ie、en-nz、en-za、en-sg。可传入一个值、列表或通配符('en-*' 表示所有英语市场,'*' 表示全部)。多个市场的结果会按排名合并并按 URL 去重;每个市场都会单独计费,因此除非需要更广泛的覆盖范围,否则建议只传入一个市场。 |
| offset | enum | 否 | 首个结果的偏移量,用于分页:每页最多 10 条结果,每个查询最多 40 条。0 | 10 | 20 | 30 |
| full_content | enum | 否 | 随每条结果返回的页面文本。默认值 'none' 会省略文本;'markdown' 会添加文本并使响应变得非常大;'html' 会更大。none | markdown | html |
| exclude_domains | any | 否 | 排除这些网站的结果,例如 ['pinterest.com'];也排除其子域名。最多 10 个。不能与 include_domains 同时使用。 |
| include_domains | any | 否 | 仅返回这些网站的结果,例如 ['wikipedia.org'];匹配其子域名。最多 10 个。不能与 exclude_domains 同时使用。 |
| simple_response | boolean | 否 | false 会返回原始 Staan 响应,按市场各返回一条,而不是扁平列表。 |
详细介绍
Staan-MCP
这是一个面向欧洲搜索引擎 STAAN(Search Trusted API Access Network)的 MCP 服务,由 European Perspective SAS 构建,该公司由 Qwant 和 Ecosia 搜索引擎共同持有。它是 Exa Search 和其他 SERP 服务的开放欧洲替代方案,拥有自己的索引,主要面向欧洲市场(以及部分英联邦地区),提供其他搜索服务商可能缺少的独特视角。免费订阅每月可发起 1000 次请求。
MCP 提供两个工具:
web_search:网页搜索,可选获取完整页面内容(Markdown 或 HTML),并可同时搜索一个或多个 Staan 市场。answer:基于资料并带有引文的回答,以带有 GitHub 风格脚注的 Markdown 格式呈现。
它通过标准输入/输出(默认方式)、可流式传输的 HTTP 或无状态可流式传输的 HTTP 运行,并可选择使用 OAuth 2.1 资源服务端保护 HTTP 传输。如果只需要标准输入/输出,也可以无需安装任何内容即可运行:只需下载仓库 ZIP 文件,并让你的 AI Agent 指向此文件(README.md)。
注意:
staan-mcp与 Staan、European Perspective、Qwant 或 Ecosia 均无关联。这只是一个个人项目,旨在提供高质量的 Python MCP;在发布时,此类项目尚不存在。
重要:
answerAPI 端点仅限受邀用户使用,即使你已获邀,该端点也可能随时临时停用,导致返回{"message":"Forbidden","statusCode":403}响应。
快速开始
你需要一个 Staan API 密钥(在 Staan 控制台中创建,免费套餐每月包含 1,000 次请求)。如果已安装 uv,只需配置你的客户端即可——无需从此仓库下载任何内容,uv 会通过 uvx 命令自动为你下载最新版 MCP。
{
"mcpServers": {
"staan": {
"command": "uvx",
"args": ["staan-mcp"],
"env": { "STAAN_API_KEY": "your-staan-api-key" }
}
}
}
这份 JSON 配置可在 Claude Desktop 和 Cursor 中直接使用。Claude Code、VS Code 和 Hermes Agent 使用不同的格式:请参阅客户端配置。
如果你是大模型,或者只是需要更多详情,请继续阅读。
详细设置
三种运行方式
- 安装包
uvx staan-mcp,或 pip install staan-mcp——会安装多个依赖项:MCP SDK、httpx2、pydantic、anyio(OAuth 网关还需要 pyjwt)。作为交换,它不仅提供 stdio,还提供 Streamable HTTP 和无状态 Streamable HTTP 连接,两种 HTTP 连接均带有 OAuth 网关。
- 下载并运行
通过 GitHub 上的 Code->Download ZIP 下载,或通过 git clone 获取。然后,你可以将 MCP 配置为通过 stdio_server.py 运行(参见安装指南中的无需安装任何内容部分)
- 通过发布版本
下载最新的发布版本
| 1. 安装包 | 2. 下载并运行(检出代码) | 3. 单文件打包 | |
|---|---|---|---|
| 方式 | uvx staan-mcp,或 pip install staan-mcp | git clone(或下载源代码树的 zip),然后运行 python src/staan_mcp/stdio_server.py | 从最新发布版本下载 staan-mcp-stdio.pyz,然后运行 python staan-mcp-stdio.pyz |
| Python | 3.11 或更新版本 | 3.11 或更新版本,纯解释器,无需安装任何内容 | 3.11 或更新版本,纯解释器,无需安装任何内容 |
| 依赖项 | MCP SDK、httpx2、pydantic、anyio(OAuth 网关还需要 pyjwt) | 无(仅使用标准库) | 无(仅使用标准库) |
| 传输方式 | stdio、Streamable HTTP、无状态 Streamable HTTP | stdio | stdio |
| HTTP OAuth 网关 | 有 | 无 | 无 |
| 工具、参数、结果、错误消息 | 参考实现 | 完全相同(见下文) | 完全相同(使用相同代码) |
只有安装包提供 HTTP 服务和 OAuth 网关。方式 2 和方式 3 运行的是同一个
stdio_server.py;打包文件包含该文件及其导入的少量标准库模块,压缩成一个文件,可放在任意位置。
客户端配置
所有示例都使用 uvx 启动服务,并通过 STAAN_API_KEY
环境变量传递密钥。请将 your-staan-api-key 替换为你的密钥。
Claude Desktop
编辑 claude_desktop_config.json(Settings、Developer、Edit Config):
{
"mcpServers": {
"staan": {
"command": "uvx",
"args": ["staan-mcp"],
"env": { "STAAN_API_KEY": "your-staan-api-key" }
}
}
}
Claude Code
claude mcp add staan --env STAAN_API_KEY=your-staan-api-key -- uvx staan-mcp
添加 --scope user,使其在每个项目中都可用;或添加 --scope project,写入可共享的 .mcp.json(不要将密钥放入要提交的文件中;请在其中使用环境变量展开,例如 "STAAN_API_KEY": "${STAAN_API_KEY}")。
VS Code
创建 .vscode/mcp.json。VS Code 会提示你输入一次密钥,并安全地存储它:
{
"inputs": [
{
"type": "promptString",
"id": "staan-api-key",
"description": "Staan API key",
"password": true
}
],
"servers": {
"staan": {
"type": "stdio",
"command": "uvx",
"args": ["staan-mcp"],
"env": { "STAAN_API_KEY": "${input:staan-api-key}" }
}
}
}
Cursor
在项目中创建 .cursor/mcp.json,或创建 ~/.cursor/mcp.json 以供所有项目使用。Cursor 会展开 ${env:NAME},因此密钥可以保留在 shell 环境中:
{
"mcpServers": {
"staan": {
"command": "uvx",
"args": ["staan-mcp"],
"env": { "STAAN_API_KEY": "${env:STAAN_API_KEY}" }
}
}
}
该代码仓库也提供 Cursor 插件(.cursor-plugin/plugin.json 和 mcp.json)。安装为插件后,Cursor 会要求你将 Staan API 密钥作为插件变量提供,而不是从 shell 中读取。
Hermes Agent
Hermes 从 ~/.hermes/config.yaml 中的 mcp_servers 读取 MCP 服务:
mcp_servers:
staan:
command: "uvx"
args: ["staan-mcp"]
env:
STAAN_API_KEY: "your-staan-api-key"
Hermes 还提供原生插件,无需启动第二个进程,即可提供同样的两个工具 staan_web_search 和
staan_answer:参见 src/hermes。该插件独立于服务,不属于 staan-mcp 包。
其他客户端
任何能够启动 stdio 命令的客户端都可以使用:在环境中设置 STAAN_API_KEY,然后运行 uvx staan-mcp。有关 HTTP 客户端,请参见传输方式。
无需安装任何内容
方式 2 和方式 3 无需运行 pip install、无需虚拟环境,也无需第三方软件包;只需普通的 Python 3.11 或更新版本,且仅支持 stdio(HTTP 和 OAuth 门控需要已安装的软件包)。
从代码检出目录运行(方式 2)。 下载仓库(使用 git clone 或下载源码树的 zip),将 src/staan_mcp/ 保持在一起,然后让客户端指向一个文件:
src/staan_mcp/stdio_server.py:
{
"mcpServers": {
"staan": {
"command": "python",
"args": ["/path/to/Staan-MCP/src/staan_mcp/stdio_server.py"],
"env": { "STAAN_API_KEY": "your-staan-api-key" }
}
}
}
该脚本会将自己的 src 目录添加到 sys.path,并复用软件包中的标准库模块(市场、领域、结果格式化、错误、配置和工具文本),因此不能单独复制这个文件。若将其复制到仓库之外,它会打印一行说明,然后以状态码 2 退出,不会显示回溯;请改用打包文件。
使用哪个 Python。 两种方式都需要 Python 3.11 或更新版本。在 Windows 上,python 可能不存在,也可能是 Microsoft Store 的占位程序;请使用启动器,并设置 "command": "py", "args": ["-3", "C:\\path\\to\\staan-mcp-stdio.pyz"](JSON 中的反斜杠需要重复),或使用解释器的完整路径。在 macOS 和 Linux 上,命令通常是 python3。如果 Python 版本过旧,文件会立即停止,并显示 staan-mcp needs Python 3.11 or newer; this is X.Y.Z (path),然后以状态码 2 退出。使用 python --version 检查版本。
它是什么。 它不会导入 MCP SDK、httpx2、pydantic、anyio 或 pyjwt;测试会使用 python -I -S 运行它,并阻止所有非标准库导入,以验证这一点;打包文件也会以相同方式运行。它仅通过 stdio 提供与已安装软件包相同的 web_search 和 answer 工具、环境变量、结果和错误消息(测试会使用相同输入对两者进行比较)。它使用自己的简易 urllib 传输,而不是 httpx2,并采用相同的重试策略。它与软件包的其余部分一样,会通过 lint、类型、复杂度和覆盖率检查;只有变异测试会跳过它。
单文件打包文件(方式 3)。 从最新版本下载 staan-mcp-stdio.pyz,将其放在任意位置,然后让客户端指向该文件:
{
"mcpServers": {
"staan": {
"command": "python",
"args": ["/path/to/staan-mcp-stdio.pyz"],
"env": { "STAAN_API_KEY": "your-staan-api-key" }
}
}
}
该文件是一个 Python zip 应用:运行 python staan-mcp-stdio.pyz --version 会打印 staan-stdio X.Y.Z。它仅包含 stdio_server.py 导入的软件包标准库模块,不含其他内容。每个版本都会将它与 wheel 和 sdist 一起发布,并附有已签名的构建来源证明(gh attestation verify staan-mcp-stdio.pyz --repo MarcinWolnyEU/Staan-MCP)。此构建可复现:在检出对应标签的仓库中运行 python scripts/build_zipapp.py,会生成字节完全相同的文件并打印其 SHA-256。
工具参考
这两个工具均为只读且面向开放世界(它们会访问外部服务,不会更改任何内容)。失败时会以 MCP 工具错误的形式报告简短且安全的消息,绝不会包含你的密钥。
web_search
通过 POST /v2/search/web 搜索网页。每次调用会返回一页结果,每个市场最多 10 条。
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
query | 字符串,1 到 400 个字符 | 必填 | 搜索查询。支持 site: 和 -site: 运算符。 |
market | 字符串或字符串列表 | "en-gb" | 一个或多个 Staan 市场。支持通配符,见下文。 |
full_content | "none"、"markdown" 或 "html" | "none" | 以指定格式返回每个结果页面的文本。默认值 "none" 会省略此内容,从而减小响应(也减少令牌用量)。费用和延迟几乎不变,见下文说明。 |
offset | 0、10、20 或 30 | 0 | 分页时的结果偏移量。每一页都是单独计费的请求。 |
include_domains | 字符串列表,最多 10 个 | 无 | 将结果限制为这些域名。域名也涵盖其子域名。 |
exclude_domains | 字符串列表,最多 10 个 | 无 | 排除这些域名的结果,包括其子域名。不能与 include_domains 同时使用。 |
simple_response | 布尔值 | true | true 返回扁平、精简的列表。false 返回原始上游响应。 |
说明:
count不是参数。Staan 只接受 10 条的页面大小,且服务器始终会发送该值。使用offset翻页。- 域名会自动规范化:
https://www.Example.com:443/a?b会变为example.com,而en.wikipedia.org会保持为en.wikipedia.org。 - 服务器始终会请求额外摘要。如果你的密钥所属套餐包含这些摘要,它们会合并到每条结果的
snippet中。如果不包含,上游响应中就不会有此字段,也不会造成问题。 - 列出的域名也涵盖其子域名。于 2026-10-05 实时验证:
include_domains: ["python.org"]返回了discuss.python.org、docs.python.org、wiki.python.org和www.python.org;exclude_domains: ["python.org"]则移除了所有这些域名。你无需逐个列出子域名。 full_content会改变响应大小,不会改变费用。 服务器始终会向 Staan 请求额外摘要,Staan 正是据此抓取并排序页面文本,因此"none"请求和"markdown"请求按相同档位计费,耗时也大致相同(2026-10-05 测量结果:相差约 0.1 到 0.5 秒)。不同之处在于文本量:一次en-gb调用使用"markdown"时,在单个市场中返回了约 130,000 个字符的页面文本(约 30,000 到 40,000 个令牌),而"html"的内容量会大上数倍。默认值"none"会将相同调用的响应缩减到一小部分(该测试中约为 20 KB)。仅在需要阅读页面时才请求"markdown"。- 使用
full_content: "markdown"或"html"时,输出大小默认不受限制。许多客户端会限制工具结果(Claude Code 的默认上限约为 25,000 个令牌),因此此类调用可能会在这些客户端中被截断或拒绝;market: "*"则会将输出量最多扩大 12 倍。操作者可以使用STAAN_MCP_MAX_CONTENT_CHARS(见配置)限制每个页面的内容长度:较长的页面会在段落边界处截断,并以[... truncated: N of M characters shown ...]结尾。摘要和额外摘要永远不会被截断。此限制默认关闭;如果你的客户端会限制工具输出且你需要页面文本,可从STAAN_MCP_MAX_CONTENT_CHARS=8000开始设置。
市场和通配符
Staan 支持 12 个市场:fr-fr、de-de、en-us、en-gb、en-fr、en-ca、en-au、
en-in、en-ie、en-nz、en-za 和 en-sg。代码不区分大小写。
market 可接受单个代码、代码列表,或使用 shell 风格通配符的模式
(*、? 和 [...],用于匹配这 12 个代码):
| 值 | 展开为 |
|---|---|
"en-gb" | en-gb |
["en-us", "en-gb"] | en-us、en-gb |
"en-*" 或 "en*" | 10 个 en- 市场 |
"*-fr" | fr-fr、en-fr |
"*" | 全部 12 个市场 |
重复项会被移除,并保留首次出现的顺序。空值、未知代码或未匹配任何内容的模式都会报错,并列出有效市场。
警告:每个市场都会产生一笔计费请求,每一页也同样如此。
market: "*"每次调用会产生 12 笔请求,market: "en-*"会产生 10 笔。Staan 还会实施每秒 20 次请求的速率限制,服务端会通过延迟请求来遵守该限制(请参阅速率限制)。 建议使用明确且精简的市场列表。请查看 Staan 的定价,了解当前费率(撰写本文时,每月前 1,000 次请求免费,普通网页搜索每 1,000 次请求收费 EUR 1,包含额外摘要或完整内容的 请求每 1,000 次收费 EUR 2)。
选择多个市场时,请求会并发运行。如果部分市场失败,仍会返回其他市场的结果,并在 stderr 上记录一条警告,指出失败的市场。只有当所有市场都失败,或密钥本身被拒绝(无效、已禁用或不被允许)时,工具才会失败,因为这些错误会在每个市场中重复出现。
simple_response=true 时的输出(默认)
以文本内容形式返回单个 JSON 数组。选择多个市场时,结果按排名轮流交错排列(第一个市场的排名 0、第二个市场的排名 0,……然后是排名 1,以此类推),并按 URL 去重,因此保留首次出现的结果。
[
{
"title": "Top 7 open source LLMs for 2026",
"url": "https://www.example.com/open-source-llms-2026",
"snippet": "Top open source LLMs in 2026 (...) Open source LLMs are fully accessible for anyone to use, modify and distribute.",
"full_content": "# Top 7 open source LLMs for 2026\n\nLarge Language Models are ..."
},
{
"title": "A list of open LLMs available for commercial use",
"url": "https://github.com/example/open-llms",
"snippet": "A list of open LLMs available for commercial use."
}
]
snippet是上游摘要,后接以(...)拼接的所有额外摘要。- 仅当你请求了
full_content且 Staan 返回了非空文本时,才会出现full_content。无法抓取的页面不会包含full_content键。示例展示了第一个结果,其中full_content: "markdown";默认情况下,没有结果包含此键。
simple_response=false 时的输出
返回一个 JSON 数组,每个请求的市场对应一项,顺序与市场顺序一致。成功的市场包含原始上游响应(此处已删节)。失败的市场则包含一个带有市场和错误消息的对象。
[
{
"search_id": "0d920ee2fc05ffd5fc834ba3ed346b02",
"query": { "q": "open source llms", "market": "en-gb", "count": 10, "offset": 0 },
"web": {
"results": [
{
"title": "Top 7 open source LLMs for 2026",
"url": "https://www.example.com/open-source-llms-2026",
"snippet": "Top open source LLMs in 2026 ...",
"display_url": "https://www.example.com/open-source-llms-2026",
"hostname": "www.example.com",
"extra_snippets": [{ "chunk": "Open source LLMs are ...", "score": 0.99 }],
"full_content": { "text": "# Top 7 ...", "format": "markdown", "length": 5120 }
}
]
}
},
{ "market": "de-de", "error": "Staan rate limit reached (HTTP 429). Try again shortly." }
]
工具调用示例:
{ "query": "python 3.14 release notes", "full_content": "markdown" }
{ "query": "bundesliga tabelle", "market": "de-de", "include_domains": ["kicker.de"] }
{ "query": "oat milk prices", "market": ["en-gb", "en-ie"], "offset": 10, "simple_response": false }
answer
向 POST /v2/answer 请求基于网络来源(或你提供的来源)生成的答案。服务器始终要求返回非流式 Markdown 答案,且不会发送对话历史。
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
query | 字符串,1 到 2,000 个字符 | 必填 | 问题。 |
filter | 字符串 | 无 | 将搜索限制在一个域名内,例如 frandroid.com。也接受 URL,并会将其简化为纯域名。空字符串表示不进行筛选。设置了 sources 时,Staan 会忽略此项。 |
language | "en" 或 "fr" | "en" | 答案的语言。(Staan 自身的默认语言为法语。) |
mode | "short" 或 "long" | "short" | 答案的长度。 |
sources | 来源对象列表,最多 10 个 | 无 | 使用你自己的来源作为答案依据,而不是进行网络搜索。 |
sources_mode | "search" 或 "context" | "search" | "search" 会获取并重新排序来源;"context" 会直接注入其 content。仅在设置了 sources 时发送。 |
simple_response | 布尔值 | true | true 仅返回带引用的答案文本。false 返回原始上游对象。 |
来源对象可包含可选的 url、title(最多 500 个字符)和 content(最多 50,000 个字符)。每个来源都必须至少包含 url 或 content。
Staan 会获取你提供的 url 值,因此会先验证这些值:必须是带有主机名的绝对 http 或 https URL,长度不超过 2,048 个字符,且不含空白字符或控制字符,也不能是字面上的私有、环回、链路本地或保留 IP 地址(localhost 也不行)。其他 URL 会在发送请求前被拒绝。这项检查针对的是 URL 字面值,并不保证主机名解析到的地址符合要求。
simple_response=true(默认值)时的输出
包含一个键的对象。引用会转换为 GitHub 风格的 Markdown 脚注:在每个引用位置插入类似 [^1] 的标记,并在文末附上包含来源 URL 的脚注列表。脚注按首次出现的顺序编号,且只列出被引用的来源。
{
"answer": "The Eiffel Tower is about 330 metres tall.[^1] It was completed in 1889 for the World Fair.[^2] Millions of people visit it every year.\n\n[^1]: https://example.org/eiffel-tower\n[^2]: https://example.org/world-fair-1889"
}
如果响应中没有可用的引用,则原样返回答案。若响应中没有文本类型的 answer 字段,则会报告工具错误(无论 simple_response 的值是什么)。
脚注渲染的详细信息:
- 根据
source_id将引用与其来源匹配。对于编号而非命名的来源(完全没有 ID),按位置匹配("2"表示第二个来源)。如果来源带有 ID,则无法识别的 ID 会被丢弃,而不会猜测对应关系,因为错误引用比不引用更糟。 - 标记不会出现在代码围栏或表格内部,也不会紧接在其末尾,因为这会破坏代码块并吞掉脚注列表。标记会移到该块之前的文本末尾(如果答案开头就是代码块,则会丢弃该块的引用)。
- 只列出没有空白字符或控制字符、长度不超过 2,048 个字符的
http和https来源 URL。其他 URL 会被忽略,以防上游文本在脚注后注入 Markdown。
该工具声明了输出架构:包含必需的 answer 字符串的对象(当 simple_response 为 false 时,还包含其他上游字段)。文本内容与该 JSON 相同,但格式紧凑。
simple_response=false 时的输出
Staan 文档中所述的原始上游对象(经删节):
{
"request_id": "01938fc2-1b3a-7e4d-9c12-a4f8e3c20d91",
"answer": "The Eiffel Tower is about 330 metres tall. It was completed in 1889 ...",
"citations": [{ "position": 42, "source_id": "1" }, { "position": 87, "source_id": "3" }],
"sources": [
{ "id": "1", "url": "https://example.org/eiffel-tower", "title": "Eiffel Tower facts" },
{ "id": "3", "url": "https://example.org/world-fair-1889", "title": "The 1889 World Fair" }
],
"related_queries": ["How was the Eiffel Tower built?"],
"usages": [{ "step": "answer", "input_tokens": 2840, "output_tokens": 194 }]
}
工具调用示例:
{ "query": "What changed in the latest EU AI Act guidance?", "mode": "long" }
{ "query": "meilleur smartphone 2026", "language": "fr", "filter": "frandroid.com" }
{
"query": "Summarise our release policy",
"sources_mode": "context",
"sources": [{ "title": "Policy", "content": "Releases happen on the first Monday of the month ..." }]
}
错误响应
| 情况 | 工具报告的内容 |
|---|---|
未设置 STAAN_API_KEY | 指明该变量的配置错误。 |
| 密钥被拒绝(401) | 身份验证错误。请检查密钥。 |
密钥已禁用(403,KEY_DISABLED) | 密钥已禁用错误。 |
| 未获许可(403) | 对于 answer:此 API 密钥未获启用(仅限受邀用户,请联系 [email protected]),或 Staan 暂时禁用了该功能。 |
| 参数无效(400、422) | Staan 的验证消息,以 ; 连接。 |
| 请求过多(429)、服务器错误(5xx)、连接失败 | 请求会重试(参见 STAAN_MAX_RETRIES),然后报告速率限制、服务器或传输错误。绝不回显上游的服务器错误文本。 |
读取超时(在 STAAN_TIMEOUT 内没有收到答案) | 立即报告,且不重试:请求已经发送,重试会导致重复计费。 |
请求因速率限制而必须等待到 STAAN_DEADLINE 之后才能发送 | 立即报告相同的截止时间错误,且不发送该请求。 |
调用耗时超过 STAAN_DEADLINE | 默认在 50 秒后报告传输错误,其中包括重试和等待时间,以便模型能在典型的 60 秒客户端超时前收到明确的错误。 |
STAAN_BASE_URL 或 STAAN_API_KEY 无法用于 HTTP 请求 | 配置错误会指出这两个变量(绝不会指出密钥)。无论市场数量是多少,整个调用都会立即终止。 |
| 响应超过 32 MiB | 报告响应错误;丢弃响应正文。 |
速率限制
Staan 将每个 API 密钥的请求速率限制为每秒 20 个。market: "*" 会同时发送 12 个请求,因此同时进行两次这样的调用(24 个请求)就会超出限制,而且每次重试也算作一个请求。因此,服务会统计自己发送的请求,并对超出限额的请求延迟处理;绝不会拒绝请求,延迟只会让调用稍微耗时更长。
- 计数规则。 一个服务发送给 Staan 的每个请求:
web_search和answer、每次调用的每个市场,以及每次重试。任意一秒窗口内(采用滑动窗口,因此不会在两秒内突发 40 个请求),最多启动STAAN_RATE_LIMIT个请求(默认值为 20),并在每个窗口增加 50 毫秒的安全余量,以吸收时序抖动:默认情况下,每 1.05 秒最多 20 个请求。请求按发出顺序处理。 - 示例。 同时发起两次
market: "*"调用:24 个请求中的 20 个立即启动,其余 4 个在 1.05 秒后启动。两次调用都会返回完整结果。 - 设置。
STAAN_RATE_LIMIT表示每秒请求数。从 1 开始的小数会向下取整(2.5表示每秒允许 2 个请求);低于 1 时,请求会按1 / rate秒的间隔发送(0.5表示每两秒发送一个)。0表示关闭限流器。允许使用大于 20 的值(你的套餐可能支持更高上限),但 Staan 的默认上限是 20,只有确定自己的上限更高时才应调高。 - 截止时间。 等待时间属于调用耗时,因此会计入
STAAN_DEADLINE(但不计入STAAN_TIMEOUT,后者用于限制单个请求在线路上的耗时)。如果请求必须等到截止时间之后才能发送,就会立即以截止时间错误失败,且不会发送。调用在等待期间取消时,尚未发送的请求不会发出,其占用的时段也会释放给其他请求。 - 范围。 限额按单个服务进程计算。共享一个 API 密钥的多个服务进程(两个客户端、一个 Docker 容器和一个本地服务)互不知情,因此它们的限额会累加:使用
STAAN_RATE_LIMIT在它们之间分配限额。 - 日志。 延迟的请求会以
DEBUG级别记录,并注明延迟时长(下载并运行版本会向 stderr 写入一行);未超出限额的请求不会产生日志。
配置
每个选项都可以通过标志或环境变量设置。标志优先于环境变量。 空环境变量值视为未设置。
下载并运行的变体会读取下方的 Staan API 变量(不会读取其他变量),且只接受 --version 和 --help:服务和 OAuth 表适用于已安装的软件包。
Staan API
| 变量 | 默认值 | 描述 |
|---|---|---|
STAAN_API_KEY | 无 | Staan API 密钥,会作为 Authorization: Bearer 发送到上游。调用工具时必需。必须是没有空格的可见 ASCII 字符(如果密钥中粘贴了弯引号,启动时会拒绝,但不会回显该密钥)。 |
STAAN_BASE_URL | https://api.staan.ai/v2 | API 基础 URL。必须使用 https,除非主机是环回地址;必须包含有效端口,且不得包含凭据(user:password@)。 |
STAAN_TIMEOUT | 30 | 单次尝试的超时时间,以秒为单位(必须大于 0)。 |
STAAN_DEADLINE | 50 | 单次工具调用的总时限,以秒为单位,包括重试和等待(必须大于 0)。请将其设为低于客户端自身的工具超时时间(通常为 60 s)。 |
STAAN_MAX_RETRIES | 2 | 对 429、500、502、503、504 和连接错误的重试次数(0 到 10);读取和写入超时绝不重试。退避时间为 min(0.5 * 2**attempt, 8) 秒;如果存在 Retry-After,还会额外等待其指定时间(最长 30 s)。 |
STAAN_RATE_LIMIT | 20 | 服务每秒启动的最多请求数,包含重试,所有工具和市场合并计算;超出的请求会等待(参见速率限制)。数值:大于等于 1 时向下取整,小于 1 时按 1 / rate 秒的间隔发送请求。0 表示关闭限制。Staan 的默认上限为 20。 |
STAAN_MCP_MAX_CONTENT_CHARS | 0(无限制) | 每个搜索结果保留的最长 full_content 文本长度(0 或至少为 200)。较长页面会在段落边界处截断,并标记为 [... truncated ...]。适用于两种 simple_response 模式。 |
服务
| 标志 | 环境变量 | 默认值 | 描述 |
|---|---|---|---|
--transport {stdio,http,stateless} | STAAN_MCP_TRANSPORT | stdio | 提供服务的传输方式。 |
--host HOST | STAAN_MCP_HOST | 127.0.0.1 | 绑定地址(HTTP 传输方式)。 |
--port PORT | STAAN_MCP_PORT | 8000 | 绑定端口(HTTP 传输方式)。 |
--path PATH | STAAN_MCP_PATH | /mcp | MCP 端点的 URL 路径。 |
--allowed-host HOST | STAAN_MCP_ALLOWED_HOSTS | 无 | 接受的 Host 标头值。可重复使用此标志,或使用逗号分隔的变量值。 |
--allowed-origin ORIGIN | STAAN_MCP_ALLOWED_ORIGINS | 无 | 接受的 Origin 标头值。语法相同。 |
--insecure-no-auth | 无 | 关闭 | 允许在非环回 HTTP 地址上绑定,而不启用 OAuth 门禁。仅可在受信任且经过身份验证的代理之后使用。 |
--log-level LEVEL | STAAN_MCP_LOG_LEVEL | WARNING | 日志级别,也应用于 HTTP 服务(WARNING 会隐藏其逐请求访问日志)。日志仅输出到 stderr。 |
--version | 无 | 无 | 打印版本并退出。 |
OAuth 网关
设置任何 STAAN_MCP_AUTH_* 变量都会启用网关(仅限 HTTP 传输)。配置不完整或不一致会导致启动错误。
| 变量 | 必需 | 描述 |
|---|---|---|
STAAN_MCP_AUTH_ISSUER_URL | 是 | 授权服务器的颁发者 URL。令牌必须包含此 iss。 |
STAAN_MCP_AUTH_RESOURCE_URL | 是 | 此 MCP 端点的规范公共 URL,例如 https://mcp.example.com/mcp。令牌的受众必须为此值。 |
STAAN_MCP_AUTH_SCOPES | 否 | 每个令牌都必须包含的作用域,以空格或逗号分隔。 |
STAAN_MCP_AUTH_JWKS_URL | 二选一 | 用于验证 JWT 访问令牌的 JWKS URL。需要 oauth extra。 |
STAAN_MCP_AUTH_INTROSPECTION_URL | 二选一 | RFC 7662 令牌内省端点。需要客户端 ID 和密钥。 |
STAAN_MCP_AUTH_CLIENT_ID | 使用内省时必需 | 用于内省的客户端 ID(HTTP Basic)。 |
STAAN_MCP_AUTH_CLIENT_SECRET | 使用内省时必需 | 用于内省的客户端密钥。 |
STAAN_MCP_AUTH_ALGORITHMS | 否 | 接受的 JWT 算法。默认值为 RS256,ES256。始终拒绝 none。 |
除主机为环回地址外,所有 URL 都必须使用 https,且不得包含凭据(user:password@)。JWKS URL 和内省 URL 必须且只能设置一个。
仅包含源站的 URL(无路径,例如 https://auth.example.com)会在受保护资源元数据中以尾随斜杠形式发布,因为 SDK 会对其进行规范化。因此,校验会接受仅包含源站的颁发者和资源 URL 的两种写法:aud 或 iss 为 https://host 或 https://host/ 的令牌均可通过。带路径的 URL(https://host/mcp)则必须完全匹配。
JWT 验证会以 10 s 超时获取 JWKS,并且每 30 s 最多刷新一次,因此包含未知 kid 的令牌无法让服务器不断请求你的身份提供方。这需要 PyJWT 2.14 或更高版本,而 oauth extra 会要求此版本。内省会通过一个共享连接池,向你的授权服务器针对每个令牌发送一次带凭据的请求。超过 4,096 个字符或包含非可见 ASCII 字符的令牌会在本地被拒绝,不会发出请求。
安全:OAuth 网关与威胁模型
凭据
- 上游:Staan 不使用 OAuth。唯一的上游凭据是
STAAN_API_KEY中的 API 密钥,由服务器持有,并作为Authorization: Bearer发送。该密钥绝不会写入日志或错误消息,也不会显示在设置对象的repr()中。 - 入站:服务器绝不会将入站访问令牌转发给 Staan(MCP 规范禁止“令牌直通”)。令牌仅用于判定谁可以使用该服务器。没有按请求提供的客户端密钥标头,也没有静态共享 Bearer 令牌。
OAuth 网关
配置后,HTTP 传输会作为 OAuth 2.1 资源服务器运行,具体说明见 MCP 授权规范。授权服务器由你运行(任何能够颁发 JWT 或响应 RFC 7662 内省请求的服务器均可)。网关会:
- 发布 RFC 9728 受保护资源元数据,并对未经身份验证的请求返回
401,同时附带指向该元数据的WWW-Authenticate标头; - 验证每个令牌:签名(JWKS)或
active(内省)、exp、颁发者、与STAAN_MCP_AUTH_RESOURCE_URL相等的受众,以及所需作用域; - 仅接受已配置的算法,绝不接受
none; - 将任何验证失败视为无效令牌,并在调试级别记录原因,但不记录令牌。
威胁模型
| 威胁 | 缓解措施 | 残余风险 |
|---|---|---|
| 他人通过暴露的 HTTP 端口消耗你的 Staan 配额 | 默认绑定到环回地址。非环回绑定需要启用 OAuth 门控或显式使用 --insecure-no-auth,并指定 --allowed-host。 | 是否使用 --insecure-no-auth 由你决定:请在它前面配置身份验证。 |
| DNS 重绑定或恶意网页调用本地服务 | 验证 Host 和 Origin(环回地址上由 SDK 提供保护,其他情况则由你配置允许列表)。 | 错误设置的通配符允许列表会削弱此保护。 |
| 为其他服务签发的令牌在此处被重放 | 受众必须与规范资源 URL 一致。系统会检查颁发者、过期时间和范围。 | 取决于你的授权服务器是否签发了正确的受众。 |
| 访问令牌被盗 | 短期令牌、范围检查,以及在你的代理上使用 TLS。 | 有效令牌在过期前就代表有效用户。请在授权服务器上撤销它。 |
| 密钥通过日志或错误泄露 | 日志仅写入 stderr。密钥、Authorization 标头和令牌绝不会被记录或回显。不会转发上游 5xx 文本。 | 你自己的客户端或代理可能会记录参数或标头。 |
搜索结果(full_content、摘要片段)中的提示词注入 | 两个工具均为只读且无副作用:它们无法写入文件、运行代码或调用其他服务。 | 返回的网页文本是不可信的第三方内容。你的 MCP 客户端和模型必须据此处理;在未经审查的情况下,不应向读取不可信网页的 Agent 提供其他强大工具。 |
成本放大(market: "*"、多页、重试) | 限制市场和域名,设置重试上限,不重试已发送的请求(读取超时),设置整体调用期限,并提供计费说明。 | 单次调用仍可能产生最多 12 次计费请求。请使用 Staan 的用量控制。 |
经提示词注入的模型让 Staan 通过 answer 来源获取 file://、内部或超大的 URL | 来源 URL 必须是看起来公开、绝对、长度不超过 2,048 个字符的 http(s) URL,且不得包含字面形式的私有、环回或链路本地 IP。 | 检查针对的是 URL 字面内容:解析到内部地址的主机名无法在此处被拦截。抓取操作由 Staan 执行。 |
| 匿名调用者让服务不断请求身份提供方(伪造 JWT、无效的 bearer 令牌) | JWKS 刷新冷却时间(PyJWT 2.14+)、10 s 超时、共享的 introspection 连接池,以及对不合理令牌的本地拒绝。 | 对每个看似合理的未知令牌,仍会向你的授权服务器发送一次 introspection 请求;请在代理上设置速率限制。 |
| 网络上传输明文流量 | 所有上游流量均使用 https(除环回地址外,若基础 URL 为 http 则会被拒绝),OAuth URL 也使用 https。 | 服务自身的 HTTP 监听器使用明文 HTTP:请在其前面终止 TLS。 |
不在范围内:运行授权服务器、按用户设置配额,以及多租户密钥隔离。如需报告漏洞,请使用私密安全公告,不要提交公开议题。
故障排除
| 症状 | 原因和修复方法 |
|---|---|
工具提示 STAAN_API_KEY is not set | 将密钥添加到客户端的 env 块中,或在启动服务前导出该密钥。 |
| 身份验证错误 | 密钥错误或已撤销。请在 Staan 控制台中创建新密钥。 |
answer 提示此 API 密钥未启用该功能 | Answer API 仅限受邀用户使用,Staan 也可能暂时禁用该功能。如果问题持续存在,请联系 [email protected]。 |
market 报错并列出有效市场 | 使用 12 个代码之一,或使用 en-* 这样的通配符。 |
| 服务启动时以状态码 2 退出 | 查看 stderr 中的消息:通常是因为使用了非回环主机,但未配置 OAuth 或 --allowed-host。 |
staan-mcp: cannot import the staan_mcp package(单行,状态码 2) | stdio_server.py 被复制到了仓库之外。请将它保留在 src/staan_mcp/ 中,与其他模块放在一起;也可以使用单文件 staan-mcp-stdio.pyz 程序包,或已安装的程序包。 |
staan-mcp needs Python 3.11 or newer; this is ...(状态码 2) | 客户端的 command 启动的是较旧版本的 Python。请将其指向 Python 3.11 或更高版本的解释器,必要时使用完整路径。 |
客户端无法启动 python | 在 Windows 上使用 "command": "py", "args": ["-3", "C:\\path\\to\\staan-mcp-stdio.pyz"](或解释器的完整路径);在 macOS 和 Linux 上,命令通常是 python3。 |
HTTP 客户端收到 421 Misdirected Request | Host 标头不在允许列表中。请使用 --allowed-host 添加它。 |
| 工具结果被截断或客户端拒绝了结果 | 你请求了 full_content: "markdown" 或 "html",这会生成较大的内容(每次调用数万个 token,如果包含多个市场,内容会更多)。请使用默认值 "none"、减少市场数量,或设置 STAAN_MCP_MAX_CONTENT_CHARS。 |
| 响应缓慢 | 多个市场会并行处理,因此一次调用的耗时取决于最慢的那个市场。full_content 只会略微增加延迟;默认值 "none" 主要是缩小答案。调用会在 STAAN_DEADLINE 秒后终止。每秒请求超过 20 次(即两次 market: "*" 调用)时,会受到速率限制的影响。 |
The request to Staan could not be built | STAAN_BASE_URL 中的端口无效,或者 STAAN_API_KEY 包含无法在标头中发送的字符。请修复消息中指出的变量。 |
| 客户端中没有任何内容显示 | 在终端中运行服务并查看 stderr:uvx staan-mcp --log-level DEBUG,或 python staan-mcp-stdio.pyz(下载并运行的变体不接受任何标志,但会将问题记录到 stderr)。在 stdio 模式下,切勿向 stdout 写入内容。 |
开发
你需要 uv。它会安装合适的 Python 和所有工具。
git clone https://github.com/MarcinWolnyEU/Staan-MCP.git
cd Staan-MCP
uv sync # creates .venv with all dependency groups (use UV_LINK_MODE=copy on Windows across drives)
uv run staan-mcp --version
以下质量门禁均为 CI 必须通过的检查:
uv run --frozen ruff check . # every rule enabled, documented opt-outs only
uv run --frozen ruff format --check .
uv run --frozen mypy # --strict
uv run --frozen pylint src tests scripts # 10.00/10
uv run --frozen xenon --max-absolute B --max-modules A --max-average A src
uv run --frozen pytest # 100% line and branch coverage required
uv build && uv run --frozen --only-group dist twine check --strict dist/* && uv run --frozen --only-group dist check-wheel-contents dist/*.whl
uv run python scripts/build_zipapp.py # builds dist-bundle/staan-mcp-stdio.pyz; tests/repo run it
本项目使用 OMP(Opus 5.5、Sonnet 5.5、Mimo V2.6 Pro)、 VSC、git 以及 pyproject.toml 中列出的依赖构建。
- 变异测试使用 mutmut,需要 Linux、macOS 或
WSL(无法在原生 Windows 上运行;请在 Windows 上使用 Docker)。
运行
uv run mutmut run,然后运行uv run mutmut export-cicd-stats。CI 会检查mutants/mutmut-cicd-stats.json。只有stdio_server.py模块会被跳过。 - 实时测试会调用真正的 Staan API。除非设置了
STAAN_API_KEY并传入--run-live,否则会跳过这些测试:uv run pytest tests/live --run-live --no-cov。CI 从不运行这些测试。 - 钩子:运行
uv tool install prek && prek install(或使用pre-commit)会执行 ruff、mypy、 pylint、xenon、zizmor、actionlint、typos、针对server.json、工作流和 Dependabot 的架构检查、机密扫描(detect-private-key和 gitleaks),以及 Conventional Commits 检查。CI 会对每个文件运行 相同的钩子(并检查拉取请求标题),还会使用 gitleaks 扫描整个 git 历史记录。尽可能将密钥文件放在仓库目录之外;.gitignore也涵盖*key*.txt、*.pem、*.key和credentials*。 - 关于 Staan API、MCP SDK 和 OAuth 的研究笔记位于
docs/research/。请参阅 变更日志,了解变更内容。
贡献
创建一个 PR,确保流水线全部通过 (这意味着代码覆盖率达到 100%、lint 和类型检查通过、项目保持当前兼容性等)。请说明实施变更的原因, 以及变更带来的风险。我会亲自审查你的 PR。
所有拉取请求都会在 2 周内得到回复。
提供反馈和报告错误
所有问题都会在 2 周内得到回复,安全公告会在 48 小时内得到回复。
许可证
MIT (c) 2026 Marcin Wolny。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。