# MCP Python SDK
URL: https://docs.autociso.io/developers/mcp/python-sdk/
Description: Install autociso-mcp-client and make your first typed MCP tool call from Python.

`autociso-mcp-client` is the first-party Python SDK for the AutoCISO MCP server. It gives you a typed method per tool, sync and async clients, and built-in retries.

## Install

```bash
pip install autociso-mcp-client
```

The import name is `autociso_mcp`:

```python
from autociso_mcp import AutocisoMcpClient
```

## Create a client

`base_url` is **required** — there is no default. Pass the full URL **including** `/mcp`:

**Python**

```python
from autociso_mcp import AutocisoMcpClient

client = AutocisoMcpClient(
    base_url="https://api.autociso.io/mcp",
    token=os.environ["AUTOCISO_MCP_TOKEN"],  # mcp:read scope
)
```

**Warning — Treat your token like a password**

Read the token from the `AUTOCISO_MCP_TOKEN` environment variable rather than inlining it. Never commit a real token to source control; revoke it from **user menu → API Tokens** if exposed (see [API tokens](https://docs.autociso.io/developers/api-tokens/)).

## First call

Each tool maps 1:1 to a namespaced, snake_case method. Use the sync client for scripts or the async client inside an event loop:

**Python (sync)**

```python
from autociso_mcp import AutocisoMcpClient

client = AutocisoMcpClient(
    base_url="https://api.autociso.io/mcp",
    token=os.environ["AUTOCISO_MCP_TOKEN"],
)

posture = client.posture.summary()
print("Overall score:", posture.overall_score)

stale = client.access.stale_accounts()
print("Stale accounts:", len(stale))
```

**Python (async)**

```python
from autociso_mcp import AsyncAutocisoMcpClient

async def main():
    client = AsyncAutocisoMcpClient(
        base_url="https://api.autociso.io/mcp",
        token=os.environ["AUTOCISO_MCP_TOKEN"],
    )
    posture = await client.posture.summary()
    print("Overall score:", posture.overall_score)

asyncio.run(main())
```

Results are PII-masked — emails appear as `a***@domain.com` and personal names are redacted (to resolve an `employeeId` to a name, see [Employee identity](https://docs.autociso.io/developers/employee-identity/)):

```json
{
  "overallScore": 72,
  "staleAccounts": 14,
  "openRisks": 6,
  "isoGapsOpen": 23,
  "topConcern": "14 stale accounts pending review"
}
```

## Typed results

Methods for typed tools return `TypedToolResult[XResult]`. `.data`, `.is_error` and `.raw` work as before and never raise. `.result` validates `.data` lazily into a generated Pydantic model and raises `ResultSchemaError` on a mismatch; the error carries the tool name and field paths only, never values. Tools without a published schema return a plain result. The package ships `py.typed`, and the client prefers `structuredContent`, falling back to the text block.

```python
res = await client.access.anomaly_discovery()
res.data["totalCount"]        # unchanged dict access
res.result.total_count        # typed, validated on first access
```

## Patterns

- **Typed method per tool.** `client.access.stale_accounts()`, `client.risk.get(id="risk_…")`, `client.iso.gap_status()` map to the same tools as the TypeScript SDK. See the [Tool Catalog](https://docs.autociso.io/developers/mcp/tools/).
- **No `org_id`, ever.** Your org is bound to the token on the server (see [Org binding](https://docs.autociso.io/developers/mcp/authentication/#org-binding--no-org_id-from-the-client)). There is no org argument — you can't query another tenant. By design.
- **Retries on reads.** Idempotent reads retry automatically on `429` and transient errors with exponential backoff and jitter (capped at ~3 attempts).
- **Writes are not auto-retried.** There are five write tools (`risk_create_draft`, `iso_audit_simulate`, `supplier_questionnaire_clone`, `supplier_questionnaire_update_question`, `supplier_questionnaire_update_option`), for example `client.risk.create_draft(title="…")`. None is ever retried automatically: the server has no idempotency key, so a blind retry could create a duplicate or spend a second AI request.
- **Timeout override.** Pass `timeout=` per call when a tool is slow.
- **`call_raw` escape hatch.** For a tool the typed surface doesn't cover, call it by name; the result is an untyped `dict`:

**Python**

```python
raw = client.call_raw("audit_trail_search", {"limit": 25, "offset": 0})
# raw is a dict — validate before use
```

## Error handling

Tool failures raise typed exceptions (e.g. `AutocisoScopeError`, `AutocisoRateLimitError`). See [MCP Errors and Retries](https://docs.autociso.io/developers/mcp/errors/) for the full mapping and retry behavior.
