Metadata-Version: 2.4
Name: toolsplice
Version: 0.0.1
Summary: Framework-independent checked tool handoffs and offline fixtures for Python
License-Expression: MIT
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: jsonschema<5,>=4.23
Requires-Dist: referencing<1,>=0.30
Requires-Dist: google-re2<2,>=1.1
Requires-Dist: rfc8785<1,>=0.1
Provides-Extra: mcp
Requires-Dist: mcp<2,>=1.20; extra == "mcp"
Provides-Extra: postgres
Requires-Dist: psycopg[binary,pool]<4,>=3.2; extra == "postgres"
Provides-Extra: http
Requires-Dist: httpx<1,>=0.27; extra == "http"
Provides-Extra: test
Requires-Dist: pytest<10,>=8; extra == "test"
Requires-Dist: pytest-asyncio<2,>=0.24; extra == "test"
Requires-Dist: build<2,>=1; extra == "test"
Requires-Dist: mypy<2,>=1.11; extra == "test"
Dynamic: license-file

# Toolsplice for Python

Native, framework-independent SDK for checked tool handoffs, learning, typed bindings and offline maintenance. **0.0.1**, Python **3.11+**. No Node, Agno, model-provider or Cloudflare runtime dependency.

This release implements the JavaScript SDK's feature categories. Portable contracts, corrections, shared recordings and learning records use JS wire formats/fingerprints. Generated Python code and project adapters are language-specific. Tests establish the represented cases, not universal equivalence or production readiness.

## Install

```sh
python -m pip install toolsplice==0.0.1
python -m pip install 'toolsplice[mcp]==0.0.1'       # SDK adapter and proxy transports
python -m pip install 'toolsplice[postgres]==0.0.1'  # Optional database backend
python -m pip install 'toolsplice[http]==0.0.1'      # Optional HTTPX middleware
```

For local development, install the absolute checkout path to `packages/python`. Core dependencies: jsonschema, referencing, google-re2 and rfc8785, plus transitive dependencies. RE2/rpds-py use native platform components.

## Handoffs

```python
from toolsplice import ToolContract, prepare_splice

source = ToolContract(
    "docs",
    "fetch",
    {"type": "object"},
    {
        "type": "object",
        "properties": {"markdown": {"type": "string"}},
        "required": ["markdown"],
    },
)
target = ToolContract(
    "files",
    "save",
    {
        "type": "object",
        "properties": {"path": {"type": "string"}, "content": {"type": "string"}},
        "required": ["path", "content"],
    },
    {"type": "boolean"},
)
plan = {
    "format": "toolsplice-splice",
    "formatVersion": 1,
    "source": {"server": "docs", "tool": "fetch", "path": "/markdown"},
    "target": {"server": "files", "tool": "save"},
    "contextSchema": {
        "type": "object",
        "properties": {"path": {"type": "string"}},
        "required": ["path"],
    },
    "bindings": [
        {"to": "/content", "from": ""},
        {"to": "/path", "fromContext": "/path"},
    ],
}
handoff = prepare_splice(plan, source, target)
print(handoff.report)  # ready / review / blocked; assumptions and fingerprints

# Application supplies async source_caller(request) and target_caller(request).
# request: {"name": "...", "arguments": {...}}
# Return {"structuredContent": json_value}, or an MCP content/result envelope.


async def save_document(url: str) -> dict:
    """Fetch and save a document; register this ordinary function in any framework."""
    results = await handoff.run(
        source_caller,
        target_caller,
        {"url": url},
        context={"path": "application-owned-destination.md"},
        allow_review=True,
        timeout=30,
    )
    return {"saved": len(results)}
```

`prepare_splice` also accepts shared `callshape-contract` snapshots. `create_contract_from_catalog(server, catalog, tools=[...])` builds them from saved catalogs; applications own pagination/identity. `ToolContract.from_catalog` selects one tool.

Static preflight conservatively checks required/optional/missing paths, nullability, unions, local references, mapped argument types, literals and bounds. `allow_review` permits review only; blocked setup and runtime guards remain enforced. Consumer `source.valueSchema` is an additional requirement, never an implicit replacement for the server declaration.

`map_output(decoded_fixture, context=...)` checks without tools. `run` validates source input/context before the source call, known source output, `source.expect`, selection/optional JSON parsing, consumer schema, fanout cap, every destination argument and `target.uniqueBy` before any destination dispatch. Destination calls are sequential; returned outputs and aggregate result bytes are checked afterward. Plans retain shared v1 camelCase fields; Python APIs use snake_case.

## Execution and MCP

```python
from toolsplice.mcp import mcp_caller

source_caller = mcp_caller(source_session)
target_caller = mcp_caller(target_session)
```

Sessions are initialized/closed by the application. Core imports do not import MCP. Helpers include `create_tool_caller`, `invoke_tool`, `tool_functions`, `BindingCallOptions`, and `create_contract_caller`. Guards preserve successful raw results and leave normal MCP error/unfinished-result handling to the caller. Unknown tools reject before dispatch.

`run(timeout=...)` bounds the awaited workflow; `call_timeout` defaults to 120 seconds per call. Request-scoped `on_progress` reaches the SDK adapter. Normalize multiple internal progress streams in the application when exposing one outer tool. Synchronous work/schema checks cannot be preempted by asyncio.

`SpliceError` exposes stage, completed_targets and path-only issues; `explain_error` adds endpoints, row/binding origins and source selection. `SpliceCancelled` is an asyncio cancellation with stage/count. Counts mean acknowledged/collected results, not committed writes. Effects can occur before cancellation, timeout, output or size rejection. No retry, rollback, exactly-once execution or durable resume is promised.

## Shared recording, replay and corrections

```python
from toolsplice import create_splice_recorder, replay_splice_recording

recorder = create_splice_recorder(
    plan, source, target, capture="payloads", redact_keys=["customerEmail"]
)
await recorder.run(
    source_caller,
    target_caller,
    {"url": url},
    context={"path": path},
    allow_review=True,
)
recording = recorder.snapshot()
report = await replay_splice_recording(recording)
upgrade = await replay_splice_recording(recording, target_contract=candidate_target)
```

The factory creates **`toolsplice-recording` v1**, shared with JS. Replay verifies artifact/plan/contract fingerprints, exact saved call order/arguments and expected outcome. Candidate checks report passed/failed/inconclusive plus diffs/fingerprints. Failed/cancelled/transport-failed baselines cannot establish upgrade success. Replay accepts no live callers; it checks captured data, not live behavior or effects. Cancellation/timeout timing is not reproduced.

Metadata-only capture is the default; payloads are explicit and bounded. Common credential keys and configured keys/secret substrings are redacted with exact paths. Every redacted path needs a private restoration; extra paths are rejected. Redaction is not anonymization and fingerprints are not authentication. Capture failures never retry/change forwarding; snapshot/storage remain separately fallible. Applications explicitly choose where to persist recordings. Preview `Recorder`/`replay` remain for v0.1 native artifacts; use the factory above for shared artifacts. `replay` dispatches shared artifacts too.

`parse_contract_correction`/`apply_contract_correction` validate shared whole-schema replacements pinned to the original fingerprint, reason and positive/negative fixtures per changed tool/side. Originals remain intact, corrected metadata is fingerprinted and outputs labelled corrected. No correction automatically activates after guard failure.

## Typed code and projects

`emit_splice_module` generates an executable Python adapter with `create_splice`; `emit_bindings`/`emit_module` generate callable tool methods; `schema_to_python` emits declarations. TypedDict inputs/outputs and required context support static checking. Untrusted names and JSON literals are quoted safely. Python typing differs from TypeScript; runtime guards remain authoritative.

`initialize_splice_project` creates a new private fixed-file directory from synthetic data or a successful shared recording. `check_splice_project` validates inputs/fixtures/mappings and adapter freshness without calls. Existing directories/files are refused. Python projects use `adapter.py`; JS projects use `adapter.ts`; generated-language files/freshness checks are not interchangeable. Restored project fixtures can contain confidential values while the retained recording remains redacted.

## Learning and storage

```python
from toolsplice import ShapeStore, record_tool_list, record_call, extract

store = ShapeStore("/application-owned-volume/toolsplice")


def update(record):
    record_tool_list(record, catalog["tools"])
    record_call(record, "fetch", extract(raw_result))


store.transact("installation-account-name", update)
```

File operations are synchronous; `LearningStore` permits sync/async backends and `toolsplice.store.await_maybe` supports both. New directories/files use 0700/0600. Atomic replacement, shared writer locks and stale-save checks protect updates. Synchronous mutators are never retried after entry. Existing locks are not automatically reclaimed. Refresh/remove respect another process's forget; diagnostics inspect lock/temp ownership without returned values. Close watchers when finished.

```python
from toolsplice.postgres import PostgresShapeStore

store = PostgresShapeStore(database_url, namespace="project-tenant", migrate=False)
await store.transact("installation-account-name", update)
await store.close()
```

The optional async PostgreSQL backend shares JS `public.mcp_shape_*` tables, migration v1 and advisory-lock identities, including absent-row/delete locks. `migrate=True` bootstraps; use False with a restricted runtime role after migration. Namespaces partition records, not authorization. No cache/file fallback or callback retry follows a DB failure. The store owns its pool unless explicitly supplied by the application.

Learning includes bounded statistics, safe/all/none examples (default **none**), sensitive-key suppression, embedded JSON, maps/recursive trees, merges, drift revisions, structural declaration diagnostics, doctor reports and export freshness. Field names/counts/nonsensitive numeric bounds can persist under none; it is not anonymization. `create_contract_snapshot` exports learned portable contracts.

## Proxy and HTTP

`create_proxy(initialized_session, store=store, server_name=...)` gives list_tools/call_tool methods. Description/none modes preserve results; learning failures stay fail-open. Notifications are coalesced; ordinary repeated observations do not churn descriptions. Experimental `enrich="schema"` can advertise lenient schemas and attach compatible structured content when explicitly requested.

For transparent wire forwarding, pass `(upstream_read, upstream_write)` and call `proxy.run(downstream_read, downstream_write)`. Non-tool MCP requests, resources and prompts pass through too. The CLI supports stdio/Streamable HTTP upstreams. Cancellation is forwarded; deadline failure reports uncertainty and suppresses late duplicate results. `proxy.close()` does not close application-owned stores/sessions.

`toolsplice.http.LearningTransport` wraps HTTPX with explicit client/endpoint/principal identity. It observes JSON/SSE without a second fetch; bounded finite JSON catalogs can be enriched. Late/oversized bodies pass through, SSE bytes remain unchanged, and `on_changed` is a local cache-refresh signal. SDKs retain authentication and protocol negotiation. This supplies the JS v2 fetch-adapter role without requiring a Python SDK 2.x.

## CLI

```sh
python -m toolsplice init ./new-handoff
python -m toolsplice project ./new-handoff --allow-review --json
python -m toolsplice replay recording.json --target-contract next.json --json
python -m toolsplice proxy --store ./private-learning --name installation -- python server.py
```

Installed binaries: toolsplice, callshape, mcp-shape. Commands: splice, replay, correct, init/project, proxy, config wrapping, explicit/read-only-hint probe, doctor, show/list, schema, snapshot/check, forget, storage diagnostics and **emit-py**. Artifact outputs refuse existing files. Exit 2 denotes blocked/unaccepted review, regression or candidate incompatibility; invalid configuration/artifacts exit 1. MCP stdout stays protocol-only in proxy mode.

`--store` selects a directory (default `.mcp-shape` relative to cwd). `--database-env NAME`, `--namespace`, `--no-migrate`, `--database-timeout` select/control PostgreSQL. Artifact commands supplied explicit contracts/catalogs do not initialize a store or contact providers. `--source-contract`, `--target-contract`, catalog equivalents, `--fixture`, `--context`, `--summary`, `--allow-review` and `--out` support offline handoff checks/generation.

## Bounds and validation

Runtime guards support Draft 7 and 2020-12 (default), local references/recursive types, restricted ASCII RE2 patterns, unions/conditionals/tuples/dependencies and supported unevaluated assertions. External/dynamic refs, nested resources/dialects, patternProperties, ambiguous Draft 7 ref siblings and unknown assertions reject setup. Root $id is an annotation; formats/content annotations are not assertions. uniqueItems requires maxItems<=1000, matching JS. No schema is downloaded.

Shared JSON is finite, UTF-8 encodable, string-keyed and uses safe integers. Budgets reject cycles/oversized/deep data. Python integers are not silently rounded into JS numbers. No semantic matching, factual correctness, token savings, production availability, independent adoption or universal schema equivalence follows from passing tests.

```sh
python -m pip install -e './packages/python[mcp,http,postgres,test]'
python -m pytest packages/python/tests
python packages/python/examples/composite.py
python -m build packages/python
```

DB tests require TOOLSPLICE_PG_BIN and create only a disposable private loopback cluster. Cross-language conformance needs this built checkout and Node **for testing only**. Core tests/demo need no provider account or model call.
