# 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.*