From dc4af8c8b8d043788e6ab35f29141fd3440d51e0 Mon Sep 17 00:00:00 2001 From: admin-raintree <277948009+admin-raintree@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:53:50 -0700 Subject: [PATCH] fix(mcp): advertise version and tool costs --- bench/uv.lock | 2 +- docs/CHANGELOG.md | 6 +++++ plugin/.claude-plugin/plugin.json | 2 +- plugin/.codex-plugin/plugin.json | 2 +- plugin/README.md | 4 ++-- plugin/scripts/launch-docpull-mcp.mjs | 2 +- pyproject.toml | 2 +- sdk/js/package.json | 2 +- server.json | 4 ++-- src/docpull/__init__.py | 2 +- src/docpull/mcp/server.py | 34 +++++++++++++++++++++++++-- tests/test_ci_policy.py | 4 ++-- tests/test_mcp_server.py | 21 ++++++++++++++++- uv.lock | 2 +- 14 files changed, 72 insertions(+), 17 deletions(-) diff --git a/bench/uv.lock b/bench/uv.lock index fca6c8c..9838ede 100644 --- a/bench/uv.lock +++ b/bench/uv.lock @@ -848,7 +848,7 @@ wheels = [ [[package]] name = "docpull" -version = "6.5.4" +version = "6.5.5" source = { editable = "../" } dependencies = [ { name = "aiohttp" }, diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7f55125..c98ac1b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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 diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index d3681cb..e504821 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -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", diff --git a/plugin/.codex-plugin/plugin.json b/plugin/.codex-plugin/plugin.json index bfc5641..0c78004 100644 --- a/plugin/.codex-plugin/plugin.json +++ b/plugin/.codex-plugin/plugin.json @@ -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", diff --git a/plugin/README.md b/plugin/README.md index 2f21129..ecd5cb9 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -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 ``` diff --git a/plugin/scripts/launch-docpull-mcp.mjs b/plugin/scripts/launch-docpull-mcp.mjs index eb452c1..76a3580 100644 --- a/plugin/scripts/launch-docpull-mcp.mjs +++ b/plugin/scripts/launch-docpull-mcp.mjs @@ -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; diff --git a/pyproject.toml b/pyproject.toml index de7a19d..5033d5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"} diff --git a/sdk/js/package.json b/sdk/js/package.json index f4824cc..535cd29 100644 --- a/sdk/js/package.json +++ b/sdk/js/package.json @@ -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", diff --git a/server.json b/server.json index 30b186a..94085c0 100644 --- a/server.json +++ b/server.json @@ -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" } diff --git a/src/docpull/__init__.py b/src/docpull/__init__.py index a250f46..a4b9496 100644 --- a/src/docpull/__init__.py +++ b/src/docpull/__init__.py @@ -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]] = { **{ diff --git a/src/docpull/mcp/server.py b/src/docpull/mcp/server.py index d748ba5..0aa204b 100644 --- a/src/docpull/mcp/server.py +++ b/src/docpull/mcp/server.py @@ -47,6 +47,7 @@ from pathlib import Path from typing import Any, Literal, cast +from .. import __version__ from ..accounting import ( RunAccounting, blocked_action, @@ -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. @@ -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] @@ -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 diff --git a/tests/test_ci_policy.py b/tests/test_ci_policy.py index ae62219..0c02169 100644 --- a/tests/test_ci_policy.py +++ b/tests/test_ci_policy.py @@ -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 @@ -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: diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index eafffbe..76b8e46 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -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 @@ -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", diff --git a/uv.lock b/uv.lock index 3362f5b..71e2319 100644 --- a/uv.lock +++ b/uv.lock @@ -1305,7 +1305,7 @@ wheels = [ [[package]] name = "docpull" -version = "6.5.4" +version = "6.5.5" source = { editable = "." } dependencies = [ { name = "aiohttp" },