Skip to content

Commit e1b2376

Browse files
clarityzachclaude
andcommitted
chore(release): bump 2.4.0 → 2.5.0
Cuts the 2.5.0 release. CHANGELOG entry covers the MCP-server hardening that landed in c087ccd + cf3e217: - 3 HIGH security holes closed: SSRF in fetch_url, path traversal in grep_docs/read_doc via library, ReDoS in grep_docs - isError flag was being silently dropped; now propagates correctly via CallToolResult (was a real bug — every tool error was being reported as success) - 3 new tools: read_doc (follow up a grep_docs hit without filesystem access), add_source / remove_source (programmatic user-registry management) - ToolAnnotations on every tool (Anthropic Directory submission unblocker) and Server instructions - Progress notifications wired into ensure_docs - Structured output (outputSchema + structuredContent) on the 7 tools that carry data - Robustness: atomic meta writes, partial-fetch flag, _cache_fresh empty-dir check, typed input validation, YAML warnings Verification: 316/316 tests pass (39 new); ruff clean; end-to-end MCP smoke through create_connected_server_and_client_session confirms isError + structuredContent both flow through the SDK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cf3e217 commit e1b2376

3 files changed

Lines changed: 98 additions & 2 deletions

File tree

docs/CHANGELOG.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,102 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.5.0] - 2026-04-25
9+
10+
A focused MCP-server hardening pass. Closed three exploitable security
11+
holes in the agent-facing tools, added the missing `ToolAnnotations`
12+
that gate Anthropic Directory submission, exposed structured output
13+
alongside the rendered text on every tool that carries data, and
14+
added the three tools an agent obviously wants — `read_doc` to follow
15+
up a `grep_docs` hit, plus `add_source` / `remove_source` to manage
16+
the user registry programmatically.
17+
18+
### Added
19+
- **`read_doc(library, path, line_start?, line_end?)`** — read a
20+
Markdown file from a fetched library, optionally line-sliced. The
21+
natural follow-up after `grep_docs` returns a hit; agents no longer
22+
need filesystem access for surrounding context. Path is resolved
23+
and confirmed to stay under the library root.
24+
- **`add_source(name, url, ...)`** — add or update a user source
25+
alias in the writable `sources.yaml`. Refuses to shadow a builtin
26+
alias unless `force=true`; URL is HTTPS-only and validated against
27+
the same SSRF rules as `fetch_url`. Atomic write (tmp + rename).
28+
- **`remove_source(name, delete_cache?)`** — remove a user source
29+
alias and optionally its cached docs directory. Cannot remove
30+
builtins (suggest `add_source(force=true)` to shadow instead).
31+
Cache deletion does a defense-in-depth resolved-path check.
32+
- **`ToolAnnotations` on every tool**`readOnlyHint` /
33+
`destructiveHint` / `idempotentHint` / `openWorldHint` / `title`.
34+
Required for Anthropic Directory submission and unlocks host
35+
auto-approve for the four read-only tools.
36+
- **Server `instructions`** — system-prompt hint telling agents the
37+
call ordering (list_sources → ensure_docs → grep_docs → read_doc).
38+
- **Progress notifications**`ensure_docs` forwards
39+
`FETCH_COMPLETED` events as MCP progress to clients that supplied
40+
a `progressToken` on the call.
41+
- **Structured output** (`outputSchema` + `structuredContent`) on
42+
`list_sources`, `list_indexed`, `grep_docs`, `read_doc`,
43+
`ensure_docs`, `add_source`, `remove_source`. Clients that consume
44+
`structuredContent` get parseable JSON; clients that don't still
45+
see the rendered Markdown text.
46+
47+
### Fixed
48+
- **SSRF in `fetch_url`** — schema previously accepted any string
49+
with no scheme/host enforcement. An agent could request
50+
`http://169.254.169.254/`, `http://localhost`, `file:///etc/passwd`,
51+
etc. Now validated upfront with the same `UrlValidator`
52+
(HTTPS-only, no localhost / private / link-local IPs) the crawler
53+
uses, instead of relying on the slow pipeline error path.
54+
- **Path traversal in `grep_docs` / `read_doc` via `library`**
55+
`docs_dir / library` did not validate `library`, so
56+
`library="../../etc"` walked anywhere the process could read.
57+
`read_doc`'s `path` arg was similarly unchecked. Both now reject
58+
unsafe names (`is_safe_library_name`) and `read_doc` additionally
59+
resolves the joined path and confirms it stays under the library
60+
root.
61+
- **ReDoS in `grep_docs`** — pattern was compiled with no length
62+
cap and run line-by-line over every cached `.md`; Python `re`
63+
has no timeout knob. Now cap pattern length at 1000 chars and
64+
apply a 10s wall-clock budget across files.
65+
- **`isError` flag was being silently dropped** — the previous
66+
`_call_tool` returned a bare `list[TextContent]`, which the SDK's
67+
legacy path hardcodes as `isError=False` regardless of what the
68+
handler intended. Every error your tools raised was being reported
69+
to clients as success. Now `_call_tool` returns `CallToolResult`
70+
directly so `isError` propagates correctly.
71+
- **`ensure_docs` partial-fetch detection** — a crash mid-crawl
72+
used to leave files on disk with no meta, and the next call
73+
would re-fetch (correct, but wasteful) or — if a stale meta from
74+
a prior run was present — trust the half-fetched cache. Meta
75+
writes are now atomic (tmp + rename) and a `partial=true` flag
76+
marks half-fetches so `_cache_fresh` treats them as stale.
77+
- **`grep_docs` honors `context > 1`** — the schema advertised
78+
`maximum: 3` but the implementation only ever rendered one line
79+
either side. Now renders up to `context` lines on each side.
80+
- **`load_user_sources` silently swallowed YAML errors** — a typo
81+
in the user's `sources.yaml` produced "Unknown source" instead of
82+
surfacing the parse failure. Now logs a warning at WARNING level.
83+
84+
### Changed
85+
- **Tighter input validation** in the MCP `_call_tool` dispatcher:
86+
required strings checked with `_require_str`, ints coerced with
87+
`_coerce_int`. Errors that used to surface as ugly
88+
"invalid literal for int(...)" now return clear messages naming
89+
the bad argument.
90+
- **Tighter input schemas**: `https://` pattern on `fetch_url.url`,
91+
`enum` on `category`, regex + `maxLength` on `library` everywhere
92+
it appears, `maxLength: 1000` on `grep_docs.pattern`, integer
93+
bounds on `max_tokens` / `max_pages`.
94+
- `_cache_fresh` now also requires the source directory to contain
95+
at least one `.md` file — a manually-`rm -rf`'d cache no longer
96+
reports as fresh.
97+
- `_PROFILE_ALIASES` mapping deleted; `_resolve_profile` now goes
98+
through `ProfileName` directly, eliminating drift.
99+
- 39 new MCP tests (61 total in `test_mcp_tools.py`, 316 in the
100+
full suite). Coverage includes SSRF rejection, path traversal,
101+
oversized regex, partial-meta freshness, structured payloads,
102+
and the new write tools.
103+
8104
## [2.4.0] - 2026-04-26
9105

10106
A two-pass cleanup. The first pass closed every claim the code didn't back

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "docpull"
7-
version = "2.4.0"
7+
version = "2.5.0"
88
dynamic = []
99
description = "Pull documentation from the web and convert to clean markdown"
1010
readme = {file = "README.md", content-type = "text/markdown"}

src/docpull/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
print(event)
1515
"""
1616

17-
__version__ = "2.4.0"
17+
__version__ = "2.5.0"
1818

1919
from .cache import CacheManager, StreamingDeduplicator
2020
from .conversion.chunking import Chunk, TokenCounter, chunk_markdown

0 commit comments

Comments
 (0)