Skip to main content
Glama
chad-hohn-radai

spotdraft-mcp

README.md
# 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

A4.3/5.0

Scored across 19 tools

Disambiguation5/5

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.

Naming Consistency5/5

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.

Tool Count4/5

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.

Completeness5/5

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.

Maintenance

ActivityStale
ResponsivenessNo issues