Metadata-Version: 2.5
Name: cavi-mcp-core
Version: 0.1.1
Summary: Server type, result contracts, schemas, bounded processes and file journals for CAVI MCP servers
Project-URL: Homepage, https://github.com/cavi-ai/mcp-core#readme
Project-URL: Repository, https://github.com/cavi-ai/mcp-core
Project-URL: Issues, https://github.com/cavi-ai/mcp-core/issues
Project-URL: Changelog, https://github.com/cavi-ai/mcp-core/blob/main/CHANGELOG.md
Author: CAVI AI
License-Expression: MIT
License-File: LICENSE
Keywords: mcp,mcp-server,model-context-protocol
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.11
Requires-Dist: mcp<3,>=2.3
Requires-Dist: pydantic<3,>=2.11
Description-Content-Type: text/markdown

# Cavi MCP Core

Shared plumbing for the CAVI MCP servers ([blender-mcp](https://github.com/cavi-ai/blender-mcp),
[substance-mcp](https://github.com/cavi-ai/substance-mcp),
[houdini-mcp](https://github.com/cavi-ai/houdini-mcp),
[adobe-mcp](https://github.com/cavi-ai/adobe-mcp)): one server type, one failure contract,
one schema compaction, one bounded process runner, bounded file reads and an operation
journal.

## Install

```sh
uv add cavi-mcp-core
```

Requires Python 3.11+ and the MCP Python SDK 2.

## Server

```python
from cavi_mcp_core.server import READ_ONLY, Server

mcp = Server("my-mcp", package="my-mcp", instructions="Inspect before editing.")


@mcp.tool(annotations=READ_ONLY)
async def status() -> dict:
    return {"ready": True}
```

`Server` extends `mcp.server.mcpserver.MCPServer`:

- `package` sets the reported server version from that distribution's metadata.
- Tool schemas are compacted (`compact=False` keeps the SDK's schemas).
- `ValueError`, `RuntimeError` and `OSError` raised by a tool reach the caller with their
  text; any other exception is logged on the server and reported without details. Pass
  `anticipated=()` to leave tool errors to the SDK.
- Every tool error, including an unknown tool name, returns one structured result:

```json
{"error": {"version": 1, "code": "INVALID_ARGUMENT", "operation": "status",
           "outcome": "rejected", "retry_safe": false, "next_action": "inspect_inputs"}}
```

`outcome` is `rejected` only for invalid arguments, missing inputs and unknown tools;
anything else is `unknown`, and `retry_safe` is always false. Raise
`contracts.OperationRejected(message, code)` or `contracts.OperationUnknown(message, code)`
to report a specific code. Override `Server.failure` to name a different recovery tool
for unknown outcomes.

`need(value, name, action)` and `reject_foreign(action, given, allowed)` validate
action-style arguments. `READ_ONLY`, `WRITES` and `DESTRUCTIVE` are the tool annotations.

## Contracts

`contracts.Result` is a strict pydantic model whose unset optional fields stay off the
wire. `contracts.VariantResult` accepts one of several declared field sets (`shapes`)
without a discriminator field.

## Schemas

`schema.compact_schema(schema, output=False)` returns a copy without titles or
`additionalProperties: true`, with equivalent union forms shortened, and validates the same
values; `output=True` also drops defaults, which are documentation only in a response.
`schema.validate_variant` and `schema.variant_schema` back `VariantResult`.

## Processes

`proc.run(args, timeout)` runs a command with bounded output and one deadline. On a
timeout or a cancellation it stops the whole process group (process tree on Windows) and
returns once the process exits. `proc.run_sync` is the blocking form and
`proc.diagnostics(result)` formats what a failed run printed.

## Applications

`apps.find_directory`, `apps.find_app` and `apps.find_executable` locate an installed
application in one order: an environment override, which is final, then the default
location the caller passes, then `PATH`. `apps.bundle(name, identifier, pattern, env)`
finds a macOS application by its bundle identifier: the override in `env` when set, else
the newest matching bundle under `/Applications`; it raises `AppUnavailable` or
`AppMismatch` with a stable `error_code`. `apps.version` reads a bundle's version without
running it, `apps.running` asks whether an executable has a process, and
`apps.processes` lists the processes of one executable on macOS.

## Files and journals

`files.inside(workspace, path)` resolves a path inside an existing workspace and refuses
`..`, escapes through symbolic links and the workspace root. `files.read_bytes`,
`files.load_json` and `files.digest` read bounded regular files and refuse a file that
changes while it is read; `files.parse_json` refuses NaN and Infinity.

`journal.write_json` replaces a JSON file atomically, durably and readable only by its
owner. `journal.completion` reads the completion receipt of one operation,
`journal.archive` records how an interrupted operation was resolved, and
`journal.fingerprint` hashes canonical JSON.

## Development

```sh
uv sync --locked --dev
uv run --locked python -m unittest discover -s tests
uv run --locked ruff check .
```

## License

MIT
