In your agent¶
Sluicer’s MCP server is one command, and every client that speaks MCP can start it:
uvx --with "sluicer[mcp]" sluicer mcp
It needs uv on the PATH; uvx fetches Sluicer
and its mcp extra, which brings fetching and markdown, the first time.
sluicer-mcp, after uv pip install "sluicer[mcp]", is the same server.
Every one of its twelve tools only reads – the page it is given, or the web –
and says so in its MCP annotations (readOnlyHint, not destructiveHint), so
a client that asks before a tool writes runs them without asking. Measured
with Codex 0.144.4 on 2026-09-24: before the annotations, codex exec
cancelled the call unless the server’s tools were approved in advance; with
them, it runs the tool in its default mode, in writes and in auto.
Each tool a client registers costs its agent context, called or not. To offer
only some, name them: sluicer mcp --tools extract_declared,page_markdown, or
SLUICER_MCP_TOOLS=extract_declared,page_markdown for a client that sets
variables rather than arguments. A name that is not a tool stops the server
with the list of them, and exit code 2. Where the scripts directory is not on the PATH,
python -m sluicer mcp starts the same server.
The server refuses to fetch localhost, a private network or a cloud’s
metadata endpoint unless it is started with SLUICER_ALLOW_PRIVATE=1.
Claude Code¶
claude mcp add sluicer -- uvx --with "sluicer[mcp]" sluicer mcp
Or install the repository as a plugin, which brings the server and a skill that tells the agent when to reach for it:
claude plugin marketplace add Gi0tto/sluicer
claude plugin install sluicer@sluicer
Verified end to end on 2026-09-24 with Claude Code 2.1.281: given the published
server, claude -p called extract_declared and answered a page’s price, its
place and the conflict with the page’s second price; the plugin, loaded with
--plugin-dir, brought the server and the skill; and claude plugin validate
passes the repository’s manifests. Installing from the marketplace was not run.
Codex¶
codex mcp add sluicer -- uvx --with "sluicer[mcp]" sluicer mcp
This writes the server into ~/.codex/config.toml:
[mcp_servers.sluicer]
command = "uvx"
args = ["--with", "sluicer[mcp]", "sluicer", "mcp"]
Codex reads skills in the open Agent Skills format
from ~/.agents/skills/ and from .agents/skills/ in a repository. Sluicer’s
skill keeps to that format: the standard’s own validator passes it, and a
test in the suite holds it to the fields the standard allows.
git clone --depth 1 https://github.com/Gi0tto/sluicer /tmp/sluicer
mkdir -p ~/.agents/skills && cp -r /tmp/sluicer/skills/sluicer ~/.agents/skills/
Each release from 0.8.0 also carries the skill as sluicer-skill-VERSION.zip,
with the sluicer folder at its root: unzip it into ~/.agents/skills/, or
upload it to claude.ai as a custom skill.
Verified end to end on 2026-09-24 with codex-cli 0.144.4: Codex called
extract_declared and answered a page’s price with its source and place.
As an Agent Plugin¶
The repository is also a plugin in the open
Agent Plugins 1.0 format: plugin.json and
mcp.json at its root, and the skill in skills/. VS Code, GitHub Copilot,
Cursor and Codex load that format, and each gets the server, started as
uvx --with "sluicer[mcp]==VERSION" sluicer mcp at the plugin’s own version,
and the skill. It needs uv on the PATH, as the commands above do.
- Codex:
codex plugin marketplace add Gi0tto/sluicer, thencodex plugin add sluicer@sluicer. - VS Code: run Chat: Install Plugin From Source and give it
https://github.com/Gi0tto/sluicer. - Cursor: copy the repository into
~/.cursor/plugins/local/sluicerand reload the window.
Run on 2026-09-25 from a local copy of the repository, not from GitHub:
codex-cli 0.157.0 installed the plugin at its version and registered the
server with that command, and Claude Code 2.1.282, given the same folder,
still loaded its own plugin from .claude-plugin/, server and skill. The
official schemas pass both files. VS Code and Cursor were not run.
Other clients¶
Gemini CLI was run here; the others are written from each client’s own documentation, as it read on 2026-09-24, and were not. Each starts the same command.
Cursor – .cursor/mcp.json in a project, or ~/.cursor/mcp.json:
{
"mcpServers": {
"sluicer": {
"type": "stdio",
"command": "uvx",
"args": ["--with", "sluicer[mcp]", "sluicer", "mcp"]
}
}
}
VS Code – .vscode/mcp.json in a workspace, or the user profile’s
mcp.json:
{
"servers": {
"sluicer": {
"command": "uvx",
"args": ["--with", "sluicer[mcp]", "sluicer", "mcp"]
}
}
}
or, from a terminal:
code --add-mcp '{"name":"sluicer","command":"uvx","args":["--with","sluicer[mcp]","sluicer","mcp"]}'
Gemini CLI – run on 2026-09-24:
gemini mcp add -s user sluicer uvx --with "sluicer[mcp]" sluicer mcp
writes this into ~/.gemini/settings.json (or .gemini/settings.json in a
project, without -s user), and gemini mcp list then shows the server
connected. Gemini CLI starts MCP servers only in a folder it trusts: in one
it does not, the server is listed as disabled.
{
"mcpServers": {
"sluicer": {
"command": "uvx",
"args": ["--with", "sluicer[mcp]", "sluicer", "mcp"]
}
}
}
Claude Desktop – each release from 0.8.0 carries sluicer-VERSION.mcpb,
a bundle in the MCPB format:
open it with Claude Desktop to install the server. It is 134 KB and holds no
Python; its manifest has the host install sluicer[mcp] at that version from
PyPI with uv, and its two settings are the variables at the top of this
page, SLUICER_MCP_TOOLS and SLUICER_ALLOW_PRIVATE. The release checks it with the
format’s own validator, then unpacks it and lists its tools with uv, as
the manifest starts it. Installing it in Claude Desktop was not run.
Or by hand: claude_desktop_config.json, in
~/Library/Application Support/Claude/ on macOS and %APPDATA%\Claude\ on
Windows, with the same mcpServers entry as Gemini CLI’s. An application
started from the Dock may not see your shell’s PATH; if uvx is not found,
write its full path, which which uvx prints.
Zed – settings.json:
{
"context_servers": {
"sluicer": {
"command": "uvx",
"args": ["--with", "sluicer[mcp]", "sluicer", "mcp"],
"env": {}
}
}
}
In Docker – the image’s default command is the MCP server over stdio,
so a client starts it with -i:
{
"mcpServers": {
"sluicer": {
"command": "docker",
"args": ["run", "-i", "--rm", "ghcr.io/gi0tto/sluicer"]
}
}
}
Each release from 0.8.0 publishes the image, for amd64 and arm64, after a client has listed its tools from it; the HTTP API page says what it carries.
Anything else that speaks MCP – over stdio, the command above.
Over HTTP: n8n and Dify¶
n8n and Dify do not start MCP servers over stdio; they connect to one over
HTTP. sluicer serve (the api extra) is that server at /mcp, over MCP’s
streamable HTTP transport, with the same tools:
uv tool install "sluicer[api]"
export SLUICER_API_TOKEN="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
sluicer serve --host 0.0.0.0 # beyond loopback, so it needs the token
On one machine with no container in between, sluicer serve alone listens on
127.0.0.1:8000 with no token. A container’s 127.0.0.1 is its own, so for
n8n or Dify in Docker the server listens beyond loopback, with the token, and
the address names the host as the container sees it
(host.docker.internal with Docker Desktop), or Sluicer runs as a container on
the same network. The HTTP API has the details
and what the server refuses.
n8n – the MCP Client Tool node, on an AI Agent’s Tool input, or the MCP Client node for one call as a step:
| Field | Value |
|---|---|
| Endpoint URL | http://host.docker.internal:8000/mcp |
| Server Transport | HTTP Streamable |
| Authentication | Bearer Auth, a credential whose Bearer Token is the token |
| Options > Timeout | 130000, above the server’s 120-second budget; the node waits 60000 ms by default |
Dify – Tools > MCP > Add MCP Server (HTTP):
| Field | Value |
|---|---|
| Server URL | http://host.docker.internal:8000/mcp |
| Name, Server Identifier | sluicer |
| Advanced Options > Custom Headers | Authorization: Bearer <token> |
| Advanced Options > Timeouts | above the server’s 120-second budget |
Neither was run here; both are written from their documentation as it read on
2026-09-25. What was run, against sluicer serve, is the mcp Python SDK’s
client and the TypeScript SDK’s, which n8n’s MCP nodes are built on: both
listed the ten tools it had before select_values and extract_many and called them.
The documentation, for an agent¶
The site serves llms.txt in
llmstxt.org’s format, one link a page with its opening
paragraph, and every page’s markdown at the page’s path with .md
(https://gi0tto.github.io/sluicer/agents.md for this one). Both are written
from the nav and the pages at each build. context7.json at the repository’s
root says which of these pages Context7 indexes, and
the rules it gives an agent.
In your own agent’s code¶
Any framework that speaks MCP starts the same server. Run it as its own
process, as below, rather than importing it into your application’s
environment: measured on 2026-09-24, langchain-mcp-adapters 0.3.1 resolves
mcp 1.30 and fails to import against mcp 2.2, which sluicer[mcp] needs, so
the two in one environment break the application. As separate processes they
speak MCP to each other, and every one of these listed the ten tools it had
before select_values and answered a page’s price with its place:
LangChain (langchain-mcp-adapters 0.3.1, its client on mcp 1.30):
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({"sluicer": {
"command": "uvx", "args": ["--with", "sluicer[mcp]", "sluicer", "mcp"],
"transport": "stdio",
}})
tools = await client.get_tools() # hand them to any LangChain agent
OpenAI Agents SDK (openai-agents 0.22.3):
from agents import Agent
from agents.mcp import MCPServerStdio
async with MCPServerStdio(params={
"command": "uvx", "args": ["--with", "sluicer[mcp]", "sluicer", "mcp"],
}) as sluicer:
agent = Agent(name="reader", mcp_servers=[sluicer])
Pydantic AI (pydantic-ai-slim[mcp] 2.48, on FastMCP 4.0.7):
from pydantic_ai import Agent
from pydantic_ai.mcp import MCPToolset, StdioTransport
sluicer = MCPToolset(StdioTransport("uvx", ["--with", "sluicer[mcp]", "sluicer", "mcp"]))
agent = Agent("openai:gpt-5", toolsets=[sluicer])
The model is the framework’s to choose: Sluicer’s answers are the same whichever model reads them, since none is asked to produce them.
Without MCP¶
sluicer serve answers the same tools over HTTP, for any language and any
model’s function calling: POST /v1/tools/<name> with the tool’s arguments as
JSON, described at /openapi.json. See the HTTP API. And in
Python, sluicer.extract(html, url=...) is the library the server calls.
With other tools¶
Sluicer reads HTML, whoever fetched it: extract never fetches. Hand it the
page as the other tool brought it back – bytes are better than text, since
the page’s own charset declaration is still in them – with the address it
came from and, when the tool keeps them, the response’s headers: a Link
header’s canonical, an X-Robots-Tag and the charset are read from those.
Each of these was run on 2026-09-24 against a local page, and each answered
its price with its place and its canonical from the Link header:
import sluicer
# httpx 0.28
r = httpx.get(url)
sluicer.extract(r.content, url=str(r.url), headers=dict(r.headers))
# Playwright 1.63: the rendered page, as text
response = page.goto(url)
sluicer.extract(page.content(), url=page.url, headers=response.all_headers())
# Scrapling 0.4
page = Fetcher.get(url)
sluicer.extract(page.body, url=page.url, headers=dict(page.headers))
# Crawl4AI 0.9: inside `async with AsyncWebCrawler() as crawler`
result = await crawler.arun(url=url)
sluicer.extract(result.html, url=result.url, headers=result.response_headers)
# Scrapy 2.19: in a spider's callback
headers = {k.decode(): v[0].decode() for k, v in response.headers.items()}
sluicer.extract(response.body, url=response.url, headers=headers)
Firecrawl’s raw HTML is the page as it was received, and firecrawl-py
4.44 holds it in raw_html; this one was not run here, since Firecrawl’s
service needs a key:
doc = Firecrawl(api_key=key).scrape(url, formats=["rawHtml"])
sluicer.extract(doc.raw_html, url=doc.metadata.source_url)
All six install beside Sluicer’s base package in one environment; the base package needs only lxml, click, cssselect and protego.