Skip to content

chore(release): 0.14.5 — MCP metadata and tool arguments - #81

Merged
maltsev-dev merged 6 commits into
masterfrom
release/0.14.5-mcp-tool-arguments
Aug 1, 2026
Merged

chore(release): 0.14.5 — MCP metadata and tool arguments#81
maltsev-dev merged 6 commits into
masterfrom
release/0.14.5-mcp-tool-arguments

Conversation

@maltsev-dev

Copy link
Copy Markdown
Member

Summary

  • forward MCP tool_class and normalised mcp_annotations on /check
  • add MCPAdapter with cached tools/list metadata and transparent call delegation
  • forward optional tool_arguments on /execute and /gate for schema fingerprints
  • bump package version to 0.14.5 and document the release in CHANGELOG.md and __version__.py
  • isolate MCP ContextVar tests so the full suite is deterministic

Verification

  • uv run pytest tests/ -q --ignore=tests/contract — 1417 passed, 7 skipped
  • uv run pytest tests/test_mcp_adapter.py tests/test_mcp_context.py tests/test_transport.py -q — 78 passed
  • uv run ruff check src/ tests/ — passed
  • uv build — built nullrun-0.14.5 sdist and wheel; wheel metadata reports Version: 0.14.5

Compatibility

All new wire fields are optional and omitted when unavailable. MCPAdapter expects an already-connected synchronous MCP client and does not implement MCP transport or JSON-RPC framing.

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

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.99099% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/nullrun/toolbox/mcp.py 93.25% 5 Missing and 1 partial ⚠️
src/nullrun/runtime.py 33.33% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@maltsev-dev
maltsev-dev merged commit 4878402 into master Aug 1, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant