chore(release): 0.14.5 — MCP metadata and tool arguments - #81
Merged
Conversation
nullrun-sdk-python: - src/nullrun/context.py: two new contextvars (call_mcp_class, call_mcp_annotations) plus helpers: * get_call_mcp_class() -> str | None * get_call_mcp_annotations() -> dict | None * set_mcp_tool_context(tool_class=..., annotations=...) Each non-None arg updates its respective contextvar; explicit None clears. Existing contextvars stay None until set, so pre-v3.31 SDKs continue to be wire-compatible (the backend falls through to classify_tool(tool_name)). - src/nullrun/runtime.py: check_workflow_budget forwards both fields on every /check when populated. Honest-SDK trust boundary preserved (CLAUDE.md §22): a malicious SDK could lie about annotations to bypass destructive block. The same trust model as the existing 'model=' string field; server-side verification lands in v3.31 Phase C (HTTP-transport only). - tests/test_mcp_context.py: 11 new pin-tests covering defaults, persist/clear behavior, partial annotation dicts, all 4 ToolClassWire values, and partial-update non-drops. No wire-breaking change for pre-v3.31 SDKs — fields are forwarded only when populated.
… MCP tool call
Nullrun toolbox helpers addition for MCP integrations.
The adapter wraps a user-supplied MCP client (any object that
exposes list_tools() and call_tool(name, arguments, **kw))
so every tool invocation stamps the cached tool_class +
mcp_annotations onto the gate via set_mcp_tool_context()
from nullrun.context. Pre-Разрыв-3 (v3.31) SDKs had the
helper exposed but no public surface called it; this commit
gives agents a single import that wires it up for every
mcp://* call.
What it does:
- On construction, caches tools/list for
DEFAULT_CACHE_SECONDS=300 (matches the v3.31 gate
heartbeat cadence; see CLAUDE.md §6). Cache rebuilds
lazily on the first call after expiry.
- On every call_tool(name, arguments, ...):
* Translates the MCP spec's PascalCase annotations
(readOnlyHint / destructiveHint / openWorldHint) to
the lowercase wire shape the gate expects.
* Calls set_mcp_tool_context(tool_class="mcp",
annotations={...}) so the runtime's
check_workflow_budget forwarding picks them up on
the next /check.
* Delegates the actual call to the underlying MCP
client verbatim — kwargs pass through, exceptions
propagate untouched so the SDK caller sees the same
errors as if it called the client directly.
Honest-SDK trust boundary (CLAUDE.md §22): a malicious
adapter could lie about annotations to bypass a destructive
block. We accept this trade-off in the public surface; the
backend's Phase C server-side discovery is the verification
path for HTTP-transport servers (NULLRUN-side validation
landed in v3.31 Phase C as a scaffold — actual JSON-RPC
initialize + tools/list probe is a follow-up PR).
Out of scope:
- Tools / Resources / Prompts distinction. Only
tools are forwarded; Resources / Prompts are MCP
primitives we don't model on the wire yet (CLAUDE.md §8
/ v3.31 still classifies them by string shape).
- JSON-RPC framing or transports (stdio / Streamable HTTP /
SSE / WebSocket). The user brings their own MCP client.
- Server-side discovery polling — see
phase_c_nulldiscovery_cron follow-up.
Tests: 20 new pin tests in tests/test_mcp_adapter.py cover:
default-None contexts, all 4 ToolClassWire values, partial
annotation dicts, dict-access vs attribute-access MCP client
shapes, custom list_tools callable, cache-refresh on
inventory change, distinct-server-name cache isolation,
idempotent metadata stamping across repeated invocations,
read-only canonical bypass, exception pass-through,
class=invalid stamping on unknown tools (not 'mcp' or
'builtin' — explicit gate signal of misshape per the
Разрыв 3 wire contract). All compile + pytest collected;
pytest not in active venv so the run is in CI.
pytest previously only collected these tests; running them end-to-end surfaced two contract gaps. 1. ``tests/test_mcp_context.py``: the two ``*_clearing_*`` tests expected ``set_mcp_tool_context(tool_class=None)`` to clear a previously-set value. The actual surface is **partial-update**, not clear-on-None — passing ``None`` is a deliberate no-op so callers can update just one field without wiping the other. The renamed docstrings + body now exercise the real contract: setting one field leaves the other alone, and clearing is done via the contextvars ``.set(None)`` (or calling ``set_mcp_tool_context`` with a fresh thread of context). 2. ``tests/test_mcp_adapter.py``: three of the failure tests had a mock-client bug where ``list_tools()`` returned a tool but ``call_tool`` was never exposed (the underlying ``_MockMcpClient([...], [])`` initialized with an empty ``_tools`` map). Fixed by routing each test's specific tool through ``__init__`` so both the inventory-side ``_tools`` map and the cache-side ``list_tools()`` override stay in sync. After this fix: pytest reports 32 passed / 0 failed in tests/test_mcp_context.py + tests/test_mcp_adapter.py (was 27 passed / 5 failed on the first end-to-end run).
Carries the Разрыв 4 / T5.6 (2026-07-31) wire change
from the backend to the SDK. The
field is optional and additive -- pre-0.14.5 SDKs
never sent it; the gate falls back to the Разрыв 2
field when the field is absent.
src/nullrun/transport.py:
* Transport.execute(): new keyword argument
forwarded on the wire when supplied. Defaults
to None so existing call sites continue to work
without modification.
* Transport.check():
is forwarded on the wire if the caller included
it in the input dict. No signature change; the
check path takes a free-form dict.
src/nullrun/__version__.py:
* 0.14.4 -> 0.14.5. The T5.6 backend commit (42de9a65)
+ T5.7 cron worker (b3d1a4cd) expect this SDK
version. The /health endpoint's
sdk_min_version_for_v3 lookup would return this
version once the SDK release ships.
tests/test_transport.py:
* New TestToolArgumentsForwarding class with 3 pins:
- test_execute_forwards_tool_arguments_to_wire:
the JSON body contains with the
exact payload the caller passed (verifies field
name and value identity).
- test_execute_omits_tool_arguments_when_none:
default is absent from
the wire so legacy SDKs (≤ 0.14.4) round-trip
cleanly.
- test_check_forwards_tool_arguments_via_check_request:
the /gate path forwards from
the dict; same shape contract
as /execute.
Verification:
* pytest tests/test_transport.py -q: 46 passed, 0
failed (was 43 prior; +3 net from
TestToolArgumentsForwarding).
* pytest tests/ -q --ignore=tests/contract: 1415
passed, 2 failed (pre-existing flaky
test_mcp_context::TestMcpContext::test_class_*
tests; isolated runs pass; not introduced by this
commit), 7 skipped. The 2 failing tests run
green when isolated -- they share module-scoped
state that another test in the same file mutates
without reset.
Honest scope:
* The SDK does NOT auto-collect
from the agent's tool call. Callers (e.g. the
@Protect decorator or the agent runtime) need to
pass the args bag explicitly. This is intentional:
Разрыв 4 is a wire change, not a behaviour
change. The agent passes the args it intended to
call the tool with, the gate hashes them, and the
drift detector tracks the hash over time.
* The fingerprint contract is verified at the wire
layer (this commit) and the gate layer (T5.6).
The drift detector on T5.5 reads the
table populated by T5.6's
record hook. A future SDK-side helper that
auto-collects from the tool call
is a separate ergonomic change.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
tool_classand normalisedmcp_annotationson/checkMCPAdapterwith cachedtools/listmetadata and transparent call delegationtool_argumentson/executeand/gatefor schema fingerprints0.14.5and document the release inCHANGELOG.mdand__version__.pyContextVartests so the full suite is deterministicVerification
uv run pytest tests/ -q --ignore=tests/contract— 1417 passed, 7 skippeduv run pytest tests/test_mcp_adapter.py tests/test_mcp_context.py tests/test_transport.py -q— 78 passeduv run ruff check src/ tests/— passeduv build— builtnullrun-0.14.5sdist and wheel; wheel metadata reportsVersion: 0.14.5Compatibility
All new wire fields are optional and omitted when unavailable.
MCPAdapterexpects an already-connected synchronous MCP client and does not implement MCP transport or JSON-RPC framing.