# Dealspace MCP Server — Technical Specification

**Version:** 0.1 — 2026-02-28  
**Status:** Pre-implementation  
**Companion to:** SPEC.md (core platform specification)

---

## 1. Overview

The Dealspace MCP (Model Context Protocol) server exposes deal context to AI tools. It enables LLMs to assist with deal workflows — reading request status, querying answers, suggesting routing — while maintaining strict RBAC boundaries.

**Core principle:** The AI sees exactly what the authenticated user sees. No privilege escalation, no cross-project leakage, no pre-dataroom content without explicit unlock.

### 1.1 Endpoint

**Production:** `https://dealspace.com/mcp`  
**Staging:** `https://staging.dealspace.com/mcp`

### 1.2 Protocol

- **Transport:** Streamable HTTP (MCP Specification 2025-06-18)
- **Protocol Version:** `2025-06-18`
- **Server Name:** `dealspace`
- **Server Version:** `1.0.0`

---

## 2. Authentication & Session Context

### 2.1 OAuth 2.0 Flow

MCP uses standard OAuth 2.0 Authorization Code flow with PKCE:

| Endpoint | URL |
|----------|-----|
| Authorization | `https://dealspace.com/oauth/authorize` |
| Token Exchange | `https://dealspace.com/oauth/token` |
| UserInfo | `https://dealspace.com/oauth/userinfo` |
| Revocation | `https://dealspace.com/oauth/revoke` |
| Server Metadata | `https://dealspace.com/.well-known/oauth-authorization-server` |
| Protected Resource Metadata | `https://dealspace.com/.well-known/oauth-protected-resource` |

### 2.2 Access Token Scopes

| Scope | Description |
|-------|-------------|
| `read:projects` | List accessible projects |
| `read:workstreams` | Read workstream structure and entries |
| `read:requests` | Read request details and status |
| `read:answers` | Read published answers (respects dataroom gating) |
| `read:events` | Read workflow events and thread history |
| `write:routing` | Suggest routing changes (requires human confirmation) |
| `unlock:pre_dataroom` | Access pre-dataroom content (explicit opt-in) |

Default scope for MCP clients: `read:projects read:workstreams read:requests read:answers read:events write:routing`

The `unlock:pre_dataroom` scope is **never** granted by default — requires explicit user consent at authorization time.

### 2.3 Session Context

Every MCP request includes:
1. **Actor ID** — extracted from OAuth token (the authenticated user)
2. **Project ID** — passed as parameter to project-scoped tools, or session-bound after `set_project`

```http
POST /mcp HTTP/1.1
Host: dealspace.com
Authorization: Bearer {access_token}
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "list_requests",
    "arguments": {
      "project_id": "proj_abc123",
      "workstream": "finance"
    }
  }
}
```

The MCP handler:
1. Extracts access token from `Authorization` header
2. Looks up session → gets `actor_id` and granted scopes
3. Creates `AccessContext{ActorID, ProjectID, Scopes}`
4. Routes to tool handler with context
5. All data access goes through `CheckAccess()` — RBAC enforced at DB layer

### 2.4 Project Binding

Two modes of operation:

**Explicit project_id per call:**
```json
{"name": "list_requests", "arguments": {"project_id": "proj_abc123", "workstream": "finance"}}
```

**Session-bound project (via `set_project`):**
```json
{"name": "set_project", "arguments": {"project_id": "proj_abc123"}}
// subsequent calls omit project_id — uses session binding
{"name": "list_requests", "arguments": {"workstream": "finance"}}
```

Session binding is per-OAuth-session. Token refresh preserves binding. Token revocation clears it.

---

## 3. Gating Mechanism (Pre-Dataroom Tier)

### 3.1 The Problem

Answers go through multiple stages before publication:
```
draft → submitted → approved → rejected → published
```

**Pre-dataroom content** (anything not `Stage = "dataroom"` or status not `"published"`) is sensitive:
- Contains unvetted information
- May reveal negotiation strategy
- Buyers should never see unpublished content

### 3.2 The Solution: Explicit Unlock

By default, MCP tools cannot access pre-dataroom content — even if the user's role theoretically permits it via the web UI.

**Why:** LLMs process data through external inference. Even with zero-retention guarantees, the user should consciously decide: "I want AI assistance on this sensitive content."

**How it works:**

1. **Default state:** `unlock:pre_dataroom` scope NOT in token
2. **Tool behavior:** Pre-dataroom entries filtered at query time
3. **Unlock flow:** User re-authorizes with explicit scope consent
4. **Unlock result:** Token includes `unlock:pre_dataroom`, tools return full data

### 3.3 Scope Check in Tools

```go
func (h *MCPHandler) listRequests(ctx AccessContext, args ListRequestsArgs) (*ListRequestsResult, error) {
    filter := EntryFilter{
        ProjectID:  args.ProjectID,
        Type:       "request",
        Workstream: args.Workstream,
    }
    
    // Gate: unless explicitly unlocked, only show dataroom content
    if !ctx.HasScope("unlock:pre_dataroom") {
        filter.Stage = "dataroom"
        filter.Status = []string{"published"}
    }
    
    entries, err := lib.EntryRead(ctx.ActorID, args.ProjectID, filter)
    if err != nil {
        return nil, err
    }
    // ...
}
```

### 3.4 User Experience

When user asks AI about unpublished content:

```
User: "Show me the draft answers for the IT workstream"

AI: I can only access published content by default. You have draft answers
    in the IT workstream, but viewing them requires explicit authorization.
    
    [Link: Authorize access to unpublished content]
    
    This ensures you consciously decide when AI tools can see sensitive
    pre-publication data.
```

Re-authorization link triggers OAuth flow with `unlock:pre_dataroom` scope. User sees clear consent screen explaining what they're unlocking.

---

## 4. Tool Definitions

### 4.1 Project Tools

#### `list_projects`

List all projects the actor can access.

**Parameters:** None

**Returns:**
```json
{
  "projects": [
    {
      "project_id": "proj_abc123",
      "name": "Acme Corp Acquisition",
      "role": "ib_member",
      "workstreams": ["finance", "legal", "it"],
      "stage": "dataroom",
      "request_count": 142,
      "pending_count": 23
    }
  ]
}
```

**RBAC:** Returns only projects where actor has any role.

---

#### `set_project`

Bind session to a project for subsequent calls.

**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | yes | Project to bind |

**Returns:**
```json
{
  "project_id": "proj_abc123",
  "name": "Acme Corp Acquisition",
  "role": "ib_member",
  "workstreams": ["finance", "legal", "it"]
}
```

**RBAC:** Fails if actor has no access to project.

---

#### `get_project_summary`

High-level deal status — completion rates, blockers, timeline.

**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | no | Defaults to session-bound project |

**Returns:**
```json
{
  "project_id": "proj_abc123",
  "name": "Acme Corp Acquisition",
  "stage": "dataroom",
  "workstreams": [
    {
      "name": "finance",
      "total_requests": 45,
      "completed": 38,
      "pending": 5,
      "overdue": 2,
      "completion_pct": 84.4
    }
  ],
  "overall_completion_pct": 78.2,
  "blockers": [
    {"workstream": "legal", "request_ref": "LEG-012", "reason": "awaiting external counsel"}
  ],
  "next_milestone": "Buyer access opens 2026-03-15"
}
```

**RBAC:** 
- IB roles: full summary
- Seller roles: seller-relevant workstreams only
- Buyer roles: published answer counts only (no pending/overdue visibility)

---

### 4.2 Workstream Tools

#### `list_workstreams`

List workstreams in a project with role-appropriate detail.

**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | no | Defaults to session-bound project |

**Returns:**
```json
{
  "workstreams": [
    {
      "workstream_id": "ws_fin001",
      "name": "finance",
      "display_name": "Finance & Accounting",
      "role": "ib_member",
      "request_lists": [
        {"list_id": "rl_001", "name": "Initial Request List", "request_count": 42}
      ]
    }
  ]
}
```

**RBAC:** Only workstreams where actor has explicit access.

---

#### `get_workstream_summary`

Detailed workstream status.

**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | no | Defaults to session-bound project |
| `workstream` | string | yes | Workstream slug (e.g., "finance") |

**Returns:**
```json
{
  "workstream": "finance",
  "display_name": "Finance & Accounting",
  "status": {
    "total_requests": 45,
    "by_status": {
      "open": 3,
      "assigned": 5,
      "answered": 8,
      "vetted": 6,
      "published": 23
    },
    "overdue": [
      {"ref": "FIN-042", "title": "Audited financials FY2024", "due": "2026-03-01", "days_overdue": 3}
    ]
  },
  "recent_activity": [
    {"ts": 1709123456, "actor": "Jane Smith", "action": "published", "ref": "FIN-039"}
  ]
}
```

**RBAC:**
- IB/Seller with workstream access: full detail
- Buyers: published counts only
- Pre-dataroom gating applies to overdue/activity details

---

### 4.3 Request Tools

#### `list_requests`

List requests in a workstream, filtered by status.

**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | no | Defaults to session-bound project |
| `workstream` | string | yes | Workstream slug |
| `status` | string[] | no | Filter by status (default: all visible) |
| `assigned_to` | string | no | Filter by assignee user_id |
| `priority` | string | no | Filter by priority |
| `limit` | int | no | Max results (default: 50, max: 200) |
| `offset` | int | no | Pagination offset |

**Returns:**
```json
{
  "requests": [
    {
      "entry_id": "ent_req001",
      "ref": "FIN-042",
      "title": "Audited financials FY2024",
      "status": "assigned",
      "priority": "high",
      "due_date": "2026-03-15",
      "assigned_to": [{"user_id": "usr_001", "name": "John Doe"}],
      "origin": "buyer",
      "answer_count": 0
    }
  ],
  "total": 45,
  "offset": 0,
  "limit": 50
}
```

**RBAC:**
- Pre-dataroom gating filters unless unlocked
- Buyers see only their own requests + published requests

---

#### `get_request`

Full detail on a single request, including linked answers and thread.

**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | no | Defaults to session-bound project |
| `request_id` | string | yes | Entry ID or ref (e.g., "FIN-042") |
| `include_thread` | bool | no | Include entry_events thread (default: false) |

**Returns:**
```json
{
  "entry_id": "ent_req001",
  "ref": "FIN-042",
  "title": "Audited financials FY2024",
  "body": "Please provide audited financial statements for fiscal year 2024, including...",
  "status": "answered",
  "priority": "high",
  "due_date": "2026-03-15",
  "assigned_to": [{"user_id": "usr_001", "name": "John Doe"}],
  "routing_chain": [
    {"actor": "IB Analyst", "action": "forwarded", "ts": 1709000000},
    {"actor": "CFO", "action": "forwarded", "ts": 1709100000},
    {"actor": "John Doe", "action": "assigned", "ts": 1709150000}
  ],
  "answers": [
    {"answer_id": "ent_ans001", "status": "submitted", "submitted_at": 1709200000}
  ],
  "thread": [
    {"ts": 1709000000, "actor": "Jane Smith", "action": "created", "channel": "web"},
    {"ts": 1709100000, "actor": "Bob CFO", "action": "message", "body": "John, can you handle this?"}
  ]
}
```

**RBAC:**
- Routing chain visible to IB only
- Thread visible based on workstream access
- Pre-dataroom gating applies

---

#### `get_my_tasks`

Personal task inbox — requests assigned to the actor.

**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | no | Defaults to session-bound project |
| `status` | string[] | no | Filter by status |
| `workstream` | string | no | Filter by workstream |

**Returns:**
```json
{
  "tasks": [
    {
      "entry_id": "ent_req001",
      "ref": "FIN-042",
      "title": "Audited financials FY2024",
      "workstream": "finance",
      "status": "assigned",
      "priority": "high",
      "due_date": "2026-03-15",
      "return_to": {"user_id": "usr_cfo", "name": "Bob CFO"},
      "days_until_due": 15
    }
  ],
  "overdue_count": 2,
  "due_today_count": 1
}
```

**RBAC:** Always filtered to `assignee_id = actor_id`.

---

### 4.4 Answer Tools

#### `list_answers`

List answers in a workstream or linked to a request.

**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | no | Defaults to session-bound project |
| `workstream` | string | no | Filter by workstream |
| `request_id` | string | no | Filter by linked request |
| `status` | string[] | no | Filter by status |
| `limit` | int | no | Max results |
| `offset` | int | no | Pagination offset |

**Returns:**
```json
{
  "answers": [
    {
      "entry_id": "ent_ans001",
      "title": "FY2024 Audited Financials",
      "status": "published",
      "linked_requests": ["FIN-042", "FIN-043"],
      "file_count": 3,
      "published_at": 1709300000
    }
  ],
  "total": 23,
  "offset": 0
}
```

**RBAC:**
- Pre-dataroom gating: only `status=published` unless unlocked
- Buyers: only published answers they're entitled to see

---

#### `get_answer`

Full answer detail including files and linked requests.

**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | no | Defaults to session-bound project |
| `answer_id` | string | yes | Answer entry ID |
| `include_files` | bool | no | Include file metadata (default: true) |

**Returns:**
```json
{
  "entry_id": "ent_ans001",
  "title": "FY2024 Audited Financials",
  "body": "Attached are the audited financial statements for FY2024...",
  "status": "published",
  "linked_requests": [
    {"ref": "FIN-042", "title": "Audited financials FY2024", "confirmed": true},
    {"ref": "FIN-043", "title": "Annual financial report", "confirmed": true, "ai_score": 0.89}
  ],
  "files": [
    {"object_id": "obj_001", "filename": "AuditedFinancials_FY2024.pdf", "size": 2456789, "mime": "application/pdf"},
    {"object_id": "obj_002", "filename": "BalanceSheet.xlsx", "size": 156789, "mime": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}
  ],
  "vetting": {
    "vetted_by": "Jane Smith",
    "vetted_at": 1709250000,
    "notes": "Verified against source documents"
  },
  "published_at": 1709300000
}
```

**RBAC:**
- Pre-dataroom gating applies
- Vetting details visible to IB only

---

### 4.5 Routing Tools (Write — Human Confirmation Required)

#### `suggest_routing`

AI suggests how to route a request through the organization.

**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | no | Defaults to session-bound project |
| `request_id` | string | yes | Request to route |
| `analysis` | string | no | AI's reasoning (stored for audit) |

**Returns:**
```json
{
  "suggestion_id": "sug_001",
  "request_ref": "FIN-042",
  "suggested_route": [
    {"user_id": "usr_cfo", "name": "Bob CFO", "reason": "CFO oversight required for audit materials"},
    {"user_id": "usr_acct", "name": "John Accountant", "reason": "Primary preparer of financial statements"}
  ],
  "confidence": 0.87,
  "status": "pending_confirmation",
  "confirmation_url": "https://dealspace.com/confirm/sug_001"
}
```

**RBAC:**
- Requires `write:routing` scope
- Suggestion is **not applied** — human must confirm via web UI
- Logged to audit table with AI analysis

**State:** Creates `routing_suggestion` entry, status `pending`. No changes to request until human clicks confirm.

---

#### `suggest_assignment`

AI suggests who should handle a request based on workload and expertise.

**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | no | Defaults to session-bound project |
| `request_id` | string | yes | Request to assign |
| `analysis` | string | no | AI's reasoning |

**Returns:**
```json
{
  "suggestion_id": "sug_002",
  "request_ref": "LEG-007",
  "suggested_assignee": {
    "user_id": "usr_legal01",
    "name": "Sarah Lawyer",
    "reason": "Handled similar IP requests, current workload: 3 open tasks"
  },
  "alternatives": [
    {"user_id": "usr_legal02", "name": "Tom Counsel", "reason": "Available but less experience with IP"}
  ],
  "status": "pending_confirmation"
}
```

**RBAC:** Same as `suggest_routing` — write scope, human confirmation required.

---

### 4.6 Matching Tools

#### `find_matching_answers`

AI finds existing answers that might satisfy a new buyer request.

**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | no | Defaults to session-bound project |
| `request_id` | string | yes | The new buyer request |
| `threshold` | float | no | Minimum similarity score (default: 0.72) |

**Returns:**
```json
{
  "request_ref": "BUY-015",
  "request_title": "Provide employee headcount by department",
  "matches": [
    {
      "answer_id": "ent_ans042",
      "answer_title": "HR Overview - Headcount Analysis",
      "similarity": 0.89,
      "match_reason": "Contains departmental headcount breakdown",
      "status": "published"
    },
    {
      "answer_id": "ent_ans038",
      "answer_title": "Organizational Structure",
      "similarity": 0.74,
      "match_reason": "Includes department hierarchy with team sizes",
      "status": "published"
    }
  ],
  "recommendation": "High confidence match found. Suggest linking BUY-015 to answer ent_ans042."
}
```

**RBAC:**
- Only searches published answers (pre-dataroom never matched against buyer requests)
- Results filtered by buyer's workstream access

---

#### `suggest_answer_link`

AI suggests linking an answer to requests it satisfies.

**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | no | Defaults to session-bound project |
| `answer_id` | string | yes | Answer to link |
| `request_ids` | string[] | yes | Requests to link |
| `analysis` | string | no | AI's reasoning |

**Returns:**
```json
{
  "suggestion_id": "sug_003",
  "answer_id": "ent_ans042",
  "proposed_links": [
    {"request_ref": "BUY-015", "confidence": 0.89},
    {"request_ref": "BUY-022", "confidence": 0.76}
  ],
  "status": "pending_confirmation"
}
```

**RBAC:** Requires `write:routing` scope, human confirmation.

---

### 4.7 Event Tools

#### `list_events`

List workflow events for an entry or project.

**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | no | Defaults to session-bound project |
| `entry_id` | string | no | Filter to specific entry |
| `actor_id` | string | no | Filter by actor |
| `action` | string[] | no | Filter by action type |
| `since` | int | no | Unix timestamp, events after |
| `limit` | int | no | Max results |

**Returns:**
```json
{
  "events": [
    {
      "event_id": "evt_001",
      "entry_id": "ent_req001",
      "entry_ref": "FIN-042",
      "actor": {"user_id": "usr_001", "name": "John Doe"},
      "channel": "web",
      "action": "submitted",
      "ts": 1709200000,
      "summary": "Submitted answer with 3 files"
    }
  ],
  "total": 156
}
```

**RBAC:**
- Events filtered by entry access
- Internal routing events visible to IB only

---

### 4.8 System Tools

#### `get_version`

Server version and capabilities.

**Parameters:** None

**Returns:**
```json
{
  "server": "dealspace",
  "version": "1.0.0",
  "protocol": "2025-06-18",
  "capabilities": {
    "tools": true,
    "resources": false,
    "prompts": false
  }
}
```

---

## 5. System Prompt Injection

When an MCP session is established, the server injects context about the deal into the AI's system prompt.

### 5.1 Injected Context

```
You are assisting with the M&A deal: "{project_name}"

Deal Stage: {stage}
Your Role: {actor_role}
Accessible Workstreams: {workstream_list}

Deal Context:
- {total_requests} total requests across all workstreams
- {completion_pct}% overall completion
- {pending_count} requests pending response
- {overdue_count} requests overdue

Your Capabilities:
- Read request status, answers, and workflow history
- Query across workstreams you have access to
- Suggest routing and assignments (requires human confirmation)
- Find matching answers for new requests

Restrictions:
- You cannot see pre-dataroom content unless explicitly unlocked
- You cannot execute state changes — only suggest them
- You cannot see content from other projects
- All actions are logged for audit

{role_specific_guidance}
```

### 5.2 Role-Specific Guidance

**For IB roles:**
```
As an IB team member, you can:
- View full routing chains and internal discussions
- See pending/draft answers before publication
- Suggest routing through seller organization
- Monitor deal progress across all accessible workstreams

Focus on: vetting quality, timeline management, buyer request triage
```

**For Seller roles:**
```
As a seller team member, you can:
- View requests assigned to your organization
- Track answer submission and vetting status
- See feedback from IB on rejected answers

Focus on: timely responses, complete documentation, addressing feedback
```

**For Buyer roles:**
```
As a buyer, you can:
- View published data room content
- Submit new requests
- Track status of your requests

You cannot see: internal routing, unpublished answers, other buyers' requests
```

### 5.3 Dynamic Context Updates

On each tool call, the server may inject additional context:

```
[Context Update]
Request FIN-042 status changed: assigned → answered
Answer submitted by John Doe at 2026-03-15 14:30 UTC
```

---

## 6. Autonomous vs Human Confirmation

### 6.1 Fully Autonomous (No Confirmation)

| Action | Rationale |
|--------|-----------|
| List projects/workstreams/requests | Read-only, RBAC enforced |
| Get entry details | Read-only |
| Query answers | Read-only |
| Find matching answers | Computation only, no state change |
| List events | Read-only audit trail |
| Get version | System info |

### 6.2 Requires Human Confirmation

| Action | Confirmation Flow |
|--------|-------------------|
| `suggest_routing` | Creates pending suggestion → user clicks confirm in web UI |
| `suggest_assignment` | Creates pending suggestion → user clicks confirm |
| `suggest_answer_link` | Creates pending link → IB confirms before answer broadcast |

**Why human confirmation:**
- Routing affects people's task inboxes
- Assignment creates work obligations
- Answer links trigger notifications to buyers
- Mistakes in M&A have real consequences

### 6.3 Never Allowed via MCP

| Action | Why |
|--------|-----|
| Create/delete entries | Too consequential for AI autonomy |
| Publish answers | Legal/compliance implications |
| Grant/revoke access | Security-critical |
| Modify files | Document integrity |
| Send notifications | External communication |

These actions require direct web UI interaction with full audit context.

---

## 7. Go Implementation Sketch

### 7.1 Package Structure

```
dealspace/
  mcp/
    server.go       // HTTP handler, JSON-RPC routing
    tools.go        // Tool implementations
    context.go      // AccessContext, scope handling
    inject.go       // System prompt generation
    types.go        // MCP-specific types
```

### 7.2 Core Types

```go
// mcp/context.go
package mcp

type AccessContext struct {
    ActorID   string
    ProjectID string   // session-bound or per-call
    Scopes    []string // from OAuth token
    Role      string   // resolved from access table
}

func (c *AccessContext) HasScope(scope string) bool {
    for _, s := range c.Scopes {
        if s == scope {
            return true
        }
    }
    return false
}

func (c *AccessContext) CanAccessPreDataroom() bool {
    return c.HasScope("unlock:pre_dataroom")
}
```

### 7.3 Server Handler

```go
// mcp/server.go
package mcp

import (
    "encoding/json"
    "net/http"
    
    "dealspace/lib"
)

type MCPServer struct {
    db *lib.DB
}

func (s *MCPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Extract OAuth token
    token := extractBearerToken(r)
    if token == "" {
        writeError(w, "unauthorized", "missing access token")
        return
    }
    
    // Resolve session
    session, err := s.db.GetOAuthSession(token)
    if err != nil {
        writeError(w, "unauthorized", "invalid token")
        return
    }
    
    // Build access context
    ctx := AccessContext{
        ActorID:   session.UserID,
        ProjectID: session.BoundProjectID, // may be empty
        Scopes:    session.Scopes,
    }
    
    // Parse JSON-RPC request
    var req JSONRPCRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, "parse_error", err.Error())
        return
    }
    
    // Route to handler
    result, err := s.dispatch(ctx, req)
    if err != nil {
        writeJSONRPCError(w, req.ID, err)
        return
    }
    
    writeJSONRPCResult(w, req.ID, result)
}

func (s *MCPServer) dispatch(ctx AccessContext, req JSONRPCRequest) (any, error) {
    switch req.Method {
    case "tools/list":
        return s.listTools(ctx)
    case "tools/call":
        return s.callTool(ctx, req.Params)
    case "initialize":
        return s.initialize(ctx, req.Params)
    default:
        return nil, &MCPError{Code: -32601, Message: "method not found"}
    }
}
```

### 7.4 Tool Implementations

```go
// mcp/tools.go
package mcp

import (
    "dealspace/lib"
)

func (s *MCPServer) callTool(ctx AccessContext, params json.RawMessage) (any, error) {
    var call ToolCall
    if err := json.Unmarshal(params, &call); err != nil {
        return nil, err
    }
    
    switch call.Name {
    case "list_projects":
        return s.listProjects(ctx)
    case "set_project":
        return s.setProject(ctx, call.Arguments)
    case "list_requests":
        return s.listRequests(ctx, call.Arguments)
    case "get_request":
        return s.getRequest(ctx, call.Arguments)
    case "get_my_tasks":
        return s.getMyTasks(ctx, call.Arguments)
    case "list_answers":
        return s.listAnswers(ctx, call.Arguments)
    case "suggest_routing":
        return s.suggestRouting(ctx, call.Arguments)
    case "find_matching_answers":
        return s.findMatchingAnswers(ctx, call.Arguments)
    // ... other tools
    default:
        return nil, &MCPError{Code: -32602, Message: "unknown tool: " + call.Name}
    }
}

func (s *MCPServer) listRequests(ctx AccessContext, args json.RawMessage) (*ListRequestsResult, error) {
    var a ListRequestsArgs
    if err := json.Unmarshal(args, &a); err != nil {
        return nil, err
    }
    
    // Resolve project
    projectID := a.ProjectID
    if projectID == "" {
        projectID = ctx.ProjectID
    }
    if projectID == "" {
        return nil, &MCPError{Code: -32602, Message: "project_id required"}
    }
    
    // Build filter with gating
    filter := lib.EntryFilter{
        ProjectID:  projectID,
        Type:       "request",
        Workstream: a.Workstream,
    }
    
    // Pre-dataroom gating
    if !ctx.CanAccessPreDataroom() {
        filter.Stage = "dataroom"
    }
    
    if len(a.Status) > 0 {
        filter.Status = a.Status
    }
    
    // RBAC-enforced query
    entries, err := lib.EntryRead(ctx.ActorID, projectID, filter)
    if err != nil {
        return nil, err
    }
    
    // Transform to result
    result := &ListRequestsResult{
        Requests: make([]RequestSummary, 0, len(entries)),
    }
    for _, e := range entries {
        result.Requests = append(result.Requests, entryToRequestSummary(e))
    }
    
    return result, nil
}

func (s *MCPServer) suggestRouting(ctx AccessContext, args json.RawMessage) (*RoutingSuggestion, error) {
    // Require write:routing scope
    if !ctx.HasScope("write:routing") {
        return nil, &MCPError{Code: -32600, Message: "write:routing scope required"}
    }
    
    var a SuggestRoutingArgs
    if err := json.Unmarshal(args, &a); err != nil {
        return nil, err
    }
    
    // Verify access to request
    request, err := lib.EntryReadOne(ctx.ActorID, a.ProjectID, a.RequestID)
    if err != nil {
        return nil, err
    }
    
    // Create suggestion (does NOT apply it)
    suggestion := &RoutingSuggestion{
        SuggestionID: lib.NewID("sug"),
        RequestRef:   request.Data.Ref,
        Status:       "pending_confirmation",
        Analysis:     a.Analysis,
        CreatedAt:    time.Now().Unix(),
        CreatedBy:    ctx.ActorID,
    }
    
    // AI routing logic would go here
    suggestion.SuggestedRoute = s.computeRoutingSuggestion(ctx, request)
    
    // Persist suggestion
    if err := lib.SaveRoutingSuggestion(suggestion); err != nil {
        return nil, err
    }
    
    // Audit log
    lib.AuditLog(ctx.ActorID, a.ProjectID, "routing_suggestion_created", suggestion.SuggestionID, a.Analysis)
    
    return suggestion, nil
}
```

### 7.5 System Prompt Injection

```go
// mcp/inject.go
package mcp

import (
    "fmt"
    "strings"
    
    "dealspace/lib"
)

func (s *MCPServer) generateSystemPrompt(ctx AccessContext) string {
    project, _ := lib.GetProject(ctx.ActorID, ctx.ProjectID)
    if project == nil {
        return basePrompt
    }
    
    // Get role
    role, _ := lib.GetUserRole(ctx.ActorID, ctx.ProjectID)
    
    // Get accessible workstreams
    workstreams, _ := lib.GetAccessibleWorkstreams(ctx.ActorID, ctx.ProjectID)
    
    // Get stats
    stats, _ := lib.GetProjectStats(ctx.ActorID, ctx.ProjectID)
    
    prompt := fmt.Sprintf(`You are assisting with the M&A deal: "%s"

Deal Stage: %s
Your Role: %s
Accessible Workstreams: %s

Deal Context:
- %d total requests across all workstreams
- %.1f%% overall completion
- %d requests pending response
- %d requests overdue

Your Capabilities:
- Read request status, answers, and workflow history
- Query across workstreams you have access to
- Suggest routing and assignments (requires human confirmation)
- Find matching answers for new requests

Restrictions:
- You cannot see pre-dataroom content unless explicitly unlocked
- You cannot execute state changes — only suggest them
- You cannot see content from other projects
- All actions are logged for audit

%s`,
        project.Name,
        project.Stage,
        role,
        strings.Join(workstreams, ", "),
        stats.TotalRequests,
        stats.CompletionPct,
        stats.PendingCount,
        stats.OverdueCount,
        roleGuidance(role),
    )
    
    return prompt
}

func roleGuidance(role string) string {
    switch {
    case strings.HasPrefix(role, "ib_"):
        return ibGuidance
    case strings.HasPrefix(role, "seller_"):
        return sellerGuidance
    case strings.HasPrefix(role, "buyer_"):
        return buyerGuidance
    default:
        return ""
    }
}

const ibGuidance = `As an IB team member, you can:
- View full routing chains and internal discussions
- See pending/draft answers before publication
- Suggest routing through seller organization
- Monitor deal progress across all accessible workstreams

Focus on: vetting quality, timeline management, buyer request triage`

const sellerGuidance = `As a seller team member, you can:
- View requests assigned to your organization
- Track answer submission and vetting status
- See feedback from IB on rejected answers

Focus on: timely responses, complete documentation, addressing feedback`

const buyerGuidance = `As a buyer, you can:
- View published data room content
- Submit new requests
- Track status of your requests

You cannot see: internal routing, unpublished answers, other buyers' requests`
```

### 7.6 Matching Implementation

```go
// mcp/matching.go
package mcp

import (
    "dealspace/lib"
)

func (s *MCPServer) findMatchingAnswers(ctx AccessContext, args json.RawMessage) (*MatchingResult, error) {
    var a FindMatchingArgs
    if err := json.Unmarshal(args, &a); err != nil {
        return nil, err
    }
    
    // Get the request
    request, err := lib.EntryReadOne(ctx.ActorID, a.ProjectID, a.RequestID)
    if err != nil {
        return nil, err
    }
    
    // Get request embedding
    requestEmbed, err := lib.GetOrCreateEmbedding(request.EntryID, request.Data.Title + " " + request.Data.Body)
    if err != nil {
        return nil, err
    }
    
    threshold := a.Threshold
    if threshold == 0 {
        threshold = 0.72 // default from SPEC.md
    }
    
    // Find published answers in same workstream
    filter := lib.EntryFilter{
        ProjectID:  a.ProjectID,
        Type:       "answer",
        Stage:      "dataroom",
        Status:     []string{"published"},
        Workstream: request.Workstream,
    }
    
    answers, err := lib.EntryRead(ctx.ActorID, a.ProjectID, filter)
    if err != nil {
        return nil, err
    }
    
    // Score each answer
    var matches []AnswerMatch
    for _, ans := range answers {
        ansEmbed, err := lib.GetOrCreateEmbedding(ans.EntryID, ans.Data.Title + " " + ans.Data.Body)
        if err != nil {
            continue
        }
        
        score := lib.CosineSimilarity(requestEmbed, ansEmbed)
        if score >= threshold {
            matches = append(matches, AnswerMatch{
                AnswerID:    ans.EntryID,
                AnswerTitle: ans.Data.Title,
                Similarity:  score,
                MatchReason: generateMatchReason(request, ans, score),
                Status:      ans.Data.Status,
            })
        }
    }
    
    // Sort by similarity descending
    sort.Slice(matches, func(i, j int) bool {
        return matches[i].Similarity > matches[j].Similarity
    })
    
    result := &MatchingResult{
        RequestRef:   request.Data.Ref,
        RequestTitle: request.Data.Title,
        Matches:      matches,
    }
    
    if len(matches) > 0 && matches[0].Similarity >= 0.85 {
        result.Recommendation = fmt.Sprintf(
            "High confidence match found. Suggest linking %s to answer %s.",
            request.Data.Ref, matches[0].AnswerID,
        )
    }
    
    return result, nil
}
```

---

## 8. Security Considerations

### 8.1 Audit Trail

Every MCP call logged:

```go
lib.AuditLog(ctx.ActorID, projectID, "mcp_tool_call", map[string]any{
    "tool":       toolName,
    "args":       sanitizedArgs, // no raw content
    "result_count": len(results),
    "ip":         r.RemoteAddr,
    "user_agent": r.UserAgent(),
})
```

### 8.2 Rate Limiting

- Per-user: 100 requests/minute
- Per-project: 1000 requests/minute
- Embedding generation: 10/minute (expensive)

### 8.3 Data Minimization

- Tool results include IDs and summaries, not full content blobs
- File content never returned via MCP (only metadata + download URLs)
- Thread bodies truncated to 1000 chars in list views

### 8.4 Token Lifetime

- Access tokens: 1 hour
- Refresh tokens: 7 days
- `unlock:pre_dataroom` tokens: 15 minutes (forces re-consent)

---

## 9. Error Handling

### 9.1 Error Codes

| Code | Meaning |
|------|---------|
| -32600 | Invalid request (malformed JSON-RPC) |
| -32601 | Method not found |
| -32602 | Invalid params |
| -32603 | Internal error |
| 1001 | Unauthorized (invalid/expired token) |
| 1002 | Forbidden (RBAC denied) |
| 1003 | Not found |
| 1004 | Scope required |
| 1005 | Rate limited |

### 9.2 Error Response Format

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": 1002,
    "message": "Access denied to workstream: legal",
    "data": {
      "required_role": "ib_member or seller_member",
      "current_role": "buyer_member"
    }
  }
}
```

---

## 10. Testing

### 10.1 Manual Test (curl)

```bash
# Get token
TOKEN=$(curl -s -X POST https://dealspace.com/oauth/token \
  -d "grant_type=client_credentials&client_id=test&client_secret=test" \
  | jq -r .access_token)

# List projects
curl -X POST https://dealspace.com/mcp \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {"name": "list_projects", "arguments": {}}
  }'
```

### 10.2 Claude Desktop Integration

```json
{
  "mcpServers": {
    "dealspace": {
      "url": "https://dealspace.com/mcp",
      "transport": {"type": "http"}
    }
  }
}
```

---

*This document is the MCP specification. If implementation disagrees with this spec, the implementation is wrong.*
