1426 lines
36 KiB
Markdown
1426 lines
36 KiB
Markdown
# 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
|
|
|
|
```
|
|
|
|
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.*
|