> ## Documentation Index
> Fetch the complete documentation index at: https://docs.meetdoris.com/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Server

> Connect Claude, ChatGPT, Cursor, or any MCP-compatible AI tool directly to your deal intelligence

## Overview

The Doris MCP server exposes the ontology as tools that AI assistants can call directly. Ask questions about deals, commitments, and stakeholders in natural language — the AI translates your question into the right ontology queries automatically.

**Endpoint:** `https://mcp.meetdoris.com/mcp`

**Transport:** Streamable HTTP

**Auth:** OAuth 2.1 via your Doris login (Auth0). On first connection, a browser popup handles authentication.

## Setup

### Claude Code

```bash theme={null}
claude mcp add --transport http doris https://mcp.meetdoris.com/mcp
```

### Claude Desktop

Add to your MCP config (`~/Library/Application Support/Claude/claude_desktop_config.json`):

```json theme={null}
{
  "mcpServers": {
    "doris": {
      "command": "npx",
      "args": ["mcp-remote", "https://mcp.meetdoris.com/mcp"]
    }
  }
}
```

### Cursor

Settings > Features > MCP > Add Server > Streamable HTTP > `https://mcp.meetdoris.com/mcp`

### ChatGPT

Developer Mode > Add Connector > `https://mcp.meetdoris.com/mcp`

### Windsurf

Add to your MCP configuration:

```json theme={null}
{
  "mcpServers": {
    "doris": {
      "serverUrl": "https://mcp.meetdoris.com/mcp"
    }
  }
}
```

## Available Tools

The MCP server exposes 10 tools that mirror the REST API — eight read tools and two write tools:

### ontology\_types

List all entity types or get the schema for a specific type.

| Parameter   | Type   | Required | Description                                              |
| ----------- | ------ | -------- | -------------------------------------------------------- |
| `type_name` | string | No       | Specific type to get schema for. Omit to list all types. |

**Example prompt:** "What entity types are available in Doris?"

### ontology\_resolve

Resolve a single entity by type and ID, with optional expand keys for related data.

| Parameter       | Type      | Required | Description                                                       |
| --------------- | --------- | -------- | ----------------------------------------------------------------- |
| `type_name`     | string    | Yes      | Entity type (e.g., `deal`, `meeting`, `commitment`)               |
| `object_id`     | string    | Yes      | Entity ID                                                         |
| `expand`        | string\[] | No       | Related data to include (e.g., `stakeholders`, `commitments`)     |
| `expand_params` | object    | No       | Per-expand-key options, such as transcript search terms or limits |

**Available expand keys for deals:** `stakeholders`, `commitments`, `objections`, `competitors`, `meetings`, `strategy`, `emails`, `agent_summary`, `activity`, `insights`, `assessments`, `brief`, `pipeline_stages`

**Example prompt:** "Show me the Acme deal with its stakeholders and open commitments"

### ontology\_list

List entities of a specific type with filtering, sorting, and expand support.

| Parameter       | Type      | Required | Description                                                           |
| --------------- | --------- | -------- | --------------------------------------------------------------------- |
| `object_type`   | string    | Yes      | Type to list (e.g., `deal`, `commitment`)                             |
| `filters`       | object    | No       | Filter criteria (e.g., `{"stage": "Discovery"}`)                      |
| `sort`          | string    | No       | Sort field and direction (e.g., `created_at:desc`)                    |
| `expand`        | string\[] | No       | Related data to include                                               |
| `limit`         | number    | No       | Max results (default 25, max 100)                                     |
| `offset`        | number    | No       | Pagination offset                                                     |
| `mine`          | boolean   | No       | Restrict to entities owned by or assigned to you                      |
| `include_links` | boolean   | No       | Include relationship counts for each result                           |
| `changes_since` | string    | No       | Only return entities created or updated after this ISO-8601 timestamp |
| `cursor`        | string    | No       | Keyset pagination cursor from a previous response                     |

**Example prompt:** "List all deals in the Negotiation stage with their commitments"

### ontology\_search

Full-text search across all entity types.

| Parameter       | Type      | Required | Description                                                           |
| --------------- | --------- | -------- | --------------------------------------------------------------------- |
| `query`         | string    | No       | Free-text search query                                                |
| `types`         | string\[] | No       | Restrict search to specific types                                     |
| `filters`       | object    | No       | Filter criteria                                                       |
| `sort`          | string    | No       | Sort field and direction                                              |
| `limit`         | number    | No       | Max results (default 25, max 100)                                     |
| `offset`        | number    | No       | Pagination offset                                                     |
| `mine`          | boolean   | No       | Restrict to entities owned by or assigned to you                      |
| `search_fields` | string\[] | No       | Target search zones such as `title`, `content`, or `body`             |
| `include_links` | boolean   | No       | Include relationship counts for each result                           |
| `changes_since` | string    | No       | Only return entities created or updated after this ISO-8601 timestamp |
| `cursor`        | string    | No       | Keyset pagination cursor from a previous response                     |

**Example prompt:** "Search for anything mentioning 'pricing comparison' across commitments"

### ontology\_batch

Resolve up to 50 entities in a single call.

| Parameter | Type      | Required | Description                                       |
| --------- | --------- | -------- | ------------------------------------------------- |
| `refs`    | object\[] | Yes      | List of `{"type": "...", "id": "..."}` references |

**Example prompt:** "Get details on deals deal\_1, deal\_2, and deal\_3"

### ontology\_traverse

Follow relationships from one entity to discover connected entities.

| Parameter     | Type   | Required | Description                                         |
| ------------- | ------ | -------- | --------------------------------------------------- |
| `source_type` | string | Yes      | Type of the source entity                           |
| `source_id`   | string | Yes      | ID of the source entity                             |
| `link_type`   | string | No       | Filter by relationship type                         |
| `target_type` | string | No       | Filter by target entity type                        |
| `limit`       | number | No       | Max results (default 50, max 200)                   |
| `link_filter` | object | No       | Filter by link properties, such as stakeholder role |

**Example prompt:** "What entities are linked to this deal?"

### ontology\_aggregate

Group-by aggregation over an object type — counts and metrics bucketed by a field.

| Parameter     | Type      | Required | Description                                                                      |
| ------------- | --------- | -------- | -------------------------------------------------------------------------------- |
| `object_type` | string    | Yes      | Type to aggregate (e.g., `deal`, `commitment`)                                   |
| `group_by`    | string    | No       | Field to group results by (e.g., `stage`, `owner_name`)                          |
| `metrics`     | string\[] | No       | Metrics to compute per group (e.g., `sum:amount`); defaults to a per-group count |
| `filters`     | object    | No       | Filter criteria applied before aggregation                                       |
| `mine`        | boolean   | No       | Restrict to entities owned by/assigned to you                                    |
| `time_bucket` | string    | No       | Bucket results by time period (e.g., `month`, `week`)                            |
| `time_field`  | string    | No       | Date field to bucket on when `time_bucket` is set                                |

**Example prompt:** "How many deals are in each stage, and what's the total value per stage?"

### search\_transcripts

Full-text search over meeting transcript turns — find exactly what was said and where.

| Parameter     | Type      | Required | Description                                             |
| ------------- | --------- | -------- | ------------------------------------------------------- |
| `query`       | string    | Yes      | Search terms matched against spoken words               |
| `date_from`   | string    | No       | ISO-8601 lower bound on meeting start time              |
| `date_to`     | string    | No       | ISO-8601 upper bound on meeting start time              |
| `mine`        | boolean   | No       | Restrict to your own meetings; defaults to `true`       |
| `deal_id`     | string    | No       | Restrict to meetings linked to this deal                |
| `meeting_id`  | string    | No       | Restrict to a single meeting                            |
| `meeting_ids` | string\[] | No       | Restrict to several meetings                            |
| `speaker`     | string    | No       | Filter to turns whose speaker name contains this string |
| `limit`       | number    | No       | Max turns to return (default 25)                        |

**Example prompt:** "Every time security came up in the Acme calls, what was said?"

## Write Tools

Writes are a deliberately small, self-describing surface — just two tools. `ontology_update` makes typed field writes; `ontology_action` runs named domain actions (e.g. completing a commitment, moving a plan card). You never need a new tool per entity: capabilities are discovered live through `ontology_types`, which advertises each type's `writable_fields` and `actions` — including each action's name, description, and JSON-Schema `params_schema`. As new writable types and actions ship, an AI assistant learns them automatically the next time it inspects the schema, with no client changes.

<Note>
  Writes require the `ontology:write` scope. Reads only need `ontology:read`, which every token carries; `ontology:write` is granted only when your token actually holds it. A read-only token that calls a write tool is rejected with a `forbidden` error before anything is mutated.
</Note>

### ontology\_update

Write typed fields on an entity. Only the fields a type lists under `writable_fields` are accepted — unknown or read-only fields are rejected (no mass-assignment).

| Parameter   | Type   | Required | Description                                                      |
| ----------- | ------ | -------- | ---------------------------------------------------------------- |
| `type_name` | string | Yes      | The entity type (see `ontology_types` for its `writable_fields`) |
| `object_id` | string | Yes      | The entity ID to update                                          |
| `patch`     | object | Yes      | Field → value map; only the type's writable fields are allowed   |

Returns `{"ok": true, "id": ...}` on success, or an error envelope (e.g. `invalid_request`, `not_found`, `forbidden`).

**Example prompt:** "Push the due date on this commitment to next Friday"

### ontology\_action

Run a named domain action on an entity. Each type advertises its actions (and their parameter schemas) via `ontology_types`.

| Parameter   | Type   | Required | Description                                                |
| ----------- | ------ | -------- | ---------------------------------------------------------- |
| `type_name` | string | Yes      | The entity type                                            |
| `object_id` | string | Yes      | The entity ID the action targets                           |
| `action`    | string | Yes      | The action name (e.g. `complete`, `move`, `add_plan_item`) |
| `params`    | object | No       | Action parameters, per the action's `params_schema`        |

Returns an `{"ok": true, ...}` envelope on success (shape varies by action), or an error envelope.

**First writable types and actions:**

| Type                   | Action          | What it does                                             |
| ---------------------- | --------------- | -------------------------------------------------------- |
| `commitment`           | `complete`      | Mark a commitment completed (no params)                  |
| `commitment`           | `reopen`        | Reopen a completed commitment to pending (no params)     |
| `commitment`           | `update`        | Edit `description`, `owner_type`, or `due_date`          |
| `roundtable`           | `add_plan_item` | Add a card to the mutual action plan                     |
| `roundtable`           | `comment`       | Post a rep comment on a room artifact                    |
| `roundtable_plan_item` | `move`          | Move a plan card to a board column                       |
| `roundtable_plan_item` | `update`        | Edit a plan card's description / owner / due date / seat |
| `roundtable_plan_item` | `remind`        | Nudge the card owner (buyer-facing — see guardrails)     |

**Example prompt:** "Mark the security-review commitment on the Acme deal as done"

### Guardrails

* **No machine touches the buyer.** Actions that could reach a buyer — such as `remind` on a plan card — are flagged `buyer_facing` and are denied to automated/agent callers. MCP calls are treated as agent-initiated, so these actions return `forbidden`; they only run on a human surface where a person initiates them.
* **Tenant-scoped.** Every write is scoped to your tenant. The entity is loaded under your company before any mutation, so a request can never touch another tenant's data — a foreign ID looks identical to a missing one (`not_found`).
* **Role-aware.** Some actions (e.g. all roundtable writes) require the caller to hold the right role at the entity, such as a rep seat at the room.

**Example: complete a commitment via JSON-RPC**

```bash theme={null}
curl -X POST https://mcp.meetdoris.com/mcp \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "ontology_action",
      "arguments": {
        "type_name": "commitment",
        "object_id": "commitment_abc123",
        "action": "complete"
      }
    }
  }'
```

## Example Conversations

Once connected, you interact naturally:

> **You:** "What commitments are overdue on the Acme deal?"
>
> The AI calls `ontology_resolve` with `type_name="deal"`, finds the Acme deal, then expands `commitments` and filters for overdue status.

> **You:** "Prep me for my 2pm with Globex"
>
> The AI calls `ontology_search` to find the Globex deal, then `ontology_resolve` with `expand=["stakeholders", "commitments", "strategy", "meetings"]` to assemble a full prep brief.

> **You:** "Which deals have been in Discovery for more than 30 days?"
>
> The AI calls `ontology_list` with `object_type="deal"` and `filters={"stage": "Discovery"}`, then checks `days_in_stage` on each result.

## Privacy

The MCP server automatically masks personally identifiable information before data reaches your AI tool. This means contact emails, phone numbers, and LinkedIn URLs are never exposed to external LLM providers.

<Note>
  MCP connections always use **masked** mode. This is enforced server-side and cannot be changed per-request. For full or anonymous access, use the [REST API](/privacy) with the `?privacy=` query parameter.
</Note>

| Level                    | Names        | Emails                                                    | Phones                 | LinkedIn |
| ------------------------ | ------------ | --------------------------------------------------------- | ---------------------- | -------- |
| **full**                 | Adam Cross   | [adam.cross@minesoft.com](mailto:adam.cross@minesoft.com) | +44 7700 900123        | Included |
| **masked** (MCP default) | Adam C.      | a\*\*\*@minesoft.com                                      | +44 \*\*\* \*\*\* 0123 | Omitted  |
| **anonymous**            | Evaluator #1 | Omitted                                                   | Omitted                | Omitted  |

### How masking works

* **No setup required** — Masking is on by default for all MCP consumers.
* **Deal owners preserved** — Your team's names and emails on deals remain visible in masked mode, so you always know who owns a deal.
* **Expand keys respected** — Stakeholder and email expansions are masked. Non-PII data (commitments, objections, competitors, strategy) passes through unchanged.
* **Anonymous mode** — Stakeholders are replaced with role-based identifiers (e.g., "Champion #1", "Evaluator #2") and the email expansion is omitted entirely. Only available via the REST API.

### Which fields are masked

| Field                     | Masked                                         | Anonymous                     |
| ------------------------- | ---------------------------------------------- | ----------------------------- |
| `full_name`               | First name + initials (Adam C.)                | Omitted — replaced by role ID |
| `email`                   | First char + domain (a\*\*\*@minesoft.com)     | Omitted                       |
| `phone`                   | Last 4 digits visible (+44 \*\*\* \*\*\* 0123) | Omitted                       |
| `linkedin_url`            | Omitted                                        | Omitted                       |
| `owner_name`              | **Preserved** (your team)                      | Omitted                       |
| `owner_email`             | **Preserved** (your team)                      | Omitted                       |
| `sender` / `sender_email` | Initialed / masked                             | Omitted                       |

### When to use each level

| Level         | Best for                                | Available via             |
| ------------- | --------------------------------------- | ------------------------- |
| **full**      | Internal dashboards, your own analysis  | REST API only             |
| **masked**    | AI assistants, external LLM tools       | MCP (automatic), REST API |
| **anonymous** | Customer-facing exports, shared reports | REST API only             |

## Rate Limits

MCP uses the same tenant-level daily quota as the REST API, with endpoint-specific minute limits:

* **10,000 requests/day** per tenant
* **300 req/min** for resolve, list, and type operations
* **120 req/min** for search
* **60 req/min** for batch

## Local Development

For local testing with `MCP_DEV_MODE=true`:

```bash theme={null}
# Start the backend with dev mode
MCP_DEV_MODE=true make web

# Connect Claude Code to local server
claude mcp add --transport http doris-local http://localhost:5000/mcp
```

Dev mode accepts `dev-token` as a Bearer token without OAuth.
