
Staan
io.github.MarcinWolnyEU由 GitHub 帳號 MarcinWolnyEU 發布v1.0.1更新於 Oct 24, 2026
Web search with full-page content and grounded answers from the Staan API.
概覽
Staan MCP 透過 Staan API 讓助理進行網頁搜尋(可選擇回傳整頁內容),並取得附引用的有據回答。
- 可以做什麼
- 在最多 12 個 Staan 市場中搜尋網頁,支援網域包含/排除、site: 運算子與以 offset 翻頁
- 以 markdown 或 HTML 回傳每筆結果的整頁正文,或只回傳摘要讓回應保持精簡
- 提出問題並取得以網頁來源為依據的 Markdown 回答,引用以註腳形式呈現
- 改用你自行提供的來源而非網頁搜尋來產生回答,可選簡短或詳細模式
- 適用情境
- 適合需要最新網頁結果、可閱讀的頁面正文,或需要可追溯來源的回答而非模型自身記憶的情境,尤其是多市場或限定網域的調查。
- 執行需求
- 以 stdio 在本機執行,通常用 uvx 從 PyPI 套件啟動(需 Python 3.11 或更新版本);也可用純 Python 3.11+ 執行原始碼或單一檔案包。需要 Staan API 金鑰,透過環境變數 STAAN_API_KEY 傳入,並需能連線至 Staan API 的網路。選用設定包括速率限制、逾時、截止時間與內容長度。
安裝
在 SourceWeft 中
- 開啟 儀表板中的 Staan,將其新增到工作區。
- 為需要使用其工具的對話啟用該服務。
Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。
其他 MCP 客戶端
參照 儲存庫 中的啟動說明。
README
Staan-MCP
[Python versions] [PyPI version] [CI] [Codecov] [OpenSSF Scorecard] [License: MIT] [Ruff] [uv] [mypy: strict] [MCP Registry]
An MCP server for the European search engine STAAN (Search Trusted API Access Network) built by the European Perspective SAS, co-owned by Qwant and Ecosia search engines. It's an open European alternative to Exa Search and other SERP services, with its own index, oriented primarily at the European markets (+parts of Commonwealth), giving it a unique perspective that other search providers might be missing. Free subscription grants 1000 requests per month.
MCP comes with two tools:
web_search: web search with optional full-page content (Markdown or HTML), in one or many Staan markets at once.answer: a grounded answer with citations, rendered as Markdown with GitHub-style footnotes.
It runs over stdio (the default), streamable HTTP, or stateless streamable HTTP, and can protect the HTTP transports with an optional OAuth 2.1 resource-server gate. If you only need stdio, it can also run with nothing installed, just download the zip of the repository and point your AI agent to this file (README.md).
Note:
staan-mcpis not affiliated with Staan, European Perspective, Qwant or Ecosia. It's just a solo project to deliver quality Python MCP that, at the time of release, did not exist.
Important: the
answerAPI endpoint is invitation-only and even if your invitation was granted - it might be tempolarly disabled at any point in time, resulting in{"message":"Forbidden","statusCode":403}responses.
Quickstart
You need a Staan API key (create one in the Staan console, the free plan includes 1,000 requests per month). Then if you have uv installed you can simply configure your harness - you don't need to download anything from this repository here, uv through the uvx command will download the latest version of the MCP automatically for you.
This exact JSON works in Claude Desktop and Cursor. Claude Code, VS Code and Hermes Agent use a different format: see Client configuration.
If you are an LLM or simply need more details, continue further on.
Detailed setup
Three ways to run it
- Installed package
uvx staan-mcp, or pip install staan-mcp - installs a number of dependencies - the MCP SDK, httpx2, pydantic, anyio (and pyjwt for the OAuth gate). In exchange provides not just stdio, but also Streamable HTTP and stateless streamable HTTP connections, both HTTP connections with OAuth gate.
- Download and run
Download it on GitHub via Code->Download ZIP or via git clone. Then you can configure your MCP to run through stdio_server.py (See: Without installing anything part of the install guide)
- Via releases
Download the latest release
Only the installed package serves HTTP and the OAuth gate. Ways 2 and 3 run the same
stdio_server.py; the bundle is that file and the few standard-library modules it imports, zipped
into one file you can put anywhere.
Client configuration
All examples launch the server with uvx and pass the key through the STAAN_API_KEY
environment variable. Replace your-staan-api-key with your key.
Claude Desktop
Edit claude_desktop_config.json (Settings, Developer, Edit Config):
Claude Code
Add --scope user to make it available in every project, or --scope project to write a
shareable .mcp.json (do not put the key in a file you commit; use environment variable
expansion such as "STAAN_API_KEY": "${STAAN_API_KEY}" there).
VS Code
Create .vscode/mcp.json. VS Code prompts for the key once and stores it securely:
Cursor
Create .cursor/mcp.json in your project, or ~/.cursor/mcp.json for every project. Cursor
expands ${env:NAME}, so the key can stay in your shell environment:
The repository is also a Cursor plugin (.cursor-plugin/plugin.json and mcp.json). When installed as one, Cursor asks for the Staan API key as a plugin variable instead of reading it from your shell.
Hermes Agent
Hermes reads MCP servers from mcp_servers in ~/.hermes/config.yaml:
Hermes also has a native plugin that offers the same two tools as staan_web_search and
staan_answer, without a second process: see src/hermes. It is separate
from the server and not part of the staan-mcp package.
Any other client
Any client that can launch a stdio command works: run uvx staan-mcp with STAAN_API_KEY in
the environment. For HTTP clients, see Transports.
Without installing anything
Ways 2 and 3 need no pip install, no virtual environment and no third-party package: only a
plain Python 3.11 or newer, and they serve stdio only (HTTP and the OAuth gate need the
installed package).
From a checkout (way 2). Download the repository (git clone or the zip of the source tree),
keep src/staan_mcp/ together, and point your client at one file,
src/staan_mcp/stdio_server.py:
The script puts its own src directory on sys.path and reuses the package's standard-library
modules (markets, domains, result formatting, errors, configuration and the tool texts), so it is
not a file you can copy on its own. Copied away from the repository it prints one line saying so
and exits with status 2, without a traceback; use the bundle instead.
Which Python. Both need Python 3.11 or newer. On Windows, python may be missing or be the
Microsoft Store stub; use the launcher with "command": "py", "args": ["-3", "C:\\path\\to\\staan-mcp-stdio.pyz"] (JSON needs the doubled backslashes), or the full path of
the interpreter. On macOS and Linux the command is often python3. With an older Python the file
stops at once with staan-mcp needs Python 3.11 or newer; this is X.Y.Z (path) and status 2.
Check with python --version.
What it is. It never imports the MCP SDK, httpx2, pydantic, anyio or pyjwt; a test
runs it with python -I -S and with a blocker on every non-standard-library import to prove it,
and runs the built bundle the same way. It offers the same web_search and answer tools,
environment variables, results and error messages as the installed package (tests compare the two
on the same inputs), over stdio only. It has its own small urllib transport instead of httpx2,
and the same retry policy. It is checked by the same lint, type, complexity and coverage gates as
the rest of the package; only mutation testing skips it.
Single-file bundle (way 3). Download staan-mcp-stdio.pyz from the
latest release, put it anywhere and
point your client at it:
The file is a Python zip application: python staan-mcp-stdio.pyz --version prints
staan-stdio X.Y.Z. It contains exactly the standard-library modules of the package that
stdio_server.py imports, and nothing else. Each release attaches it next to the wheel and sdist,
with a signed build provenance attestation
(gh attestation verify staan-mcp-stdio.pyz --repo MarcinWolnyEU/Staan-MCP). The build is
reproducible: python scripts/build_zipapp.py in a checkout of the tag writes byte-identical
output and prints its SHA-256.
Tool reference
Both tools are read-only and open-world (they reach an external service, and change nothing). Failures are reported as MCP tool errors with a short, safe message that never contains your key.
web_search
Searches the web through POST /v2/search/web. Each call returns one page of up to 10 results
per market.
Notes:
countis not a parameter. Staan only accepts a page size of 10, and the server always sends it. Useoffsetto page.- Domains are normalised for you:
https://www.Example.com:443/a?bbecomesexample.com, anden.wikipedia.orgstaysen.wikipedia.org. - The server always requests extra snippets. If your key's plan includes them, they are merged
into each result's
snippet. If not, the field is simply absent upstream and nothing breaks. - A listed domain also covers its subdomains. Checked live on 2026-10-05:
include_domains: ["python.org"]returneddiscuss.python.org,docs.python.org,wiki.python.organdwww.python.org, andexclude_domains: ["python.org"]removed all of them. You do not need to list subdomains one by one. full_contentchanges the size of the response, not its cost. The server always asks Staan for extra snippets, which is what makes Staan fetch and rank page text, so a"none"request is billed in the same tier and takes about as long as a"markdown"one (measured on 2026-10-05: about 0.1 to 0.5 s apart). What differs is the amount of text: oneen-gbcall with"markdown"returned about 130,000 characters of page text in a single market (roughly 30,000 to 40,000 tokens), and"html"is several times larger. The default"none"brings the same call down to a small fraction (about 20 KB in that test). Ask for"markdown"only when you need to read the pages.- With
full_content: "markdown"or"html"the output size is unbounded by default. Many clients cap a tool result (Claude Code's default is about 25,000 tokens), so such a call can be truncated or rejected there, andmarket: "*"multiplies it by up to 12. The operator can cap each page withSTAAN_MCP_MAX_CONTENT_CHARS(see Configuration): longer pages are cut at a paragraph boundary and end with[... truncated: N of M characters shown ...]. Snippets and extra snippets are never cut. The limit is off by default;STAAN_MCP_MAX_CONTENT_CHARS=8000is a reasonable starting point if your client limits tool output and you want page text.
Markets and wildcards
Staan supports 12 markets: fr-fr, de-de, en-us, en-gb, en-fr, en-ca, en-au,
en-in, en-ie, en-nz, en-za and en-sg. Codes are case-insensitive.
market accepts a single code, a list of codes, or patterns using shell-style wildcards
(*, ? and [...], matched against the 12 codes):
Duplicates are removed and the first-seen order is kept. An empty value, an unknown code, or a pattern that matches nothing is an error that lists the valid markets.
Warning: every market is one billable request, and so is every page.
market: "*"costs 12 requests per call, andmarket: "en-*"costs 10. Staan also enforces a rate limit of 20 requests per second, which the server keeps to by delaying requests (see Rate limiting). Prefer an explicit, short list of markets. Check Staan's pricing for the current rates (when this was written, the first 1,000 requests per month were free, plain web search was EUR 1 per 1,000 requests, and requests with extra snippets or full content were EUR 2 per 1,000).
With several markets the requests run concurrently. If some markets fail, the others are still returned and a warning naming the failed markets is logged on stderr. The tool only fails when every market fails, or when the key itself is rejected (invalid, disabled, or not permitted), because those errors would repeat for every market.
Output with simple_response=true (default)
A single JSON array, as text content. With several markets, results are interleaved round-robin by rank (rank 0 of the first market, rank 0 of the second, ..., then rank 1, and so on) and de-duplicated by URL, so the first occurrence wins.
snippetis the upstream snippet, followed by any extra snippets joined with(...).full_contentappears only when you asked for it and Staan returned non-empty text. A page that could not be fetched has nofull_contentkey. The example shows the first result withfull_content: "markdown"; by default no result has the key.
Output with simple_response=false
A JSON array with one entry per requested market, in market order. A successful market is the raw upstream response (abridged here). A failed market is an object with the market and a message.
Example tool calls:
answer
Asks POST /v2/answer for an answer grounded in web sources (or in sources you supply). The
server always asks for a non-streamed, Markdown answer, and never sends conversation history.
A source object has optional url, title (up to 500 chars) and content (up to 50,000
chars). Each source needs at least a url or content.
Staan fetches the url values you pass, so they are validated first: an absolute http or
https URL with a host, at most 2,048 characters, no whitespace or control characters, and no
literal private, loopback, link-local or reserved IP address (nor localhost). Anything else is
rejected before a request is sent. This is a filter on the literal URL, not a guarantee about what
a host name resolves to.
Output with simple_response=true (default)
An object with one key. Citations become GitHub-flavoured Markdown footnotes: a marker such as
[^1] is inserted at each cited position, and a footnote list with the source URLs is
appended. Footnotes are numbered in order of first appearance, and only cited sources are listed.
If the response has no usable citations, the answer is returned unchanged. A response without a
text answer field is reported as a tool error (with either value of simple_response).
Details of the footnote rendering:
- A citation is matched to its source by
source_id. Sources that are numbered rather than named (no ids at all) are matched by position ("2"is the second source). When the sources do have ids, an unknown id is dropped instead of being guessed, because a wrong citation is worse than none. - A marker never lands inside a code fence or a table, or at the end of one, since that would break the block and swallow the footnote list. It moves to the end of the text before the block (a citation for a block at the very start of the answer is dropped).
- Only
httpandhttpssource URLs without whitespace or control characters, and of at most 2,048 characters, are listed. Others are ignored, so upstream text cannot inject Markdown after the footnotes.
The tool declares its output schema: an object with a required answer string (plus the other
upstream fields when simple_response is false). The text content is the same JSON, compact.
Output with simple_response=false
The raw upstream object, as documented by Staan (abridged):
Example tool calls:
Error responses
Rate limiting
Staan caps an API key at 20 requests per second. market: "*" sends 12 requests at once, so two
such calls at the same time (24 requests) would exceed it, and every retry is a request too. The
server therefore counts the requests it sends and delays the ones that would not fit; it
never refuses them, and a delayed call just takes a little longer.
- What counts. Every request to Staan that one server sends:
web_searchandanswer, every market of a call, every retry. At mostSTAAN_RATE_LIMITrequests (20 by default) start in any window of one second (a sliding window, so there is no burst of 40 across two seconds), plus a safety margin of 50 ms per window that absorbs timing jitter: at the default, 20 requests per 1.05 s. Requests are served in the order they were made. - Example. Two simultaneous
market: "*"calls: 20 of the 24 requests start at once, the other four 1.05 s later. Both calls return complete results. - Setting it.
STAAN_RATE_LIMITis requests per second. From 1 upwards a fraction is rounded down (2.5allows 2 a second); below 1 requests are spaced1 / rateseconds apart (0.5is one every two seconds).0turns the limiter off. Values above 20 are accepted (your plan may allow more), but 20 is Staan's default cap, so raise it only if you know yours is higher. - Deadline. The wait is part of the call, so it counts towards
STAAN_DEADLINE(and not towardsSTAAN_TIMEOUT, which times one request on the wire). A request that would have to wait past the deadline fails at once with the deadline error and is not sent. A call that is cancelled while waiting never sends its pending requests, and the slots they had are free for others. - Scope. The budget belongs to one server process. Several server processes that share
one API key (two clients, a Docker container next to a local one) do not know about each other,
so their budgets add up: split the budget between them with
STAAN_RATE_LIMIT. - Logs. A delayed request is logged at
DEBUGlevel with its delay (the download-and-run variants write one line to stderr); requests within the budget log nothing.
Configuration
Every option can be set with a flag or an environment variable. Flags win over the environment. Blank environment values count as unset.
The download-and-run variants read the Staan API variables below (they use no
others) and accept only --version and --help: the server and OAuth tables apply to the
installed package.
Staan API
Server
OAuth gate
Setting any STAAN_MCP_AUTH_* variable enables the gate (HTTP transports only). Partial or
inconsistent configuration is a startup error.
All URLs must be https unless the host is loopback, and none may contain credentials
(user:password@). Exactly one of the JWKS and introspection URLs must be set.
An origin-only URL (no path, such as https://auth.example.com) is published in the
protected-resource metadata with a trailing slash, because the SDK normalises it. The checks
therefore accept both spellings of an origin-only issuer and resource: a token whose aud or
iss is https://host or https://host/ passes. A URL with a path (https://host/mcp) is
matched exactly.
JWT verification fetches the JWKS with a 10 s timeout and refreshes it at most once per 30 s, so
tokens with an unknown kid cannot make the server hammer your identity provider. That needs
PyJWT 2.14 or newer, which the oauth extra requires. Introspection sends one credentialed
request per token to your authorization server over one shared connection pool. Tokens that are
longer than 4,096 characters or not visible ASCII are rejected locally without a request.
Security: OAuth gate and threat model
Credentials
- Upstream: Staan has no OAuth. The only upstream credential is the API key in
STAAN_API_KEY, held by the server and sent asAuthorization: Bearer. It is never logged, never included in error messages, and not shown inrepr()of the settings object. - Inbound: the server never forwards an inbound access token to Staan (the MCP specification's "token passthrough" ban). Tokens are used only to decide who may use the server. There is no per-request client key header and no static shared bearer token.
OAuth gate
When configured, the HTTP transports act as an OAuth 2.1 resource server, as described in the MCP authorization specification. You run the authorization server (any server that can issue JWTs or answer RFC 7662 introspection). The gate:
- publishes RFC 9728 protected-resource metadata, and answers unauthenticated requests with
401and aWWW-Authenticateheader that points to it; - verifies each token: signature (JWKS) or
active(introspection),exp, issuer, audience equal toSTAAN_MCP_AUTH_RESOURCE_URL, and the required scopes; - accepts only the configured algorithms and never
none; - treats any verification failure as an invalid token, and logs the reason at debug level without the token.
Threat model
Out of scope: running the authorization server, per-user quotas, and multi-tenant key isolation. To report a vulnerability, use a private security advisory, not a public issue.
Troubleshooting
Development
You need uv. It installs the right Python and every tool.
The quality gates, all required in CI:
Project was built using OMP (Opus 5.5, Sonnet 5.5, Mimo V2.6 Pro), VSC, git and dependencies you'll find in pyproject.toml.
- Mutation testing uses mutmut and needs Linux, macOS or
WSL (it does not run on native Windows; use Docker there).
uv run mutmut run, thenuv run mutmut export-cicd-stats. CI gates onmutants/mutmut-cicd-stats.json.stdio_server.pyis the only module it skips. - Live tests call the real Staan API. They are skipped unless
STAAN_API_KEYis set and you pass--run-live:uv run pytest tests/live --run-live --no-cov. CI never runs them. - Hooks:
uv tool install prek && prek install(or usepre-commit) runs ruff, mypy, pylint, xenon, zizmor, actionlint, typos, schema checks forserver.json, the workflows and Dependabot, secret scanning (detect-private-keyand gitleaks) and a Conventional Commits check. CI runs the same hooks on every file (and checks the pull request title), and scans the whole git history with gitleaks as well. Keep key files out of the repository directory if you can;.gitignorealso covers*key*.txt,*.pem,*.keyandcredentials*. - Research notes on the Staan API, the MCP SDK and OAuth are in
docs/research/. See the changelog for what changed.
Contributing
Create a PR, make sure pipelines pass green ( which, among others, means that the code coverage is at 100%, lint and type check passes, project retains the current compatibility, etc. etc.), describe why changes were implemented, what are the risks associated with the change. Your PR will be reviewed by me personally.
All pull requests are responded to within 2 weeks.
Providing feedback and bug reports
Report it via Issues tab. For security issues use dedicated form.
All issues are responded to within 2 weeks, security advisories within 48 hours.
License
MIT (c) 2026 Marcin Wolny.
來源:README.md,提交 e2ffe52
工具
0版本歷史
1- v1.0.1最新Oct 12, 2026
評分
已滿足適用檢查項的 50%
該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。
- 近 30 天內可連通25 分
請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。
- 已宣告認證方式10 分
請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。
- 已列出工具15 分
請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。
- 描述必需15 分
- 圖示10 分
- 文件15 分
- 授權條款10 分