Skip to content

Commit 622ed60

Browse files
committed
feat: Add XtraMCP loader to handle init and ack
1 parent 5260f83 commit 622ed60

1 file changed

Lines changed: 206 additions & 0 deletions

File tree

  • internal/services/toolkit/tools/xtramcp
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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

Comments
 (0)