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
2 changes: 1 addition & 1 deletion bench/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [6.5.5] - 2026-09-04

### Fixed
- Report the DocPull package version during MCP initialization and attach the
advertised cost description and `docpull/cost` metadata to every MCP tool.

## [6.5.4] - 2026-09-04

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "docpull",
"version": "6.5.4",
"version": "6.5.5",
"description": "Pull public web sources into Claude Code. Indexes static and server-rendered sites as local Markdown with conditional-GET caching, then exposes them as MCP tools. Local, browser-free, no API keys.",
"author": {
"name": "Raintree Technology",
Expand Down
2 changes: 1 addition & 1 deletion plugin/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "docpull",
"name": "docpull",
"version": "6.5.4",
"version": "6.5.5",
"description": "Pull public web sources into Codex as local, searchable Markdown through docpull's MCP server.",
"skills": "./skills/",
"mcpServers": "./.mcp.json",
Expand Down
4 changes: 2 additions & 2 deletions plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ for the boundary between the plugin's MCP tools and the broader CLI/SDK.
The plugin wraps the `docpull` CLI. Install the MCP extra first:

```bash
pipx install 'docpull[mcp]==6.5.4'
docpull --version # should print 6.5.4 or newer
pipx install 'docpull[mcp]==6.5.5'
docpull --version # should print 6.5.5 or newer
docpull mcp --help
```

Expand Down
2 changes: 1 addition & 1 deletion plugin/scripts/launch-docpull-mcp.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const child = spawn("docpull", ["mcp", ...process.argv.slice(2)], {
child.on("error", (error) => {
if (error.code === "ENOENT" || error.code === "EACCES") {
console.error(
"DocPull is not installed or is not executable. Run: pipx install 'docpull[mcp]==6.5.4'",
"DocPull is not installed or is not executable. Run: pipx install 'docpull[mcp]==6.5.5'",
);
process.exitCode = 127;
return;
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "docpull"
version = "6.5.4"
version = "6.5.5"
dynamic = []
description = "Declare, sync, diff, and lock context dependencies for AI agents"
readme = {file = "README.md", content-type = "text/markdown"}
Expand Down
2 changes: 1 addition & 1 deletion sdk/js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@raintree-technology/docpull-sdk",
"version": "6.5.4",
"version": "6.5.5",
"description": "TypeScript SDK for reading docpull context packs and running the docpull CLI",
"license": "MIT",
"type": "module",
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
"source": "github"
},
"websiteUrl": "https://github.com/raintree-technology/docpull",
"version": "6.5.4",
"version": "6.5.5",
"packages": [
{
"registryType": "pypi",
"identifier": "docpull",
"version": "6.5.4",
"version": "6.5.5",
"transport": {
"type": "stdio"
}
Expand Down
2 changes: 1 addition & 1 deletion src/docpull/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from .surface import PUBLIC_SDK_EXPORTS

__version__ = "6.5.4"
__version__ = "6.5.5"

_LAZY_EXPORTS: dict[str, tuple[str, str]] = {
**{
Expand Down
34 changes: 32 additions & 2 deletions src/docpull/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from pathlib import Path
from typing import Any, Literal, cast

from .. import __version__
from ..accounting import (
RunAccounting,
blocked_action,
Expand Down Expand Up @@ -89,6 +90,35 @@
)


def _advertise_tool_cost(tool: Any) -> Any:
"""Attach the cost contract promised by the server instructions."""
if tool.name == "render_url":
description = (
f"Cost: local, $0 by default; vercel and e2b runtimes are paid-capable. {tool.description}"
)
contract = {
"cost_class": "paid-capable",
"default_cost_class": "local",
"estimated_cost_usd": 0,
"paid_capable": True,
"paid_when": {"argument": "runtime", "values": ["vercel", "e2b"]},
"budget_argument": "budget",
}
else:
description = tool.description or ""
if not description.startswith("Cost:"):
description = f"Cost: local, $0. {description}"
contract = {
"cost_class": "local",
"estimated_cost_usd": 0,
"paid_capable": False,
}

metadata = dict(tool.meta or {})
metadata["docpull/cost"] = contract
return tool.model_copy(update={"description": description, "meta": metadata})


# Output schemas — keep these next to the tool list so they stay in sync.
# Tools that return free-form Markdown (fetch_url) intentionally omit a
# schema; the rest expose structured payloads alongside the rendered text.
Expand Down Expand Up @@ -1391,7 +1421,7 @@ async def _run_stdio() -> int:
return 1

server: Server = Server( # type: ignore[no-any-unimported]
"docpull", instructions=SERVER_INSTRUCTIONS
"docpull", version=__version__, instructions=SERVER_INSTRUCTIONS
)

@server.list_tools() # type: ignore[misc,no-untyped-call]
Expand Down Expand Up @@ -2470,7 +2500,7 @@ async def _list_tools() -> list[Tool]: # type: ignore[no-any-unimported]
outputSchema=_REMOVE_SOURCE_OUTPUT_SCHEMA,
),
]
return [tool for tool in tools if tool.name not in PRUNED_MCP_TOOLS]
return [_advertise_tool_cost(tool) for tool in tools if tool.name not in PRUNED_MCP_TOOLS]

async def _make_progress_callback() -> Any:
"""Return ``(pages_done, total_or_none) -> awaitable`` bound to the
Expand Down
4 changes: 2 additions & 2 deletions tests/test_ci_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ def test_plugin_launcher_reports_actionable_setup_when_docpull_is_missing(
)

assert proc.returncode == 127
assert "pipx install 'docpull[mcp]==6.5.4'" in proc.stderr
assert "pipx install 'docpull[mcp]==6.5.5'" in proc.stderr

(tmp_path / "docpull").mkdir()
proc = subprocess.run( # nosec B603
Expand All @@ -345,7 +345,7 @@ def test_plugin_launcher_reports_actionable_setup_when_docpull_is_missing(
)

assert proc.returncode == 127
assert "pipx install 'docpull[mcp]==6.5.4'" in proc.stderr
assert "pipx install 'docpull[mcp]==6.5.5'" in proc.stderr


def test_codex_plugin_default_prompt_fits_host_limit() -> None:
Expand Down
21 changes: 20 additions & 1 deletion tests/test_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from mcp.client.stdio import stdio_client

from docpull import __version__
from docpull.mcp import server as mcp_server
from mcp import ClientSession, StdioServerParameters
from tests.pack_fixtures import write_context_pack
Expand Down Expand Up @@ -185,10 +186,28 @@ async def test_stdio_server_lists_and_calls_tools(tmp_path):
env=env,
)
async with _stdio_client_with_cleanup(server) as (read, write), ClientSession(read, write) as session:
await session.initialize()
initialization = await session.initialize()
assert initialization.serverInfo.version == __version__

tools = await session.list_tools()
names = {tool.name for tool in tools.tools}
for tool in tools.tools:
assert tool.description is not None
assert tool.description.startswith("Cost:")
assert tool.meta is not None
assert "docpull/cost" in tool.meta
cost = tool.meta["docpull/cost"]
assert cost["paid_capable"] is (tool.name == "render_url")
assert cost["estimated_cost_usd"] == 0
render_cost = next(
tool.meta["docpull/cost"]
for tool in tools.tools
if tool.name == "render_url" and tool.meta is not None
)
assert render_cost["paid_when"] == {
"argument": "runtime",
"values": ["vercel", "e2b"],
}
fetch_tool = next(tool for tool in tools.tools if tool.name == "fetch_url")
assert fetch_tool.inputSchema["properties"]["remote_document_backend"]["enum"] == [
"auto",
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading