spotdraft-mcp
# SpotDraft MCP Server
**Give Claude direct, safety-gated access to your SpotDraft contracts.**
This is a locally-hosted [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server
that wraps the [SpotDraft Public API](https://api.spotdraft.com/api/docs/) so you can query and
drive your SpotDraft workspace -- contracts, templates, counterparties, obligations, analytics --
conversationally from Claude, instead of clicking through the UI or writing one-off scripts.
It ships as **two separate servers built from one codebase**:
- **`spotdraft-read`** -- every read-only operation. Safe to blanket-allow; nothing here has side effects.
- **`spotdraft-write`** -- every mutating operation (create/send/void/invite/etc.). Every single tool
requires your explicit approval on every call -- it cannot be pre-approved or run unattended.
That split exists so you can tell your MCP client "always allow reads, always ask before writes" in
one line each, instead of trusting a single server's internal judgment about what's safe.
---
## What can you do with this?
Once connected, ask Claude things like:
- *"List all contracts pending signature from Acme Corp"*
- *"What's the status of contract T-4821?"*
- *"Show me the obligations tracked on H-102"*
- *"Draft a new NDA from the Standard NDA template for this counterparty"* (write -- will prompt for approval)
- *"Void contract T-991, it was created by mistake"* (write + destructive -- will prompt for approval)
- *"Run an analytics query for all contracts executed last quarter by contract type"*
Claude figures out which SpotDraft API call(s) to make; you approve anything that changes data.
---
## Coverage
This server covers the **v2.1** SpotDraft API family (the current/latest version) plus the small
set of **v1-only endpoints that have no v2.1 successor** (legal intake, native integrations,
obligation types, view-email, webhook-sample) -- **114 operations total**. Superseded `v2` and
old-`v1` endpoints with a `v2.1` replacement are intentionally excluded.
Coverage is delivered two ways:
1. **Curated tools** (~15 read + ~13 write) for the most common contract-lifecycle workflows --
listing/searching contracts, checking status, downloading, creating from a template, sending to
counterparties, voiding, managing counterparties and users, etc. Tight, well-described input
schemas.
2. **A search + execute fallback**, so nothing is out of reach even if it doesn't have a curated
tool:
- `search_actions(intent, access?)` -- keyword-search the full 114-operation catalog.
- `execute_read_request(operationId, ...)` -- call any **read**-classified operation by ID.
- `execute_write_request(operationId, ...)` -- call any **write**-classified operation by ID
(write server only; force-gated like every other write tool).
Re-vendoring a newer OpenAPI spec and re-running `npm run gen:catalog` keeps this catalog current
without hand-editing generated code.
---
## Quick start
### What you'll need
1. **Node.js 20+** and **git**.
2. A **SpotDraft API credential** (Client ID + Client Secret): in SpotDraft, go to
**Settings -> Developer Settings -> Generate API Credential**. If you don't have access to that
page, ask whoever administers your SpotDraft workspace to generate one for you.
3. An MCP-capable client (e.g. [Claude Code](https://claude.ai/code)).
### 1. Clone and build
```bash
git clone <this-repo-url> spotdraft-mcp
cd spotdraft-mcp
npm install
npm run build
```
This produces `dist/index.js`, which both server modes run.
### 2. Configure your MCP client
Add **two** server entries -- one per mode -- pointing at the same built file, with
`SPOTDRAFT_MODE` as the only thing that differs:
```json
{
"mcpServers": {
"spotdraft-read": {
"command": "node",
"args": ["/absolute/path/to/spotdraft-mcp/dist/index.js"],
"env": {
"SPOTDRAFT_MODE": "read",
"SPOTDRAFT_CLIENT_ID": "your-client-id",
"SPOTDRAFT_CLIENT_SECRET": "your-client-secret",
"SPOTDRAFT_REGION": "us"
}
},
"spotdraft-write": {
"command": "node",
"args": ["/absolute/path/to/spotdraft-mcp/dist/index.js"],
"env": {
"SPOTDRAFT_MODE": "write",
"SPOTDRAFT_CLIENT_ID": "your-client-id",
"SPOTDRAFT_CLIENT_SECRET": "your-client-secret",
"SPOTDRAFT_REGION": "us"
}
}
}
}
```
On Windows, double the backslashes in the path (`C:\\Users\\you\\...`).
If you only ever need read access, register just `spotdraft-read`.
### 3. Set your approval rules
In your MCP client's permission settings, add:
```json
{
"permissions": {
"allow": ["mcp__spotdraft-read__*"],
"ask": ["mcp__spotdraft-write__*"]
}
}
```
This blanket-allows every read tool and requires per-call approval for every write tool. Every
write tool also sets `_meta["anthropic/requiresUserInteraction"] = true`, which forces approval on
supporting clients even if a broader "don't ask again" rule is in effect.
### 4. Verify
Ask Claude to "list SpotDraft users" (calls `list_users`, a good connectivity check) and confirm it
returns your workspace roster without any approval prompt. Then ask it to do something mutating
(e.g. draft a contract note) and confirm it *does* prompt for approval.
---
## Configuration reference
| Env var | Required | Purpose |
|---|---|---|
| `SPOTDRAFT_MODE` | yes | `read` or `write`. Selects which tool set this server instance registers. |
| `SPOTDRAFT_CLIENT_ID` | yes | SpotDraft API `client-id`. |
| `SPOTDRAFT_CLIENT_SECRET` | yes | SpotDraft API `client-secret`. Treat like a password -- never logged, never echoed in tool output. |
| `SPOTDRAFT_REGION` | no (default `us`) | `us`, `eu`, `in`, or `me` -- selects the regional API host. |
| `SPOTDRAFT_USER_EMAIL` | no | Optional `user-email` header for per-user attribution. |
| `SPOTDRAFT_BASE_URL` | no | Escape hatch to override the computed base URL entirely. |
Region -> base URL mapping: `us` -> `api.us.spotdraft.com`, `eu` -> `api.eu.spotdraft.com`,
`in` -> `api.in.spotdraft.com`, `me` -> `api.me.spotdraft.com`.
---
## Curated tools
### Read (`spotdraft-read`) -- always safe, never prompts
| Tool | What it does |
|---|---|
| `list_contracts` | Paginated contract search/list with common filters. |
| `get_contract_status` | Display status + name for one contract by composite ID. |
| `get_contract_content` | The contract's text (as plain text, structured segments, or HTML). |
| `get_contract_download_link` | A temporary signed download URL (PDF/DOCX) -- not the file bytes. |
| `list_contract_notes` | Notes/comments on a contract's activity log. |
| `get_contract_obligations` | Obligations (renewal, payment, deliverable reminders) tracked on a contract. |
| `get_contract_key_pointers` | Structured metadata fields recorded on a contract. |
| `list_templates` | Contract templates available in the workspace. |
| `get_template_metadata` | A template's field definitions (for building `create_contract` payloads). |
| `list_contract_types` | Configured contract types and their IDs. |
| `list_counterparties` | Counterparty organizations/individuals, paginated. |
| `get_counterparty` | Full detail for one counterparty. |
| `list_users` | Workspace users (also the recommended connectivity self-test). |
| `get_analytics_catalog` | Tables/columns available to `run_analytics_query`. |
| `run_analytics_query` | Runs a read-only GoogleSQL query against SpotDraft's analytics tables. |
| `search_actions` | Keyword-search the full 114-op catalog (on both servers). |
| `execute_read_request` | Call any read-classified operation by ID (full-coverage fallback). |
### Write (`spotdraft-write`) -- every call requires approval
| Tool | What it does |
|---|---|
| `create_contract` | Creates a contract from a template + contract_data + counterparties. |
| `send_contract_to_counterparties` | Emails a contract to its counterparties. |
| `upload_contract_for_signing` | Uploads an existing document as a new contract headed for signature. |
| `upload_contract_for_review` | Uploads an existing document as a new contract headed for review/redlining. |
| `mark_contract_for_execution` | Advances a contract to the execution/signature stage. |
| `void_contract` | **Destructive.** Permanently voids a contract. |
| `invite_user_to_contract` | Invites a role/user/new-email to a contract with a specific permission level. |
| `create_contract_note` | Adds a note/comment to a contract's activity log. |
| `set_contract_key_pointers` | Creates/updates structured metadata fields on a contract, in bulk. |
| `create_counterparty` | Creates a new counterparty (organization or individual). |
| `add_counterparty_contact` | Adds a contact person to an existing counterparty. |
| `create_webhook` | Registers a webhook subscription for contract/counterparty activity. |
| `create_user` | Invites a new user to the workspace. |
| `execute_write_request` | Call any write-classified operation by ID (full-coverage fallback). |
---
## Security model
- **Two servers, not one with internal flags.** Approval tooling in MCP clients keys off the tool
name `mcp__<server-name>__<tool-name>`, so splitting read/write into two server *names* is what
makes "always allow reads, always ask on writes" expressible as two simple rules.
- **Every write tool force-gates.** In addition to the two-server split, every write tool sets
`_meta["anthropic/requiresUserInteraction"] = true` and `readOnlyHint: false`; `void_contract`
additionally sets `destructiveHint: true`. Read tools set `readOnlyHint: true` and never force-gate.
This is belt-and-suspenders: the server split is the primary control, the tool annotations are a
second, client-enforced layer that can't be bypassed by a broad allow rule.
Any operation reachable through `execute_read_request` / `execute_write_request` is checked
against the same catalog `access` flag server-side, so the fallback path can't be used to sneak a
write through the read server or vice versa.
- **Semantic, not just HTTP-method-based, classification.** A handful of `POST` endpoints with no
side effects (analytics query, contract preview, sidebar query, clickwrap agreement preview,
contract-download-link generation) are classified as **read**. Everything else that isn't a `GET`
is **write**.
- **Credentials never leave your machine and are never logged.** `SPOTDRAFT_CLIENT_SECRET` /
`SPOTDRAFT_CLIENT_ID` are read from environment variables supplied by your MCP client, and are
redacted from any error message or log line before it can be printed or returned to the model.
- **Composite IDs are never mutated.** SpotDraft composite contract IDs (`T-123`, `H-45`) are sent
to the API exactly as given -- never re-prefixed, re-cased, or reformatted.
---
## How it works
```
You -> Claude -> spotdraft-read / spotdraft-write (this server, on your machine) -> SpotDraft API -> Your workspace
```
Both server modes are built from the same `dist/index.js`; `SPOTDRAFT_MODE` decides which tool set
gets registered at startup (see `src/server.ts`). The operation catalog
(`src/generated/catalog.ts`) is generated from a vendored copy of SpotDraft's own OpenAPI spec
(`openapi/spotdraft-openapi.json`) by `scripts/gen-catalog.ts` -- re-run `npm run gen:catalog` after
re-vendoring a newer spec to pick up new operations.
---
## Project structure
```
spotdraft-mcp/
├── package.json # bin: spotdraft-mcp-read / spotdraft-mcp-write; scripts: build/dev/gen:catalog/test
├── tsconfig.json
├── openapi/
│ ├── spotdraft-openapi.json # vendored OpenAPI spec (source of truth for coverage)
│ └── FETCHED.md # provenance / fetch date for the vendored spec
├── scripts/
│ ├── gen-catalog.ts # parses the vendored spec -> src/generated/catalog.ts
│ └── run-tests.mjs # cross-shell test runner (see "Development" below)
├── src/
│ ├── index.ts # entry point for both bin commands
│ ├── server.ts # builds the McpServer per SPOTDRAFT_MODE
│ ├── config.ts # env parsing, region resolution, validation
│ ├── http.ts # SpotDraft HTTP client: auth, retries, redaction
│ ├── generated/catalog.ts # generated operation catalog (do not hand-edit)
│ ├── tools/
│ │ ├── read/ # curated read tool modules
│ │ ├── write/ # curated write tool modules
│ │ ├── shared.ts # registration helper (annotations, error handling, pagination)
│ │ └── fallback.ts # search_actions + execute_read_request + execute_write_request
│ └── util/ (ids.ts, format.ts, errors.ts)
└── test/ # unit tests (node:test)
```
---
## Development
```bash
npm install # install dependencies
npm run gen:catalog # regenerate src/generated/catalog.ts from openapi/spotdraft-openapi.json
npm run build # compile TypeScript -> dist/
npm test # run the unit test suite
npm run dev # run src/index.ts directly via tsx (no build step)
```
`npm test` runs `scripts/run-tests.mjs`, which enumerates `test/*.test.ts` and passes them
explicitly to Node's built-in test runner -- letting the test runner glob-scan the `test/`
directory itself conflicts with the `tsx` ESM loader hook on some Node versions.
---
## License
MIT.
TDQS
Scored across 19 tools
Each tool has a clearly distinct purpose, with detailed descriptions that minimize ambiguity. For example, get_contract_content and get_contract_download_link are explicitly differentiated by output format and use case.
Tool names follow a consistent verb_noun pattern with underscores (e.g., list_contracts, get_contract_content, run_analytics_query). No mixing of conventions, making it easy for an agent to predict naming.
With 19 tools, the server is slightly over the typical well-scoped range of 3-15, but each tool serves a distinct read operation for a comprehensive API. The count is justified by the breadth of the domain.
The tool surface covers a wide range of read operations including contracts, counterparties, templates, analytics, and API catalog search. There are no obvious gaps for the stated read-only scope, and the inclusion of search_actions allows agents to discover additional endpoints.