Metadata-Version: 2.5
Name: voice-firm-data
Version: 0.3.1
Summary: Read-only LegalVoice and MediVoice firm data SDK, CLI and MCP server
Project-URL: Documentation, https://legalvoice.app/docs#developer-guide
Project-URL: Homepage, https://legalvoice.app
Author: LegalVoice / MediVoice
License-Expression: LicenseRef-Proprietary
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: httpx<1,>=0.28.1
Provides-Extra: mcp
Requires-Dist: fastmcp==4.0.3; extra == 'mcp'
Provides-Extra: test
Requires-Dist: fastmcp==4.0.3; extra == 'test'
Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
Requires-Dist: pytest>=8; extra == 'test'
Description-Content-Type: text/markdown

# Firm data SDK, CLI and MCP

Read stored LegalVoice and MediVoice firm records through the existing customer API. The FAB's Claude Agent SDK tools call the same backend query functions; this package gives scripts and external MCP hosts access through firm API keys.

Official developer documentation: [LegalVoice](https://legalvoice.app/docs#developer-guide) and [MediVoice](https://medivoice.org/docs#developer-guide). The `voice-firm-data` package contains the Python SDK, command-line client, and optional FastMCP server. It contains no API keys, customer records, database credentials, or backend application code.

## Install and authenticate

Use Python 3.10 or later:

```sh
python -m venv .venv-firm-data
source .venv-firm-data/bin/activate
python -m pip install --upgrade pip
python -m pip install "voice-firm-data[mcp]==0.3.1"
```

Omit `[mcp]` if you only need the Python client and CLI. Developer settings also
provide a versioned wheel download and checksum. On Windows, activate the
virtual environment with `.venv-firm-data\Scripts\activate`.

Create a data export key in the product's **Developer API** settings. Set it privately as `FIRM_DATA_API_KEY`, `LEGALVOICE_API_KEY`, or `MEDIVOICE_API_KEY`. Resolution follows that order. Keys need `data:read` or `exports:read`; the server derives the firm from the key. A key cannot select a different firm through query arguments.

**An API key is required; an OAuth client ID is not.** The key identifies the
firm (`account_id`, also called `agency_id`). The API's `client_id` identifies a
person/client record *inside* that firm; use it as an optional filter when you
want that client's calls, notes, tasks, or web chats. It is not a password and
cannot grant access to another firm's records. `firm-data whoami` reports the
account and key identity; `firm-data query clients` discovers client records.

Set `FIRM_DATA_BASE_URL` to the API origin shown in Developer settings. It defaults to `https://api.legalvoice.app`, the shared API default. Use the API origin, not the dashboard's domain. HTTPS is required except for localhost development. Credentials are not accepted as CLI arguments, returned in errors, or forwarded across redirects.

## CLI

```sh
firm-data whoami
firm-data discover
firm-data query clients --search Jane
firm-data query members
firm-data query web-chats --filter client_id=CLIENT_ID
firm-data query web-chat-messages --filter widget_key=WIDGET_KEY --filter session_id=SESSION_ID
firm-data query web-chat-tools --filter widget_key=WIDGET_KEY --filter session_id=SESSION_ID
firm-data query activity --filter client_id=CLIENT_ID
firm-data query calls --since 2026-09-01 --until 2026-09-08 --filter callback_requested=true
firm-data count calls --since 2026-09-01 --until 2026-09-08 --group-by status
firm-data query tasks --filter status=open --date-field due_on --until 2026-09-15
firm-data query notes --filter client_id=CLIENT_ID
firm-data query profile-notes --search Jane
firm-data query calls --search 'car accident'
firm-data get calls CALL_ID_OR_SID
firm-data query notes --all > notes.jsonl
```

Single requests print JSON with source references and coverage metadata. Lists return `has_more` and `next_cursor`; pass `--cursor CURSOR` with the same filters. Grouped counts also paginate. Ungrouped `count` returns the exact count of all matches, regardless of `--limit`.

`query --all` streams JSONL through every page (default ceiling: 1,000 pages; configurable with `--max-pages`). It exits nonzero on errors, repeated/missing cursors or an exceeded page ceiling. A file written before an error is **incomplete**; check the exit status. Pages are live reads, so concurrent inserts or updates can shift pagination. This is not a transactional snapshot.

Dates are UTC. `since` is inclusive and `until` exclusive. For September 1–7, use `--since 2026-09-01 --until 2026-09-08`. Timestamps require an explicit timezone offset. Exact filters and grouping fields vary by resource; discovery lists the supported fields for the deployed schema. Search is literal, case-insensitive substring matching over listed fields, including call transcripts.

## Python SDK

```python
from voice_firm_data import FirmDataClient

with FirmDataClient.from_env() as firm:
    identity = firm.identity()  # account_id, scopes and enforced key/account budgets
    catalog = firm.discover()
    total = firm.query("calls", mode="count", since="2026-09-01")
    tasks = firm.query("tasks", filters={"status": "open"})
    for note in firm.iter_records("notes", filters={"client_id": "CLIENT_ID"}):
        print(note["body"], note["source_ref"])
    call = firm.get("calls", "CALL_ID")  # full transcript/content
```

An explicit client is `FirmDataClient(api_key, base_url)`. Close it or use a context manager. Queries default to 25 records, maximum 250. Transcripts, metadata and document payloads are omitted from list results unless `include_full_payload=True`; each result lists `omitted_fields`. Detail requests include full content. Sensitive configuration resources always use a limited metadata projection, even in full mode.

## IDs and relationships

| Field | Meaning |
| --- | --- |
| `account_id` | The firm's existing `agency_id`, determined by the authenticated key |
| `client_id` | The stable `agency_people.id` returned by `clients` and `people`; changing a name or phone does not change it |
| `member_id` | A firm membership's `agency_members.id` |
| `user_id` | The member's login identity; one login can belong to multiple firms |

`client_id` is an alias for an existing identity, so there is no new ID backfill. Related records expose it when `person_id` or `primary_person_id` is recorded. An unlinked record has `client_id: null`; clients are never joined by guessing from names or phone numbers. Discovery reports each resource's `client_id_field` and whether it supports the `client_id` filter. Other relationships use the catalog's specific IDs, such as `call_sid`, `engagement_id`, `run_id`, and `document_id`.

## Enforced API budgets

All developer-key requests share PostgreSQL counters across server replicas. A reservation checks and charges both the key and its account atomically, before the handler runs. Creating or rotating keys does **not** reset the account budget. Existing keys automatically receive the standard limits.

| Budget | Per UTC minute | Per UTC day | Concurrent requests |
| --- | --- | --- | --- |
| Standard key | 120 units | 12,000 units | 3 |
| Limited key preset | 30 units | 3,000 units | 3 |
| Account, shared by all keys | 600 units | 60,000 units | 8 |

Ordinary requests cost **1 unit**, queries/counts **5**, discovery/export manifests **10**, and CSV exports **60**. These are fixed UTC minute/day windows, not rolling windows. Customer data requests have a 90-second deadline; query execution also has a 30-second database timeout. Short leases recover concurrency capacity after a worker stops unexpectedly.

Owners/admins choose a standard or limited budget when creating a key in Developer API settings. The key-creation endpoint also accepts `rate_limits: {"per_minute": 30, "per_day": 3000}`. Custom positive values cannot exceed the server's standard caps, including when reading stored metadata. A request costing more than a key's entire minute/day budget returns **422**; limited keys must use paginated JSON instead of CSV.

An exhausted budget returns **429** with `Retry-After` in seconds. Responses expose `Limit`, `Remaining`, `Reset` (Unix seconds), `Day-Limit`, `Day-Remaining`, `Account-Limit`, `Account-Remaining`, and `Cost`, all prefixed with `X-RateLimit-`. `GET /me` / `firm-data whoami` reports the configured limits. Failed quota storage returns **503** without executing the request. Requests rejected for missing/revoked keys do not reserve a budget; authenticated requests consume units even if their handler rejects the request.

The SDK, CLI, and MCP adapter retry 429 responses up to twice, honoring delays up to 60 seconds. A longer delay, or exhausted retries, raises `FirmDataRateLimitError` with `status_code=429` and `retry_after`; the CLI exits nonzero. Python callers can set `max_retries=0` to handle retries themselves and inspect `client.last_rate_limits`. Multi-page exports retain their incomplete-file behavior on quota exhaustion.

## MCP

Install the `mcp` extra, then configure your MCP host to run `firm-data-mcp` over stdio. The server uses the standalone [FastMCP framework](https://gofastmcp.com/), pinned to **4.0.3**, with `from fastmcp import FastMCP` and decorated tools. FastMCP owns schema generation, validation, transport and protocol handling. This package exposes local stdio transport; there is no hosted MCP URL or OAuth connector in this release.

```json
{
  "mcpServers": {
    "firm-data": {
      "command": "/absolute/path/to/.venv-firm-data/bin/firm-data-mcp",
      "env": {
        "FIRM_DATA_BASE_URL": "https://api.legalvoice.app",
        "FIRM_DATA_API_KEY": "<set your firm key privately in the host>"
      }
    }
  }
}
```

Use the host's private environment/secret settings where available. The placeholder must be replaced; it is not automatic shell-variable interpolation. GUI apps may require the executable's absolute path. The server writes protocol traffic to stdout and does not open a listening network port.

The three tools are:

| Tool | Purpose |
| --- | --- |
| `discover_firm_data` | Available resources, search/filter/date/group fields and query schema |
| `query_firm_data` | Search, filter, paginate or count all matching records |
| `get_firm_record` | Full record details with a source reference |

Try: “Show overdue open tasks grouped by owner,” “Find calls mentioning a car accident,” “Which members handle this client's work?” or “Summarize this client's web chats, notes and open tasks.” Resolve the client through `clients`, then use its `client_id`. For web chat transcripts and tool activity, query `web-chat-messages` and `web-chat-tools` with both `widget_key` and `session_id` from `web-chats`.

## HTTP contract and coverage

All routes use the existing customer API key authentication and scopes:

- `GET /api/v1/customer/v1/data` — discovery.
- `GET /api/v1/customer/v1/me` — account, key scopes and budgets.
- `POST /api/v1/customer/v1/query` — a **read-only** query body, e.g. `{"resource":"tasks","filters":{"status":"open"}}`.
- `GET /api/v1/customer/v1/records/{resource}/{record_id}` — record detail.

The registry covers **67 named resources** (including aliases). Some tables are optional and differ by deployment; discovery reports availability explicitly. Requests to unavailable resources fail instead of claiming zero records. Each registered resource also has list, detail, and CSV export routes in the Developer API catalog.

| Area | Resources |
| --- | --- |
| Clients and members | Clients/people, canonical contacts and cases, members/users, identity links, client facts, client messages, client audit history |
| Communications and intake | Calls/transcripts, intakes, web intakes, web chat sessions/messages/tool calls, email, SMS, messaging agent sessions/turns, appointments |
| Work and activity | Saved notes, profile notes, tasks, client activity, engagements/participants/links/events, work items and their events |
| Documents | Source/generated documents, projects, annotations, mentions, document workflows, e-sign envelopes/packages, knowledge sources/text chunks, extraction templates/runs |
| Automation and campaigns | Automations/dispatches, workflow definitions/runs/node runs/events/artifacts/human tasks, campaigns/runs/recipients/events/timeline |
| Administration | Phone numbers, departments, agent profiles/configuration, intake forms, integration metadata, webhook delivery history, audit events, usage and billing periods |

Activity and event feeds expose **persisted history**. They do not imply that every historical UI click or unrecorded action was captured, and this release does not add a universal activity recorder.

`notes` contains durable team notes; `profile-notes` contains nonempty `agency_people.metadata.contact_notes`. These are separate feeds, matching the two sources in the dashboard. `people` contains dashboard person IDs; `contacts` uses the canonical contact store when present and falls back to people only when that table is absent. Do not add overlapping resource counts together as unique clients or intakes.

These tools expose registered, stored firm data, not arbitrary database tables, SQL, provider credentials, or every record in an external CRM. Live external case data continues through the existing connected CRM tools. The FAB's chat controls can disable the new tools with the firm-data/recent-context groups; they remain available in review-only mode because they do not mutate records.

## Distribution and license

Install from PyPI or the versioned download in Developer settings. The package
uses a **proprietary SDK license**, not an open-source license. Authorized
commercial integrations are permitted under the included `LICENSE`; standalone
redistribution and publication of modified SDK versions require written
permission. Third-party dependencies keep their own licenses.

The SDK sends requests only to the API origin configured by the developer or
MCP host. Use an API origin you trust: the configured origin receives your firm
API key. HTTPS certificate verification is enabled; redirects and environment
proxy discovery are disabled. No telemetry is implemented by this SDK.

The MCP host may send returned firm data to its selected model provider. Use a
host and provider approved for the data you access. Model instructions cannot
supply a different API key, account, or API origin through these tools.
