Um servidor Model Context Protocol (MCP) que expõe o Ironclad como uma superfície de ferramentas para o Claude — permitindo que advogados e engenheiros de legal-ops peçam ao Claude para pesquisar um workflow, buscar no repositório de contratos executados, extrair um tipo de cláusula específico, resumir os metadados de um workflow ou anotar um registro, tudo a partir de uma conversa no Claude em vez da interface do Ironclad. O scaffold está em apps/web/public/artifacts/mcp-server-ironclad-legal/ e é entregue majoritariamente de leitura por design: rascunhos dentro de workflows ativos são tipicamente work product privilegiado, então o servidor trunca corpos de documentos por padrão e força uma segunda chamada de ferramenta explícita para recuperar o texto completo.
Quando usar
Recorra a isso quando sua equipe in-house já usa o Ironclad e você consegue nomear três ou mais consultas recorrentes que os advogados executam clicando pela interface do Ironclad várias vezes por semana — exemplos típicos: “liste cada MSA ativo acima de $500K”, “extraia a cláusula de indenização dos últimos vinte deals fechados”, “mostre os workflows que estão aguardando a contraparte há mais de cinco dias úteis”. Essas consultas são mecânicas: identificar um tipo de contrato, filtrar por uma propriedade, retornar um campo de metadados. São exatamente o formato de trabalho que comprime bem em uma conversa com ferramentas do Claude.
O argumento econômico: uma equipe de legal-ops Estágio 4 Otimizado que executa o equivalente a 200 dessas consultas por semana, a aproximadamente quatro minutos por consulta de ponta a ponta (abrir o Ironclad, executar a busca, filtrar, copiar o resultado, colar nas notas do matter), gasta cerca de 13 horas por semana em navegação pela interface. Comprimindo isso para ~30 segundos por turno no Claude, coloca o tempo em menos de duas horas. As horas restantes voltam para o trabalho de revisão substantiva — que é onde a hora marginal da equipe é realmente escassa.
Quando NÃO usar
Pule se o volume de sua equipe dessas consultas recorrentes for menor que aproximadamente vinte por semana — o custo de setup (revisão jurídica da postura de privilege, revisão de segurança do raio de impacto do bearer token e o ciclo de validação de sandbox para produção) não compensa nesse volume. Clique pela interface do Ironclad; revisite quando o volume crescer.
Pule se seu tenant estiver em um tier ou região cuja superfície de API pública não foi validada contra o caminho base assumido do scaffold (https://ironcladapp.com/public/api/v1/). O scaffold não foi testado em runtime; rodá-lo contra uma URL base não verificada produz 404s que se mascaram como “dados ausentes” dentro de conversas Claude, que é exatamente o modo de falha que corrói a confiança em ferramentas jurídicas mediadas por MCP.
Pule se sua política de gestão de matters trata todo conteúdo de workflow — rascunhos, redlines, logs de auditoria, comentários — como privilegiado sem exceção. A postura de truncar-por-padrão do servidor lida com o caso comum, mas um regime de privilege estrito precisa de uma camada adicional de aplicação de privilege-tag (item 5 na lista de TODOs do bundle) antes de qualquer deploy, incluindo somente-leitura.
Por fim, pule se você ainda não tem uma política de AI para equipes jurídicas que cubra o acesso do Claude a dados de contratos. Configure a política primeiro; depois, este servidor.
Setup
O setup está documentado em detalhes em apps/web/public/artifacts/mcp-server-ironclad-legal/README.md. Resumo:
Clone o bundle em um repositório privado. Execute pip install -e . dentro do virtualenv do bundle.
Provisione um token de API do Ironclad no console de administração (Admin → API Keys → Create) com escopo de leitura em workflows, registros e documentos. Adicione escopo de escrita de comentários apenas se pretender usar add_comment. Provisione o papel de service-account subjacente de forma restrita — o bearer token vê tudo o que esse papel consegue ver.
Defina variáveis de ambiente: IRONCLAD_API_TOKEN, IRONCLAD_TRUNCATE_AT (padrão 4000 chars por corpo de documento em respostas de resumo), IRONCLAD_DEFAULT_WORKFLOW_TYPES (por ex. msa,nda,sow,dpa).
Registre com o Claude Desktop via o trecho JSON no README.
Verifique de sanidade pedindo ao Claude que resuma um ID de workflow conhecido, confirmando que a resposta é somente-metadados com marcadores _truncated_at em qualquer campo de corpo, depois pedindo o corpo completo do documento e confirmando que chega apenas após a chamada explícita de get_document.
A recuperação em duas etapas é o ponto — se o passo 5 retornar um corpo de documento completo inline na primeira chamada, o guard de truncamento está mal configurado e você deve parar e corrigi-lo antes de expor o servidor a alguém além do engenheiro que o conectou.
O que expõe
O servidor registra nove ferramentas, agrupadas pelo modelo de privilege:
Leituras de objeto (somente-leitura):get_workflow, get_record, get_document. Cada uma retorna os metadados do objeto solicitado; apenas get_document retorna o texto completo do corpo, e apenas quando chamado explicitamente.
Busca (somente-leitura):search_records (texto livre contra o repositório de contratos executados), list_workflows (filtrado por status e tipo).
Helpers jurídicos (somente-leitura):clauses_by_type retorna cláusulas extraídas de um tipo específico (por ex. indemnification, liability_cap, termination) dos documentos de um workflow; expiring_contracts retorna registros se aproximando da renovação ou expiração em uma janela.
Classe de auditoria (truncar por padrão):summarize_workflow retorna um resumo somente-metadados mais IDs e títulos de documentos; corpos de documentos no resumo são truncados para IRONCLAD_TRUNCATE_AT chars com um marcador _truncated_at.
Escritas leves (privilegiadas):add_comment anexa um comentário a um registro. O único caminho de escrita de propósito. Comentários dentro do Ironclad são eles próprios descobríveis — não escreva nada aqui que você não escreveria diretamente na interface do Ironclad.
A lógica de dispatch, com o helper de truncamento e o logger de auditoria somente-metadados, fica em apps/web/public/artifacts/mcp-server-ironclad-legal/src/ironclad_legal_mcp/server.py.
Modelo de privilege
Três escolhas concretas de postura, cada uma com um guard no scaffold:
Majoritariamente de leitura. Sem delete_*, sem edições de rascunho, sem transições de fase do workflow, sem mudanças de signatário. O único caminho de escrita é add_comment. Guard: o dispatch em server.py simplesmente não registra ferramentas de escrita além de comentários. Adicionar qualquer ferramenta que muda estado requer uma mudança de código explícita com uma revisão de privilege.
Truncar por padrão.summarize_workflow trunca corpos de documentos para IRONCLAD_TRUNCATE_AT (padrão 4000 chars) e marca a resposta com _truncated_at para que o Claude saiba emitir uma chamada de acompanhamento get_document quando o usuário pede explicitamente. Guard: o helper truncate_body() em server.py é o único chokepoint; ampliá-lo muda a postura de privilege para todos os call sites de uma vez.
Metadados de consulta de busca não são persistidos. O logger de auditoria registra timestamp, usuário, nome da ferramenta e contagem de resultados — nunca a string de consulta em si. Guard: o helper log_invocation() não tem parâmetro query; expor um exigiria uma mudança de código revisada contra a política de privilege.
Combinadas, essas três escolhas significam que o Claude pode navegar pelo repositório de contratos, destacar os metadados que um advogado precisa para tomar uma decisão e documentar uma ação com um comentário — mas não pode inadvertidamente exfiltrar work product privilegiado ou criar um registro descobrível das prioridades de revisão da equipe. A postura de privilege é o produto; as ferramentas são a superfície.
Custo real
Três itens, todos reais:
Assinatura do Claude. Claude Desktop ou Claude Code com MCP habilitado. Pro a $20/usuário/mês ou Team a $25-30/usuário/mês cobre a maioria dos setups de equipe jurídica in-house; usuários muito intensos podem justificar o Max.
Hospedagem do servidor. Processo Python self-hosted. Rode localmente por advogado para desenvolvimento, ou em uma VM interna pequena (1 vCPU / 1 GB RAM é suficiente para volume sub-100 chamadas/dia) atrás da sua VPN para uso compartilhado. Aproximadamente $5-20/mês em um hyperscaler, gratuito se você já tem capacidade Kubernetes interna.
Quota de API do Ironclad. O Ironclad limita por tenant; uma equipe executando 200 consultas/semana fica bem dentro das cotas padrão, mas uma equipe que constrói uma automação que varre o repositório inteiro nightly vai atingir os limites rapidamente. A lista de TODOs no README do bundle sinaliza retries com backoff exponencial como uma tarefa pré-produção — queime a quota uma vez e vai entender por quê.
O item não orçado é o tempo de revisão jurídica. Planeje de duas a quatro horas do tempo de advogado in-house sobre a postura de privilege antes de qualquer deploy em produção, e mais uma a duas horas por trimestre em revisão à medida que o Ironclad entrega recursos que mudam a superfície da API.
O que o sucesso parece
Observe três números se moverem:
UI-time-por-consulta, medido por amostragem: escolha cinco consultas recorrentes que a equipe executa semanalmente, cronometre-as na interface do Ironclad antes do rollout, cronometre as mesmas cinco via conversa Claude após o rollout, divida. Meta: 5x ou melhor. Abaixo de 2x e o custo de setup não está compensando.
Taxa de acionamento de truncamento, observável no log de auditoria: com que frequência um advogado segue uma chamada summarize_workflow com um get_document explícito? A faixa certa é aproximadamente 20-50%. Acima de 70% significa que o cap de truncamento está muito agressivo e os advogados estão sendo bloqueados; abaixo de 10% significa que estão aceitando metadados que na verdade não respondem à pergunta.
Comentários adicionados por semana.add_comment é o único caminho de escrita, e é o único sinal de que um advogado agiu com base no que o Claude destacou. Uma contagem zero ou plana dois meses após o rollout significa que a ferramenta está sendo usada como conveniência somente-de-lookup, o que está ótimo, mas não justifica o custo de revisão de privilege.
Versus as alternativas
Três escolhas reais, cada uma com uma tradeoff distinta:
Recursos nativos de AI do Ironclad. O Ironclad entrega recursos de extração de cláusulas e sumarização por AI dentro do produto. Escolha esses se o seu workflow fica dentro do Ironclad e as respostas pertencem ao registro. Escolha este servidor MCP se a resposta precisa chegar em uma conversa Claude que também alcança notas de gestão de matters, seus guardrails de política de AI, o restante da sua superfície de ferramentas — isto é, se a integração com o raciocínio do Claude é o valor, não a busca de contrato em si.
AI jurídica de fornecedor (Harvey, EvenUp, etc.). Esses fornecedores entregam modelos de domínio jurídico pré-treinados sobre seus próprios pipelines de ingestão. Escolha um fornecedor se você precisa de workflows privilegiados por padrão, avaliação de recuperação de nível de advogado e tem o orçamento (cinco dígitos médios e acima anualmente). Escolha este servidor MCP se sua preferência de modelo é Claude, sua ingestão é nativa do Ironclad e sua equipe é pequena o suficiente para o preço por assento de um fornecedor não fechar os números.
Status quo: advogados clicam pela interface do Ironclad. Este é o baseline honesto. O servidor MCP o supera apenas quando o volume de consultas é alto o suficiente para amortizar o custo de revisão de privilege e setup. Abaixo de ~20 consultas/semana por advogado, o status quo vence.
Pontos de atenção
O README do bundle enumera a lista completa. Três modos de falha valem ser destacados aqui, cada um emparelhado com o guard específico que o mitiga:
Vazamento de privilege via inclusão inadvertida de corpo. Uma implementação ingênua de summarize_workflow incluiria o corpo do documento inline. Guard: summarize_workflow roteia todo campo body pelo truncate_body(), que limita em IRONCLAD_TRUNCATE_AT e marca a resposta com _truncated_at. Ampliar isso requer editar um helper, que é o único chokepoint que um revisor de privilege precisa auditar.
Log de consulta de busca que revela estratégia de revisão jurídica. Registrar a string de consulta de search_records criaria um registro descobrível mostrando o que a equipe está procurando — metadados privilegiados em si. Guard: log_invocation() aceita apenas nome da ferramenta e contagem de resultados; a string de consulta nunca é gravada nos logs. Restaurá-la requer uma mudança de código revisada contra a política de privilege.
Falta de atualização de OAuth no scaffold. O scaffold usa um bearer token estático do Ironclad, que não pode ser revogado granularmente quando um advogado sai da empresa. Guard (aberto): o item 2 na lista de TODOs do bundle sinaliza OAuth-com-refresh como uma tarefa pré-produção. Até que isso seja implementado, rotacione o token a cada mudança de pessoal e trate o deploy com token estático como uma postura somente-de-desenvolvimento.
Stack
Servidor MCP Python self-hosted (o scaffold usa o SDK oficial mcp, httpx, pydantic) conversando com a API pública do Ironclad no backend; Claude Desktop ou Code no frontend. Opcional: logging estruturado via python-json-logger encaminhado para sua trilha de auditoria de gestão de matters; exportação Sentry ou OpenTelemetry, com strings de consulta e corpos de documentos removidos antes da transmissão.
# 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())