|
| 1 | +package xtramcp |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "encoding/json" |
| 6 | + "fmt" |
| 7 | + "io" |
| 8 | + "net/http" |
| 9 | + "paperdebugger/internal/libs/db" |
| 10 | + "paperdebugger/internal/services" |
| 11 | + "paperdebugger/internal/services/toolkit/registry" |
| 12 | +) |
| 13 | + |
| 14 | +// MCPToolsResponse represents the JSON-RPC response from your backend |
| 15 | +type MCPToolsResponse struct { |
| 16 | + JSONRPC string `json:"jsonrpc"` |
| 17 | + ID int `json:"id"` |
| 18 | + Result struct { |
| 19 | + Tools []ToolSchema `json:"tools"` |
| 20 | + } `json:"result"` |
| 21 | +} |
| 22 | + |
| 23 | +// loads tools dynamically from backend |
| 24 | +type XtraMCPLoader struct { |
| 25 | + db *db.DB |
| 26 | + projectService *services.ProjectService |
| 27 | + baseURL string |
| 28 | + client *http.Client |
| 29 | + sessionID string // Store the MCP session ID after initialization for re-use |
| 30 | +} |
| 31 | + |
| 32 | +// NewXtraMCPLoader creates a new dynamic XtraMCP loader |
| 33 | +func NewXtraMCPLoader(db *db.DB, projectService *services.ProjectService, baseURL string) *XtraMCPLoader { |
| 34 | + return &XtraMCPLoader{ |
| 35 | + db: db, |
| 36 | + projectService: projectService, |
| 37 | + baseURL: baseURL, |
| 38 | + client: &http.Client{}, |
| 39 | + } |
| 40 | +} |
| 41 | + |
| 42 | +// LoadToolsFromBackend fetches tool schemas from backend and registers them |
| 43 | +func (loader *XtraMCPLoader) LoadToolsFromBackend(toolRegistry *registry.ToolRegistry) error { |
| 44 | + // Initialize MCP session ONCE |
| 45 | + sessionID, err := loader.initializeMCP() |
| 46 | + if err != nil { |
| 47 | + return fmt.Errorf("failed to initialize MCP: %w", err) |
| 48 | + } |
| 49 | + loader.sessionID = sessionID |
| 50 | + |
| 51 | + // Fetch tools from backend using the session (currently returns mock data) |
| 52 | + toolSchemas, err := loader.fetchAvailableTools() |
| 53 | + if err != nil { |
| 54 | + return fmt.Errorf("failed to fetch tools from backend: %w", err) |
| 55 | + } |
| 56 | + |
| 57 | + // Register each tool dynamically, passing the session ID |
| 58 | + for _, toolSchema := range toolSchemas { |
| 59 | + dynamicTool := NewDynamicTool(loader.db, loader.projectService, toolSchema, loader.baseURL, loader.sessionID) |
| 60 | + |
| 61 | + // Register the tool with the registry |
| 62 | + toolRegistry.Register(toolSchema.Name, dynamicTool.Description, dynamicTool.Call) |
| 63 | + |
| 64 | + fmt.Printf("Registered dynamic tool: %s\n", toolSchema.Name) |
| 65 | + } |
| 66 | + |
| 67 | + return nil |
| 68 | +} |
| 69 | + |
| 70 | +// initializeMCP performs the full MCP initialization handshake |
| 71 | +func (loader *XtraMCPLoader) initializeMCP() (string, error) { |
| 72 | + // Step 1: Initialize |
| 73 | + sessionID, err := loader.performInitialize() |
| 74 | + if err != nil { |
| 75 | + return "", fmt.Errorf("step 1 - initialize failed: %w", err) |
| 76 | + } |
| 77 | + |
| 78 | + // Step 2: Send notifications/initialized |
| 79 | + err = loader.sendInitializedNotification(sessionID) |
| 80 | + if err != nil { |
| 81 | + return "", fmt.Errorf("step 2 - notifications/initialized failed: %w", err) |
| 82 | + } |
| 83 | + |
| 84 | + return sessionID, nil |
| 85 | +} |
| 86 | + |
| 87 | +// performInitialize performs MCP initialization (1. establish connection) |
| 88 | +func (loader *XtraMCPLoader) performInitialize() (string, error) { |
| 89 | + initReq := map[string]interface{}{ |
| 90 | + "jsonrpc": "2.0", |
| 91 | + "method": "initialize", |
| 92 | + "id": 1, |
| 93 | + "params": map[string]interface{}{ |
| 94 | + "protocolVersion": "2024-11-05", |
| 95 | + "capabilities": map[string]interface{}{}, |
| 96 | + "clientInfo": map[string]interface{}{ |
| 97 | + "name": "paperdebugger-client", |
| 98 | + "version": "1.0.0", |
| 99 | + }, |
| 100 | + }, |
| 101 | + } |
| 102 | + |
| 103 | + jsonData, err := json.Marshal(initReq) |
| 104 | + if err != nil { |
| 105 | + return "", fmt.Errorf("failed to marshal initialize request: %w", err) |
| 106 | + } |
| 107 | + |
| 108 | + req, err := http.NewRequest("POST", loader.baseURL, bytes.NewBuffer(jsonData)) |
| 109 | + if err != nil { |
| 110 | + return "", fmt.Errorf("failed to create initialize request: %w", err) |
| 111 | + } |
| 112 | + |
| 113 | + req.Header.Set("Content-Type", "application/json") |
| 114 | + req.Header.Set("Accept", "application/json, text/event-stream") |
| 115 | + |
| 116 | + resp, err := loader.client.Do(req) |
| 117 | + if err != nil { |
| 118 | + return "", fmt.Errorf("failed to make initialize request: %w", err) |
| 119 | + } |
| 120 | + defer resp.Body.Close() |
| 121 | + |
| 122 | + // Extract session ID from response headers |
| 123 | + sessionID := resp.Header.Get("mcp-session-id") |
| 124 | + if sessionID == "" { |
| 125 | + return "", fmt.Errorf("no session ID returned from initialize") |
| 126 | + } |
| 127 | + |
| 128 | + return sessionID, nil |
| 129 | +} |
| 130 | + |
| 131 | +// sendInitializedNotification completes MCP initialization (acknowledges initialization) |
| 132 | +func (loader *XtraMCPLoader) sendInitializedNotification(sessionID string) error { |
| 133 | + notifyReq := map[string]interface{}{ |
| 134 | + "jsonrpc": "2.0", |
| 135 | + "method": "notifications/initialized", |
| 136 | + "params": map[string]interface{}{}, |
| 137 | + } |
| 138 | + |
| 139 | + jsonData, err := json.Marshal(notifyReq) |
| 140 | + if err != nil { |
| 141 | + return fmt.Errorf("failed to marshal notification: %w", err) |
| 142 | + } |
| 143 | + |
| 144 | + req, err := http.NewRequest("POST", loader.baseURL, bytes.NewBuffer(jsonData)) |
| 145 | + if err != nil { |
| 146 | + return fmt.Errorf("failed to create notification request: %w", err) |
| 147 | + } |
| 148 | + |
| 149 | + req.Header.Set("Content-Type", "application/json") |
| 150 | + req.Header.Set("Accept", "application/json, text/event-stream") |
| 151 | + req.Header.Set("mcp-session-id", sessionID) |
| 152 | + |
| 153 | + resp, err := loader.client.Do(req) |
| 154 | + if err != nil { |
| 155 | + return fmt.Errorf("failed to send notification: %w", err) |
| 156 | + } |
| 157 | + defer resp.Body.Close() |
| 158 | + |
| 159 | + return nil |
| 160 | +} |
| 161 | + |
| 162 | +// fetchAvailableTools makes a request to get available tools from backend |
| 163 | +func (loader *XtraMCPLoader) fetchAvailableTools() ([]ToolSchema, error) { |
| 164 | + // List all tools using the established session |
| 165 | + requestBody := map[string]interface{}{ |
| 166 | + "jsonrpc": "2.0", |
| 167 | + "method": "tools/list", |
| 168 | + "params": map[string]interface{}{}, |
| 169 | + "id": 2, |
| 170 | + } |
| 171 | + |
| 172 | + jsonData, err := json.Marshal(requestBody) |
| 173 | + if err != nil { |
| 174 | + return nil, fmt.Errorf("failed to marshal request: %w", err) |
| 175 | + } |
| 176 | + |
| 177 | + req, err := http.NewRequest("POST", loader.baseURL, bytes.NewBuffer(jsonData)) |
| 178 | + if err != nil { |
| 179 | + return nil, fmt.Errorf("failed to create request: %w", err) |
| 180 | + } |
| 181 | + |
| 182 | + req.Header.Set("Content-Type", "application/json") |
| 183 | + req.Header.Set("Accept", "application/json, text/event-stream") |
| 184 | + req.Header.Set("mcp-session-id", loader.sessionID) |
| 185 | + |
| 186 | + resp, err := loader.client.Do(req) |
| 187 | + if err != nil { |
| 188 | + return nil, fmt.Errorf("failed to make request: %w", err) |
| 189 | + } |
| 190 | + defer resp.Body.Close() |
| 191 | + |
| 192 | + // Parse response |
| 193 | + var mcpResponse MCPToolsResponse |
| 194 | + err = json.NewDecoder(resp.Body).Decode(&mcpResponse) |
| 195 | + if err != nil { |
| 196 | + return nil, fmt.Errorf("failed to parse response: %w", err) |
| 197 | + } |
| 198 | + |
| 199 | + return mcpResponse.Result.Tools, nil |
| 200 | + |
| 201 | + // mock data; return hardcoded tool schemas for testing |
| 202 | + // mockToolsJSON := `[{"name":"get_user_papers","description":"Fetch all papers published by a specific user identified by email. Supports 'summary' (abstract truncated to 150 words) and 'detailed' (full abstract).","inputSchema":{"properties":{"email":{"description":"Email address of the user whose papers to fetch. Must be a valid email string.","examples":["alice@example.com","bob@university.edu"],"title":"Email","type":"string"},"format":{"default":"detailed","description":"Format of the response. 'summary' shows title, venue, authors, URL, and the first 150 words of the abstract (default). 'detailed' shows the full abstract.","enum":["summary","detailed"],"examples":["summary","detailed"],"title":"Format","type":"string"}},"required":["email"],"title":"get_user_papers_toolArguments","type":"object"},"outputSchema":{"properties":{"result":{"title":"Result","type":"string"}},"required":["result"],"title":"get_user_papers_toolOutput","type":"object"}},{"name":"search_papers_on_openreview","description":"Search for academic papers on OpenReview by keywords within specific conference venues. This tool supports various matching modes and is ideal for discovering recent or broader papers beyond those available in the local database. Use this tool when results from search_relevant_papers are insufficient.","inputSchema":{"properties":{"query":{"description":"Keywords, topics, content, or a chunk of text to search for.","examples":["time series token merging","neural networks"],"title":"Query","type":"string"},"venues":{"description":"List of conference venues and years to search in. Each entry must be a dict with 'venue' and 'year'.","examples":[{"venue":"ICLR.cc","year":"2024"},{"venue":"ICML","year":"2024"},{"venue":"NeurIPS.cc","year":"2023"},{"venue":"NeurIPS.cc","year":"2022"}],"items":{"additionalProperties":{"type":"string"},"type":"object"},"minItems":1,"title":"Venues","type":"array"},"search_fields":{"default":["title","abstract"],"description":"Fields to search within each paper. Options: 'title', 'abstract', 'authors'.","items":{"enum":["title","abstract","authors"],"type":"string"},"title":"Search Fields","type":"array"},"match_mode":{"default":"majority","description":"Match mode:\n- any: At least one keyword must match\n- all: All keywords must match\n- exact: Match the entire phrase exactly\n- majority: Match majority of keywords (>50%)\n- threshold: Match percentage of terms based on 'match_threshold'.","enum":["any","all","exact","majority","threshold"],"title":"Match Mode","type":"string"},"match_threshold":{"default":0.5,"description":"Minimum fraction (0.0-1.0) of search terms that must match when using 'threshold' mode. Example: 0.5 = 50% of terms must match.","maximum":1,"minimum":0,"title":"Match Threshold","type":"number"},"limit":{"default":10,"description":"Maximum number of results to return (1-16).","maximum":16,"minimum":1,"title":"Limit","type":"integer"},"min_score":{"default":0.6,"description":"Minimum match score (0.0-1.0). Lower values allow looser matches; higher values enforce stricter matches.","maximum":1,"minimum":0,"title":"Min Score","type":"number"}},"required":["query","venues"],"title":"search_papers_openreview_toolArguments","type":"object"},"outputSchema":{"properties":{"result":{"title":"Result","type":"string"}},"required":["result"],"title":"search_papers_openreview_toolOutput","type":"object"}},{"name":"search_relevant_papers","description":"Search for similar or relevant papers by keywords against the local database of academic papers. This tool uses semantic search with vector embeddings to find the most relevant results. It is the default and recommended tool for paper searches.","inputSchema":{"properties":{"query":{"description":"Keywords, topics, content, or a chunk of text to search for.","examples":["time series token merging","neural networks","...when trained on first-order Markov chains, transformers with two or more layers consistently develop an induction head mechanism to estimate the in-context bigram conditional distribution"],"title":"Query","type":"string"},"top_k":{"description":"Number of top relevant or similar papers to return.","title":"Top K","type":"integer"},"date_min":{"description":"Minimum publication date (YYYY-MM-DD) to filter papers.","examples":["2023-01-01","2022-06-25"],"title":"Date Min","type":"string"},"date_max":{"description":"Maximum publication date (YYYY-MM-DD) to filter papers.","examples":["2024-12-31","2023-06-25"],"title":"Date Max","type":"string"},"countries":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"description":"List of country codes in ISO ALPHA-3 format to filter papers by author affiliations.","examples":[["USA","CHN","SGP","GBR","DEU","KOR","JPN"]],"title":"Countries"},"min_similarity":{"description":"Minimum similarity score (0.0-1.0) for returned papers. Higher values yield more relevant results but fewer papers.","examples":[0.3,0.5,0.7,0.9],"title":"Min Similarity","type":"number"}},"required":["query","top_k","countries","min_similarity"],"title":"search_papers_toolArguments","type":"object"},"outputSchema":{"properties":{"result":{"title":"Result","type":"string"}},"required":["result"],"title":"search_papers_toolOutput","type":"object"}},{"name":"identify_improvements","description":"Analyzes a draft academic paper against the standards of top-tier ML conferences (ICLR, ICML, NeurIPS). Identifies issues in structure, completeness, clarity, and argumentation, then provides prioritized, actionable suggestions.","inputSchema":{"properties":{"paper_content":{"description":"The full text content of the academic paper draft. Paper content should not be truncated.","title":"Paper Content","type":"string"},"target_venue":{"default":"NeurIPS","description":"The target top-tier conference to tailor the feedback for.","enum":["ICLR","ICML","NeurIPS"],"title":"Target Venue","type":"string"},"focus_areas":{"anyOf":[{"items":{"enum":["Structure","Clarity","Evidence","Positioning","Style","Completeness","Soundness","Limitations"],"type":"string"},"type":"array"},{"type":"null"}],"default":null,"description":"List of specific areas to focus the analysis on. If empty, default areas are: {DEFAULT_FOCUS_AREAS}.","title":"Focus Areas"},"severity_threshold":{"default":"major","description":"The minimum severity level to report. 'major' will show blockers and major issues.","enum":["blocker","major","minor","nit"],"title":"Severity Threshold","type":"string"}},"required":["paper_content"],"title":"identify_improvementsArguments","type":"object"},"outputSchema":{"properties":{"result":{"title":"Result","type":"string"}},"required":["result"],"title":"identify_improvementsOutput","type":"object"}},{"name":"enhance_academic_writing","description":"Suggest context-aware academic paper writing enhancements for selected text.","inputSchema":{"properties":{"full_paper_content":{"description":"Surrounding context from the manuscript (e.g., abstract, background, or several sections). This need not be the entire paper; providing a substantial excerpt helps tailor the tone, terminology, and level of detail to academic venues (journals and conferences).","examples":["In reinforcement learning, one could structure these metrics (previously for evaluation) as rewards that could be boosted during training (Sharma et al., 2021; Yadav et al., 2021; Deng et al., 2022; Liu et al., 2023a; Xu et al., 2024; Wang et al., 2024b), to optimize complex objective functions even at testing time (OpenAI, 2024). However, when reward weights remain static, the weakest metric (the 'short-board') becomes a bottleneck that restricts overall LLM effectiveness, which introduces the short-board effect in multi-reward optimization. For example, in Figure 2, when the scaled reward itself (or its growth trend) has not yet reached saturation, its update magnitude should accordingly be increased."],"title":"Full Paper Content","type":"string"},"selected_content":{"description":"The specific text excerpt selected for improvement from the paper.","examples":["...when the scaled reward itself (or its growth trend) has not yet reached saturation, its update magnitude should accordingly be increased..."],"title":"Selected Content","type":"string"}},"required":["full_paper_content","selected_content"],"title":"improve_academic_passage_toolArguments","type":"object"},"outputSchema":{"properties":{"result":{"title":"Result","type":"string"}},"required":["result"],"title":"improve_academic_passage_toolOutput","type":"object"}}]` |
| 203 | + |
| 204 | + // var mockTools []ToolSchema |
| 205 | + // err := json.Unmarshal([]byte(mockToolsJSON), &mockTools) |
| 206 | +} |
0 commit comments