Un servidor Model Context Protocol (MCP) que expone Ironclad como superficie de herramientas para Claude — permite que abogados e ingenieros de legal-ops le pidan a Claude consultar un workflow, buscar en el repositorio de contratos ejecutados, traer un tipo específico de cláusula, resumir el metadata de un workflow, o anotar un registro, todo desde una conversación de Claude en vez de la UI de Ironclad. El scaffold está en apps/web/public/artifacts/mcp-server-ironclad-legal/ y se entrega mayoritariamente de lectura por diseño: los borradores dentro de workflows activos son típicamente trabajo amparado por privilegio, así que el servidor trunca los cuerpos de documento por defecto y fuerza una segunda llamada explícita a una tool para recuperar el texto completo.
Cuándo usarlo
Recurre a esto cuando tu equipo in-house ya está en Ironclad y puedes nombrar tres o más consultas recurrentes que los abogados corren clickeando por la UI de Ironclad varias veces por semana — ejemplos típicos: “lista todo MSA activo por encima de $500K”, “trae la cláusula de indemnización de los últimos veinte deals cerrados”, “muéstrame los workflows que han estado esperando a la contraparte por más de cinco días hábiles”. Esas consultas son mecánicas: identificar un tipo de contrato, filtrar por una propiedad, devolver un campo de metadata. Son exactamente la forma de trabajo que se comprime bien en una conversación de Claude con tools.
El argumento económico: un equipo de legal-ops en Stage 4 Optimized que corre el equivalente a 200 consultas así por semana, a aproximadamente cuatro minutos por consulta end-to-end (abrir Ironclad, correr la búsqueda, filtrar, copiar el resultado, pegar en notas del matter), gasta cerca de 13 horas por semana en navegación de UI. Comprimir eso a ~30 segundos por turno de Claude pone el tiempo bajo dos horas. Las horas restantes vuelven al trabajo de revisión sustantiva — que es donde la hora marginal del equipo realmente escasea.
Cuándo NO usarlo
Sáltatelo si el volumen de estas consultas recurrentes de tu equipo está bajo aproximadamente veinte por semana — el costo de setup (revisión legal de la postura de privilegio, revisión de seguridad del blast radius del bearer token, y el ciclo de validación sandbox-to-production) no se repaga a ese volumen. Clickea por la UI de Ironclad; revísalo cuando el volumen crezca.
Sáltatelo si tu tenant está en un tier o región cuya superficie de API pública no ha sido validada contra el base path asumido por el scaffold (https://ironcladapp.com/public/api/v1/). El scaffold no está runtime-testeado; correrlo contra una base URL no verificada produce 404s que se disfrazan de “data faltante” dentro de conversaciones de Claude, que es exactamente el modo de falla que erosiona la confianza en herramientas legales mediadas por MCP.
Sáltatelo si tu política de matter-management trata todo el contenido de workflows — borradores, redlines, audit logs, comentarios — como privilegiado sin excepción. La postura de truncate-by-default del servidor maneja el caso común, pero un régimen de privilegio estricto necesita una capa adicional de enforcement de privilege-tag (item 5 en la lista de TODOs del bundle) antes de cualquier deployment, incluso read-only.
Finalmente, sáltatelo si todavía no tienes una AI policy para equipos legales que cubra el acceso de Claude a datos de contratos. Levanta la política primero; después este servidor.
Setup
El setup está documentado en detalle en apps/web/public/artifacts/mcp-server-ironclad-legal/README.md. Resumen:
Clona el bundle en un repo privado. Corre pip install -e . dentro del virtualenv del bundle.
Provisiona un API token de Ironclad en la consola de admin (Admin → API Keys → Create) con scope de lectura en workflows, records y documents. Agrega scope de comment-write solo si pretendes usar add_comment. Provisiona el role de service-account subyacente de forma acotada — el bearer token ve todo lo que ese role puede ver.
Define variables de entorno: IRONCLAD_API_TOKEN, IRONCLAD_TRUNCATE_AT (default 4000 chars por cuerpo de documento en respuestas de resumen), IRONCLAD_DEFAULT_WORKFLOW_TYPES (e.g. msa,nda,sow,dpa).
Registra con Claude Desktop vía el snippet JSON del README.
Sanity-checkea pidiéndole a Claude que resuma un workflow ID conocido, después confirma que la respuesta es solo metadata con marcadores _truncated_at en cualquier campo body, después pide el cuerpo completo del documento y confirma que llega solo tras la llamada explícita a get_document.
La recuperación en dos pasos es el punto — si el paso 5 devuelve el cuerpo completo del documento inline en la primera llamada, la guarda de truncación está mal configurada y deberías parar y arreglarla antes de exponer el servidor a alguien más allá del ingeniero que lo cableó.
Qué expone
El servidor registra nueve tools, agrupadas por el modelo de privilegio:
Lecturas de objeto (read-only):get_workflow, get_record, get_document. Cada una devuelve el metadata del objeto pedido; solo get_document devuelve texto del cuerpo completo, y solo cuando se llama explícitamente.
Búsqueda (read-only):search_records (texto libre contra el repositorio de contratos ejecutados), list_workflows (filtrado por status y tipo).
Helpers legales (read-only):clauses_by_type devuelve cláusulas extraídas de un tipo específico (e.g. indemnification, liability_cap, termination) de los documentos de un workflow; expiring_contracts devuelve records que se acercan a renovación o vencimiento en una ventana.
Audit-class (truncate-by-default):summarize_workflow devuelve un resumen solo-metadata más IDs y títulos de documentos; los cuerpos de documento en el resumen están truncados a IRONCLAD_TRUNCATE_AT chars con un marcador _truncated_at.
Escrituras ligeras (privilegiadas):add_comment agrega un comentario a un record. El único path de escritura, a propósito. Los comentarios dentro de Ironclad son ellos mismos discoverables — no escribas aquí nada que no escribirías directamente en la UI de Ironclad.
La lógica de dispatch, con el helper de truncación y el audit logger solo-metadata, vive en apps/web/public/artifacts/mcp-server-ironclad-legal/src/ironclad_legal_mcp/server.py.
Modelo de privilegio
Tres decisiones de postura concretas, cada una con una guarda en el scaffold:
Mayoritariamente lectura. No hay delete_*, no hay edición de borradores, no hay transiciones de stage de workflow, no hay cambios de signer. El único path de escritura es add_comment. Guarda: el dispatch en server.py simplemente no registra tools de escritura más allá de comentarios. Agregar cualquier tool que cambie estado requiere un cambio de código explícito con una revisión de privilegio.
Truncate-by-default.summarize_workflow trunca los cuerpos de documento a IRONCLAD_TRUNCATE_AT (default 4000 chars) y taggea la respuesta con _truncated_at para que Claude sepa que tiene que emitir una llamada follow-up a get_document cuando el usuario lo pide explícitamente. Guarda: el helper truncate_body() en server.py es el único chokepoint; ensancharlo cambia la postura de privilegio para todos los call sites a la vez.
El metadata de query de búsqueda no se persiste. El audit logger registra timestamp, usuario, nombre de tool y conteo de resultados — nunca el string de query. Guarda: el helper log_invocation() no tiene parámetro query; exponerlo requeriría un cambio de código revisado contra la política de privilegio.
Combinadas, estas tres decisiones significan que Claude puede navegar el repositorio de contratos, exponer el metadata que un abogado necesita para tomar una decisión, y documentar una acción con un comentario — pero no puede exfiltrar inadvertidamente trabajo amparado por privilegio ni crear un record discoverable de las prioridades de revisión del equipo. La postura de privilegio es el producto; las tools son la superficie.
Realidad de costo
Tres line items, todos reales:
Suscripción a Claude. Claude Desktop o Claude Code con MCP habilitado. Pro a $20/usuario/mes o Team a $25–30/usuario/mes cubren la mayoría de setups de equipos legales in-house; usuarios muy pesados pueden justificar Max.
Hosting del servidor. Proceso Python self-hosted. Córrelo localmente por abogado para desarrollo, o en una VM interna chica (1 vCPU / 1 GB RAM está bien para volumen sub-100-call/día) detrás de tu VPN para uso compartido. Aproximadamente $5–20/mes en un hyperscaler, gratis si ya tienes capacidad de Kubernetes interno.
Cuota de API de Ironclad. Ironclad rate-limita por-tenant; un equipo corriendo 200 queries/semana se mantiene bien dentro de las cuotas default, pero un equipo que construye una automatización que scanea el repositorio entero cada noche pegará los límites rápido. La lista de TODOs en el README del bundle marca los retries con exponential-backoff como tarea pre-producción — quema la cuota una vez y entenderás por qué.
El line item sin presupuestar es el tiempo de revisión legal. Planea dos a cuatro horas de tiempo de in-house counsel sobre la postura de privilegio antes de cualquier deployment a producción, y otra una a dos horas por trimestre en re-review a medida que Ironclad lanza features que cambian la superficie de API.
Cómo se ve el éxito
Mira tres números moverse:
UI-time-per-query, medido por muestreo: elige cinco queries recurrentes que el equipo corre semanalmente, mídelas en la UI de Ironclad antes del rollout, mide las mismas cinco vía conversación de Claude después del rollout, divide. Objetivo: 5x o mejor. Debajo de 2x y el costo de setup no se repaga.
Truncation-trigger rate, observable en el audit log: ¿con qué frecuencia un abogado sigue una llamada a summarize_workflow con un get_document explícito? La banda correcta es aproximadamente 20–50%. Por encima de 70% significa que el cap de truncación está muy agresivo y los abogados están siendo bloqueados; por debajo de 10% significa que están aceptando metadata que en realidad no responde la pregunta.
Comentarios agregados por semana.add_comment es el único path de escritura, y es la única señal de que un abogado actuó sobre lo que Claude expuso. Un conteo plano o cero a los dos meses del rollout significa que la herramienta se está usando como conveniencia solo-lookup, lo cual está bien, pero no justifica el costo de revisión de privilegio.
Versus las alternativas
Tres opciones reales, cada una con un tradeoff distinto:
Las features de AI nativas de Ironclad. Ironclad trae features de extracción de cláusulas y resúmenes con AI dentro del producto. Elige esas si tu workflow se queda dentro de Ironclad y las respuestas le pertenecen al record. Elige este servidor MCP si la respuesta tiene que aterrizar en una conversación de Claude que también alcanza notas de matter-management, tus guardrails de AI-policy, el resto de tu superficie de herramientas — es decir, si la integración con el razonamiento de Claude es el valor, no el lookup del contrato en sí.
AI legal de vendor (Harvey, EvenUp, etc.). Esos vendors traen modelos pre-entrenados de dominio legal sobre sus propios pipelines de ingestion. Elige un vendor si necesitas workflows privileged-by-default, evaluación de retrieval de grado-abogado, y tienes el presupuesto (cifras de cinco dígitos medianas para arriba anuales). Elige este servidor MCP si tu preferencia de modelo es Claude, tu ingestion es nativa de Ironclad, y tu equipo es lo suficientemente chico como para que el pricing por-seat del vendor no cierre.
Statu quo: los abogados clickean por la UI de Ironclad. Este es el baseline honesto. El servidor MCP le gana solo cuando el volumen de queries es lo suficientemente alto como para amortizar el costo de revisión de privilegio y setup. Por debajo de ~20 queries/semana por abogado, el statu quo gana.
Watch-outs
El README del bundle enumera la lista completa. Tres modos de falla vale la pena exponerlos acá, cada uno emparejado con la guarda específica que lo mitiga:
Fuga de privilegio vía inclusión inadvertida del cuerpo. Una implementación naive de summarize_workflow inlinearía el cuerpo del documento. Guarda: summarize_workflow rutea cada campo body a través de truncate_body(), que capea en IRONCLAD_TRUNCATE_AT y taggea la respuesta con _truncated_at. Ensanchar esto requiere editar un helper, que es el único chokepoint que un revisor de privilegio necesita auditar.
Logging de query de búsqueda que revela estrategia de revisión legal. Loggear el string de query de search_records crearía un record discoverable mostrando lo que el equipo está buscando — metadata privilegiado en sí mismo. Guarda: log_invocation() acepta solo nombre de tool y conteo de resultados; el string de query nunca se escribe a logs. Restaurarlo requeriría un cambio de código revisado contra la política de privilegio.
Falta de OAuth refresh en el scaffold. El scaffold usa un bearer token estático de Ironclad, que no puede ser revocado granularmente cuando un abogado deja el firm. Guarda (abierta): item 2 en la lista de TODOs del bundle marca OAuth-with-refresh como tarea pre-producción. Hasta que eso esté implementado, rota el token en cada cambio de personnel y trata el deployment con token estático como una postura development-only.
Stack
Servidor MCP Python self-hosted (el scaffold usa el SDK oficial mcp, httpx, pydantic) hablando con la API pública de Ironclad en el backend; Claude Desktop o Code en el front end. Opcional: structured logging vía python-json-logger piped al audit trail de tu matter-management; export a Sentry u OpenTelemetry, con strings de query y cuerpos de documento scrubeados antes de la transmisión.
# mcp-server-ironclad-legal
An MCP server tuned for in-house legal teams using Ironclad CLM. Exposes workflows, records, and documents as Claude tools, plus two legal-helper queries (`clauses_by_type`, `expiring_contracts`), an audit-class summary (`summarize_workflow`), and one privileged write (`add_comment`). Read-mostly by design — drafts inside active workflows are typically privileged content, so the server defaults to metadata-only responses with explicit drill-down.
> **STATUS: scaffold — not runtime-tested.** The code below is structurally
> complete and follows the official `mcp` Python SDK conventions, but it
> has not been executed against a live Ironclad tenant. Treat it as a
> starting point you adapt to your tenant's workflow types, custom-field
> conventions, and clause-extraction model. The Ironclad public API
> surface (paths, response shapes, association labels) varies by tenant
> tier and feature flags.
## What it exposes
### Object-read tools (read-only)
- `get_workflow(workflow_id)` — workflow metadata, current step, participants
- `get_record(record_id)` — executed-contract record (post-signature repository entry)
- `get_document(document_id, version?)` — full document body for a specific version (explicit drill-down only)
### Search tools (read-only)
- `search_records(query, limit?)` — search the executed-contract repository
- `list_workflows(status?, type?)` — active workflows by status and workflow type
### Legal helpers (read-only)
- `clauses_by_type(workflow_id, clause_type)` — extracted clauses of a specific type (e.g. `indemnification`, `liability_cap`, `termination`) from a workflow's documents
- `expiring_contracts(window_days=90)` — records approaching renewal or expiration in the window, sorted by next-action date
### Audit-class tool (truncate-by-default)
- `summarize_workflow(workflow_id)` — metadata-only summary: counterparty, type, status, step history, participants, associated documents (IDs + titles only). Document body fetch is a separate explicit `get_document` call.
### Light writes (privileged)
- `add_comment(record_id, body)` — append a comment to a record. Comments inside Ironclad are themselves discoverable; this tool is the only write path on purpose.
The server **does not** expose `delete_*`, draft edits, workflow-stage transitions, or signer changes. Privileged content (drafts in active workflows, audit logs, redlines) flows through truncate-by-default responses; the user must request the body via an explicit second tool call. The principle: Claude can navigate, summarize, and annotate; the attorney drives every state-changing decision.
## Setup
### 1. Install
```bash
git clone <wherever you put this>
cd mcp-server-ironclad-legal
python -m venv .venv
source .venv/bin/activate # or .venv\Scripts\activate on Windows
pip install -e .
```
### 2. Create an Ironclad API token
In Ironclad: Admin → API Keys → Create. Grant read scope on workflows, records, and documents. Grant comment-write scope only if you intend to use `add_comment`. Copy the token.
The token is bearer-style — it bypasses Ironclad's per-user role permissions, so it sees everything the API key's role can see. Provision a service-account role with the narrowest scope your team can tolerate, not the broadest.
### 3. Configure environment
```bash
export IRONCLAD_API_TOKEN="ic_..."
export IRONCLAD_TRUNCATE_AT="4000" # chars per body in summary responses
export IRONCLAD_DEFAULT_WORKFLOW_TYPES="msa,nda,sow,dpa" # narrows list_workflows
```
`IRONCLAD_TRUNCATE_AT` is the character cap for any document-body field returned by `summarize_workflow`. Bodies above this length are truncated and the response includes a `_truncated_at` marker so Claude knows to issue a follow-up `get_document` call when the user explicitly asks.
### 4. Register with Claude Desktop
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
```json
{
"mcpServers": {
"ironclad-legal": {
"command": "python",
"args": ["-m", "ironclad_legal_mcp.server"],
"env": {
"IRONCLAD_API_TOKEN": "ic_...",
"IRONCLAD_TRUNCATE_AT": "4000",
"IRONCLAD_DEFAULT_WORKFLOW_TYPES": "msa,nda,sow,dpa"
}
}
}
}
```
Restart Claude Desktop. You should see ~9 tools registered under `ironclad-legal`.
### 5. Sanity-check
Ask Claude: "Summarize workflow {known-id}." Confirm the response is metadata-only (no document body inline) and contains a `_truncated_at` marker on any body field. Then ask Claude: "Now get me the full body of document {id} from that workflow." Confirm the body is returned only after the explicit second call. Tune `IRONCLAD_TRUNCATE_AT` until the metadata responses are useful for navigation but not large enough to ship privileged drafts inadvertently.
## Watch-outs
- **Drafts inside active workflows are typically privileged.** A draft MSA mid-redline is attorney work product. The truncate-by-default posture in `summarize_workflow` is the guard — never widen it without an explicit per-team policy review. If your team's matter-management policy treats some draft classes as non-privileged, configure `IRONCLAD_TRUNCATE_AT=999999` per-deployment, not per-tool.
- **The audit log is itself privileged content.** Ironclad's audit log records who looked at what when — surfacing it through an MCP tool would let Claude reason over privileged metadata. This server intentionally does not expose audit-log queries. If you need audit-log access, build a separate tool with a documented legal-hold exception path, not through this scaffold.
- **Search query metadata reveals legal strategy.** "Find all MSAs with uncapped indemnification" is a query whose existence — even without results — discloses the team's review priorities. The dispatch logs only timestamps, user IDs, and result counts (never the query string itself). Do not patch the dispatch to log query text without a privilege review; doing so creates a discoverable record of legal strategy.
- **Bearer tokens bypass per-user permissions.** Anyone with access to the MCP client sees every record the API key's role can reach. Provision the service-account role narrowly and document it with your security team.
- **Clause extraction is model-dependent.** Ironclad's clause extraction may miss non-standard clause headings or clauses inside attachments. `clauses_by_type` returns whatever Ironclad surfaces — never use it as the sole source for compliance attestations.
## Limits and TODOs (before production use)
- [ ] Validate the Ironclad public API base path against your tenant's actual endpoint — some tiers use a regional subdomain.
- [ ] Implement OAuth-with-refresh in place of the static bearer token. Static tokens cannot be revoked granularly when an attorney leaves; OAuth refresh + per-user delegation is the production posture.
- [ ] Add request-level retries with exponential backoff (Ironclad rate-limits aggressively under burst load).
- [ ] Wire structured logging via `python-json-logger` and pipe to your matter-management audit trail.
- [ ] Add a privilege-tag enforcement layer: refuse to return any document marked `privileged: true` in custom properties, even via explicit `get_document`, unless the user passes a documented `--privilege-acknowledged` flag.
- [ ] Write integration tests against an Ironclad sandbox.
- [ ] Add Sentry / OpenTelemetry export — but scrub query strings and document bodies before transmission.
"""
ironclad-legal-mcp — MCP server tuned for in-house legal teams using Ironclad.
Exposes object-read tools (workflows, records, documents), search tools,
two legal helpers (clauses_by_type, expiring_contracts), one audit-class
summary (summarize_workflow, truncate-by-default), and one privileged
write (add_comment). Read-mostly by design — drafts inside active
workflows are typically privileged work product, so document bodies are
truncated by default and the user must request the full body via an
explicit second tool call.
STATUS: scaffold — not runtime-tested. Adapt the workflow type names,
clause-type vocabulary, custom-property paths, and (in particular) the
public API base path to your tenant before use. Some Ironclad tiers use
a regional subdomain.
Run as: python -m ironclad_legal_mcp.server
"""
from __future__ import annotations
import logging
import os
from datetime import datetime, timedelta, timezone
from typing import Any
import httpx
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import TextContent, Tool
# ----- Configuration (read from env at startup) -----
IRONCLAD_API_TOKEN = os.environ.get("IRONCLAD_API_TOKEN")
TRUNCATE_AT = int(os.environ.get("IRONCLAD_TRUNCATE_AT", "4000"))
DEFAULT_WORKFLOW_TYPES = [
s.strip()
for s in os.environ.get("IRONCLAD_DEFAULT_WORKFLOW_TYPES", "").split(",")
if s.strip()
]
API_BASE = "https://ironcladapp.com/public/api/v1"
# Privilege-aware logger: NEVER include query strings, document bodies, or
# clause text in log records. Only metadata: timestamp, user, tool name,
# result count. Surfacing query text would create a discoverable record
# of legal review strategy.
audit_log = logging.getLogger("ironclad_legal_mcp.audit")
def require_config() -> None:
if not IRONCLAD_API_TOKEN:
raise RuntimeError("IRONCLAD_API_TOKEN env var is required")
def auth_headers() -> dict[str, str]:
return {
"Authorization": f"Bearer {IRONCLAD_API_TOKEN}",
"Content-Type": "application/json",
}
def log_invocation(tool: str, result_count: int | None = None) -> None:
"""Metadata-only audit record. Never includes query text or body."""
audit_log.info(
"tool=%s ts=%s results=%s",
tool,
datetime.now(timezone.utc).isoformat(),
result_count if result_count is not None else "n/a",
)
def truncate_body(body: str | None) -> dict[str, Any]:
"""Return a body field that is truncated to TRUNCATE_AT chars, with a
`_truncated_at` marker so Claude knows to issue a follow-up
explicit get_document call when the user asks for the full text."""
if body is None:
return {"text": None, "_truncated_at": None}
if len(body) <= TRUNCATE_AT:
return {"text": body, "_truncated_at": None}
return {
"text": body[:TRUNCATE_AT],
"_truncated_at": TRUNCATE_AT,
"_full_length": len(body),
"_hint": "Call get_document(document_id, version) for the full body.",
}
# ----- Ironclad HTTP helpers -----
async def ic_get(path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
async with httpx.AsyncClient(timeout=30.0) as client:
r = await client.get(f"{API_BASE}{path}", headers=auth_headers(), params=params)
r.raise_for_status()
return r.json()
async def ic_post(path: str, body: dict[str, Any]) -> dict[str, Any]:
async with httpx.AsyncClient(timeout=30.0) as client:
r = await client.post(f"{API_BASE}{path}", headers=auth_headers(), json=body)
r.raise_for_status()
return r.json()
# ----- Server + tool registry -----
server = Server("ironclad-legal")
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="get_workflow",
description=(
"Fetch workflow metadata, current step, and participant list "
"by workflow ID. Does not include document bodies — use "
"summarize_workflow for that, then get_document for full text."
),
inputSchema={
"type": "object",
"properties": {"workflow_id": {"type": "string"}},
"required": ["workflow_id"],
},
),
Tool(
name="get_record",
description=(
"Fetch an executed-contract record (post-signature repository "
"entry) by record ID. Returns metadata + linked document IDs."
),
inputSchema={
"type": "object",
"properties": {"record_id": {"type": "string"}},
"required": ["record_id"],
},
),
Tool(
name="get_document",
description=(
"Fetch the full body of a document at a specific version. "
"This is the explicit drill-down call the user must request "
"after seeing a truncated body in summarize_workflow."
),
inputSchema={
"type": "object",
"properties": {
"document_id": {"type": "string"},
"version": {"type": "string"},
},
"required": ["document_id"],
},
),
Tool(
name="search_records",
description=(
"Search the executed-contract repository by free-text query. "
"Query string is NOT logged; only timestamp, user, and "
"result count are recorded."
),
inputSchema={
"type": "object",
"properties": {
"query": {"type": "string"},
"limit": {"type": "integer", "default": 25},
},
"required": ["query"],
},
),
Tool(
name="list_workflows",
description=(
"List active workflows, optionally filtered by status (e.g. "
"'in_review', 'awaiting_signature') and type (e.g. 'msa', "
"'nda'). When type is omitted, IRONCLAD_DEFAULT_WORKFLOW_TYPES "
"is used if set."
),
inputSchema={
"type": "object",
"properties": {
"status": {"type": "string"},
"type": {"type": "string"},
"limit": {"type": "integer", "default": 50},
},
},
),
Tool(
name="clauses_by_type",
description=(
"Return extracted clauses of a specific type (e.g. "
"'indemnification', 'liability_cap', 'termination', "
"'governing_law') from the documents attached to a workflow. "
"Backed by Ironclad's clause-extraction model — coverage is "
"best-effort, not authoritative for compliance attestations."
),
inputSchema={
"type": "object",
"properties": {
"workflow_id": {"type": "string"},
"clause_type": {"type": "string"},
},
"required": ["workflow_id", "clause_type"],
},
),
Tool(
name="expiring_contracts",
description=(
"Return executed-contract records approaching renewal or "
"expiration within window_days, sorted by next-action date "
"ascending."
),
inputSchema={
"type": "object",
"properties": {"window_days": {"type": "integer", "default": 90}},
},
),
Tool(
name="summarize_workflow",
description=(
"Metadata-only summary of a workflow: counterparty, type, "
"status, step history, participants, document IDs + titles. "
"Document bodies are truncated to IRONCLAD_TRUNCATE_AT chars "
"with a _truncated_at marker. Use get_document for full text."
),
inputSchema={
"type": "object",
"properties": {"workflow_id": {"type": "string"}},
"required": ["workflow_id"],
},
),
Tool(
name="add_comment",
description=(
"Append a comment to an executed-contract record. The only "
"write path exposed by this server. Comments are themselves "
"discoverable inside Ironclad — write nothing here you would "
"not write directly in the Ironclad UI."
),
inputSchema={
"type": "object",
"properties": {
"record_id": {"type": "string"},
"body": {"type": "string"},
},
"required": ["record_id", "body"],
},
),
]
# ----- Tool dispatch -----
@server.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
require_config()
if name == "get_workflow":
data = await ic_get(f"/workflows/{arguments['workflow_id']}")
log_invocation("get_workflow", 1)
return [TextContent(type="text", text=str(data))]
if name == "get_record":
data = await ic_get(f"/records/{arguments['record_id']}")
log_invocation("get_record", 1)
return [TextContent(type="text", text=str(data))]
if name == "get_document":
params: dict[str, Any] = {}
if v := arguments.get("version"):
params["version"] = v
data = await ic_get(f"/documents/{arguments['document_id']}", params or None)
log_invocation("get_document", 1)
return [TextContent(type="text", text=str(data))]
if name == "search_records":
body = {
"query": arguments["query"],
"limit": arguments.get("limit", 25),
}
result = await ic_post("/records/search", body)
# Log result count only — never the query string itself.
log_invocation("search_records", len(result.get("results", [])))
return [TextContent(type="text", text=str(result))]
if name == "list_workflows":
params: dict[str, Any] = {"limit": arguments.get("limit", 50)}
if status := arguments.get("status"):
params["status"] = status
wf_type = arguments.get("type")
if wf_type:
params["type"] = wf_type
elif DEFAULT_WORKFLOW_TYPES:
params["type"] = ",".join(DEFAULT_WORKFLOW_TYPES)
result = await ic_get("/workflows", params)
log_invocation("list_workflows", len(result.get("workflows", [])))
return [TextContent(type="text", text=str(result))]
if name == "clauses_by_type":
wf = await ic_get(
f"/workflows/{arguments['workflow_id']}",
{"include": "documents,clauses"},
)
clause_type = arguments["clause_type"].lower()
# Filter the workflow's surfaced clauses by type. Body of each
# clause is left intact (clauses are short; truncation is for
# full document bodies). If a downstream tenant has very long
# clauses, fold truncate_body() over the `text` field here.
clauses = []
for doc in wf.get("documents", []):
for c in doc.get("clauses", []):
if c.get("type", "").lower() == clause_type:
clauses.append(
{
"document_id": doc.get("id"),
"document_title": doc.get("title"),
"clause_type": c.get("type"),
"text": c.get("text"),
"page": c.get("page"),
}
)
log_invocation("clauses_by_type", len(clauses))
return [TextContent(type="text", text=str({"clauses": clauses}))]
if name == "expiring_contracts":
window_days = arguments.get("window_days", 90)
cutoff = datetime.now(timezone.utc) + timedelta(days=window_days)
body = {
"filters": [
{
"property": "next_action_date",
"operator": "LTE",
"value": cutoff.isoformat(),
},
{
"property": "status",
"operator": "IN",
"values": ["active", "auto_renewing"],
},
],
"sort": [{"property": "next_action_date", "direction": "ASC"}],
"limit": 200,
}
result = await ic_post("/records/search", body)
log_invocation("expiring_contracts", len(result.get("results", [])))
return [TextContent(type="text", text=str(result))]
if name == "summarize_workflow":
wf = await ic_get(
f"/workflows/{arguments['workflow_id']}",
{"include": "documents,participants,history"},
)
# Build a metadata-only summary. Any document body present is
# passed through truncate_body() so the response includes a
# _truncated_at marker that Claude reads as a hint to call
# get_document explicitly when the user asks for the full text.
summary = {
"id": wf.get("id"),
"type": wf.get("type"),
"status": wf.get("status"),
"counterparty": wf.get("counterparty"),
"current_step": wf.get("current_step"),
"participants": [
{"id": p.get("id"), "role": p.get("role"), "email": p.get("email")}
for p in wf.get("participants", [])
],
"step_history": wf.get("history", []),
"documents": [
{
"id": d.get("id"),
"title": d.get("title"),
"version": d.get("version"),
"body_preview": truncate_body(d.get("body")),
}
for d in wf.get("documents", [])
],
}
log_invocation("summarize_workflow", len(summary["documents"]))
return [TextContent(type="text", text=str(summary))]
if name == "add_comment":
body = {
"body": arguments["body"],
"created_at": datetime.now(timezone.utc).isoformat(),
}
result = await ic_post(
f"/records/{arguments['record_id']}/comments", body
)
log_invocation("add_comment", 1)
return [
TextContent(
type="text",
text=f"Added comment {result.get('id')} to record {arguments['record_id']}",
)
]
raise ValueError(f"Unknown tool: {name}")
# ----- Entrypoint -----
async def main() -> None:
require_config()
async with stdio_server() as (read, write):
await server.run(read, write, server.create_initialization_options())
if __name__ == "__main__":
import asyncio
asyncio.run(main())