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
51 changes: 32 additions & 19 deletions .clusterfuzzlite/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ anyio==4.14.1 \
--hash=sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e
# via
# httpx
# httpx2
# mcp
# sse-starlette
# starlette
Expand Down Expand Up @@ -165,27 +166,31 @@ h11==0.16.0 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
# via
# httpcore
# httpcore2
# uvicorn
httpcore==1.0.9 \
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
# via httpx
httpcore2==2.9.1 \
--hash=sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2 \
--hash=sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26
# via httpx2
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via
# mcp
# recon-tool
httpx-sse==0.4.3 \
--hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \
--hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d
# via recon-tool
httpx2==2.9.1 \
--hash=sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a \
--hash=sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a
# via mcp
idna==3.18 \
--hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \
--hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848
# via
# anyio
# httpx
# httpx2
jsonschema==4.26.0 \
--hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \
--hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce
Expand All @@ -198,10 +203,14 @@ markdown-it-py==4.2.0 \
--hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \
--hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a
# via rich
mcp==1.28.1 \
--hash=sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df \
--hash=sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683
mcp==2.0.0 \
--hash=sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728 \
--hash=sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6
# via recon-tool
mcp-types==2.0.0 \
--hash=sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0 \
--hash=sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379
# via mcp
mdurl==0.1.2 \
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
Expand All @@ -210,6 +219,10 @@ networkx==3.6.1 \
--hash=sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509 \
--hash=sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762
# via recon-tool
opentelemetry-api==1.44.0 \
--hash=sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a \
--hash=sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef
# via mcp
publicsuffixlist==1.0.2.20260625 \
--hash=sha256:4cb45166fcb328a2894dfed949534e698f3a00f27f08a8d083ff7e05e6baf110 \
--hash=sha256:8123f244bb7f2163f2cc4ce42bad56218e6b65f23efffbab5dcd5b36fcc3ada1
Expand All @@ -223,7 +236,7 @@ pydantic==2.13.4 \
--hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6
# via
# mcp
# pydantic-settings
# mcp-types
pydantic-core==2.46.4 \
--hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \
--hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \
Expand Down Expand Up @@ -318,10 +331,6 @@ pydantic-core==2.46.4 \
--hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \
--hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff
# via pydantic
pydantic-settings==2.14.2 \
--hash=sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440 \
--hash=sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f
# via mcp
pygments==2.20.0 \
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
Expand All @@ -330,10 +339,6 @@ pyjwt==2.13.0 \
--hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \
--hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728
# via mcp
python-dotenv==1.2.2 \
--hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \
--hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3
# via pydantic-settings
python-multipart==0.0.32 \
--hash=sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e \
--hash=sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23
Expand Down Expand Up @@ -567,6 +572,12 @@ starlette==1.3.1 \
# via
# mcp
# sse-starlette
truststore==0.10.4 \
--hash=sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301 \
--hash=sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981
# via
# httpcore2
# httpx2
typer==0.26.8 \
--hash=sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c \
--hash=sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e
Expand All @@ -576,7 +587,10 @@ typing-extensions==4.15.0 \
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
# via
# anyio
# httpx2
# mcp
# mcp-types
# opentelemetry-api
# pydantic
# pydantic-core
# referencing
Expand All @@ -588,7 +602,6 @@ typing-inspection==0.4.2 \
# via
# mcp
# pydantic
# pydantic-settings
uvicorn==0.49.0 ; sys_platform != 'emscripten' \
--hash=sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f \
--hash=sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3
Expand Down
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,57 @@ operator, corporate group, ownership, or control.

## [Unreleased]

### Changed

- Production now runs the MCP v2 SDK, so recon serves the 2026-07-28 protocol
revision. This is what makes recon reachable from a modern-only client: the
spec's own compatibility matrix says a modern client against a legacy server
fails, and `server/discover`, per-request `_meta`, `resultType`, and cache
hints all come from the SDK. A v2 server still answers the legacy
`initialize` handshake, so existing clients on 2025-11-25 keep working, and
recon's compatibility gate verifies that rather than trusting it. 1.28.1
remains the documented rollback pin and stays blocking in the matrix, so both
generations keep being exercised.
- The optional remote adapter runs on both SDK generations instead of refusing
anything but v1. Two things had to move: the read-only allow-list read
`annotations.readOnlyHint`, which does not exist on v2, so every tool looked
non-read-only and the remote surface would have come up empty; and the
transport options moved from a mutable `settings` object to keyword arguments
on `streamable_http_app`. Both differences are resolved once in
`sdk_compat` rather than at the call sites.

### Fixed

- A Google CSE probe failure now stays an unavailable channel instead of
reporting no CSE configured, so a transport error cannot become a negative
observation.
- A degraded email channel can no longer become a weak-email-security posture
claim. The score counts observed controls, so an unread channel dragged it to
zero and fired the weak-posture observation off a collection gap. The same
rule exists separately on the signal path and both are now pinned.
- Parent-platform presence no longer becomes a child-product use claim through
a TXT verification token: the role-establishing record requirement is now
enforced for both the module and infrastructure insight paths.
- A dormant provider slug alone no longer produces an email security score. A
tenant discovered through identity endpoints with no mail configuration
rendered "Email security 0/5", which reads as configured but badly secured
rather than not measured.
- `chain_lookup` over MCP clamped depth with a literal `3` instead of the
shared `MAX_CHAIN_DEPTH`, so the two copies of that bound could drift apart
silently. Depth also scales the aggregate timeout.

### Changed

- The result-cache poison table asserted nothing. Every one of its twelve rows
wrote a payload for one domain into another domain's cache file, so
`cache_get` rejected on the domain binding before reaching the field under
test. Re-keying the rows made all twelve real and immediately caught a
range check that had never been exercised. A control row now fails if the
table goes vacuous again.
- `recon fingerprints`, posture, chain-depth, and Google CSE invariants that
were enforced in code but named by no test are now pinned by tests verified
to fail when the guard is removed.

## [2.8.0] - 2026-07-31

### Tool Surface Changes
Expand Down
9 changes: 5 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,15 @@ breaking protocol release, and the official Python SDK moves on its own
schedule regardless of recon. The compatibility work is bounded and remains
blocking in CI without displacing track 1.

**State:** the exact `1.28.1` and stable `2.0.0` matrix passed on 2026-07-28,
**State:** adopted on 2026-07-31. Production serves 2026-07-28 on the v2 SDK,
and CI keeps both pins blocking. The same registration and domain logic passes
legacy initialization and final stateless `server/discover` behavior.
legacy initialization and final stateless `server/discover` behavior, and the
optional remote adapter now runs on either generation.

**Closed when:** the stable matrix stays green; tool and resource order stays
deterministic; declared output schemas and structured results conform on both
generations; and the local stdio workflow remains intact. Production stays on
`mcp>=1.28.1,<2` until a separate adoption review changes it. The named
generations; and the local stdio workflow remains intact. Production runs
`mcp>=2.0.0,<3`; `1.28.1` remains the documented rollback pin. The named
optional remote-access need and its separate architecture review now live in
[the cloud deployment plan](docs/optional-cloud-deployment-plan.md); that work
does not imply production v2 adoption, OAuth, Roots, Sampling, Apps, or Tasks.
Expand Down
22 changes: 14 additions & 8 deletions docs/mcp-2026-07-28-readiness.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# MCP 2026-07-28 Readiness Plan

Status: final stable compatibility matrix complete; production adoption
decision pending
Status: adopted. Production serves MCP 2026-07-28 on the v2 SDK; 1.28.1
remains the rollback pin and stays blocking in the compatibility matrix
Review date: 2026-07-28

The Model Context Protocol 2026-07-28 specification and official Python SDK
Expand Down Expand Up @@ -151,12 +151,18 @@ supported-version list for an unsupported version, `-32602` rather than the
retired `-32002` for an unknown resource, and a `server/discover` payload
carrying instructions and server identity. No SDK nonconformance was found.

What blocks adoption. `build_remote_application` refuses any SDK family but v1
(`src/recon_tool/remote_server.py`), so moving the pin does not degrade the
optional remote adapter, it disables it. That adapter shipped in v2.7.0. The
pin change is therefore gated on porting the adapter to the v2 HTTP application
and settings API, or on an explicit decision to withdraw it. Nothing about the
local stdio default is blocked.
What blocked adoption, and how it was cleared. `build_remote_application`
refused any SDK family but v1, so moving the pin would have disabled the
optional remote adapter rather than degraded it. The adapter has since been
ported and the guard removed. Two differences had to move into `sdk_compat`:
the read-only allow-list read `annotations.readOnlyHint`, which does not exist
as an attribute on v2, so every tool looked non-read-only and the remote
surface would have come up empty; and the transport options moved from a
mutable `settings` object to keyword arguments on `streamable_http_app`.

Adoption landed on 2026-07-31. Both compatibility rows stay blocking: 24 of 24
checks pass on 2.0.0, and 17 pass with 7 correctly reported not-applicable on
1.28.1.

Two defects the review found and fixed under the v1 pin, because both are
era-independent:
Expand Down
20 changes: 10 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ dependencies = [
"dnspython>=2.6",
"pyyaml>=6.0",
"defusedxml>=0.7",
# 1.28.1 is the first stable SDK covered by recon's complete compatibility
# matrix. Earlier releases do not all expose the FastMCP API recon uses.
#
# v2 is the line that speaks MCP 2026-07-28, and recon runs green on it
# under the compatibility gate, but adopting it is blocked on the optional
# remote adapter: build_remote_application refuses any family but v1, so the
# pin change would disable a shipped feature rather than degrade it. The
# remaining adoption steps and their evidence are in
# docs/mcp-2026-07-28-readiness.md.
"mcp>=1.28.1,<2",
# v2 is the SDK line that speaks MCP 2026-07-28. Serving that revision is
# what makes recon reachable from a modern-only client at all: the spec's
# compatibility matrix says modern client plus legacy server fails, and
# server/discover, per-request _meta, resultType, and cache hints are all
# SDK-provided. A v2 server still answers the legacy initialize handshake,
# so clients on 2025-11-25 keep working; recon's own gate verifies that
# rather than trusting it. 1.28.1 stays the documented rollback pin and
# stays blocking in the compatibility matrix, so both generations remain
# exercised. See docs/mcp-2026-07-28-readiness.md.
"mcp>=2.0.0,<3",
"networkx>=3.0",
# Public Suffix List for registrable-domain (apex) reduction, so a pasted
# browser URL or sub-host (mail.example.co.uk) is analyzed at the apex
Expand Down
18 changes: 13 additions & 5 deletions scripts/generate_surface_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import click
import typer

from recon_tool.mcp_client.sdk_compat import model_wire_dict, tool_schemas

_ROOT = Path(__file__).resolve().parent.parent
_DEFAULT_OUTPUT = _ROOT / "docs" / "surface-inventory.json"
_DEFAULT_PACKAGED_OUTPUT = _ROOT / "src" / "recon_tool" / "data" / "surface-inventory.json"
Expand Down Expand Up @@ -267,23 +269,29 @@ async def _mcp_inventory_async() -> dict[str, object]:
for tool in sorted(tools, key=lambda item: item.name):
annotations = {}
if tool.annotations is not None:
annotations = tool.annotations.model_dump(mode="json", exclude_none=True)
# by_alias keeps the protocol's camelCase spelling. The two SDK
# generations name these fields differently, and a bare dump would
# silently rewrite the generated inventory to the other spelling.
annotations = tool.annotations.model_dump(mode="json", by_alias=True, exclude_none=True)
tool_input_schema, tool_output_schema = tool_schemas(tool)
tool_entries.append(
{
"name": tool.name,
"summary": _summary(tool.description),
"annotations": annotations,
"structured_output": tool.outputSchema is not None,
"input_parameters": _schema_parameters(tool.inputSchema),
"output_schema": _schema_outline(tool.outputSchema),
"structured_output": tool_output_schema is not None,
"input_parameters": _schema_parameters(tool_input_schema),
"output_schema": _schema_outline(tool_output_schema),
}
)
# Read through the wire dictionary: the SDK generations spell mimeType and
# mime_type differently and only the protocol form exists on both.
resource_entries = [
{
"uri": str(resource.uri),
"name": resource.name,
"summary": _summary(resource.description),
"mime_type": resource.mimeType,
"mime_type": model_wire_dict(resource).get("mimeType"),
}
for resource in sorted(resources, key=lambda item: str(item.uri))
]
Expand Down
Loading
Loading