From 39c9c8b5636819560d48cd420e948661d5400994 Mon Sep 17 00:00:00 2001 From: 4ndrelim Date: Mon, 29 Dec 2025 03:19:56 +0800 Subject: [PATCH 1/7] feat: update loader to inject security context --- .../services/toolkit/tools/xtramcp/loader.go | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/internal/services/toolkit/tools/xtramcp/loader.go b/internal/services/toolkit/tools/xtramcp/loader.go index a47b95c..3f33f16 100644 --- a/internal/services/toolkit/tools/xtramcp/loader.go +++ b/internal/services/toolkit/tools/xtramcp/loader.go @@ -53,17 +53,45 @@ func (loader *XtraMCPLoader) LoadToolsFromBackend(toolRegistry *registry.ToolReg // Register each tool dynamically, passing the session ID for _, toolSchema := range toolSchemas { - dynamicTool := NewDynamicTool(loader.db, loader.projectService, toolSchema, loader.baseURL, loader.sessionID) + // some tools require secrutiy context injection e.g. user_id to authenticate + requiresInjection := loader.requiresSecurityInjection(toolSchema) + + dynamicTool := NewDynamicTool( + loader.db, + loader.projectService, + toolSchema, + loader.baseURL, + loader.sessionID, + requiresInjection, + ) // Register the tool with the registry toolRegistry.Register(toolSchema.Name, dynamicTool.Description, dynamicTool.Call) - fmt.Printf("Registered dynamic tool: %s\n", toolSchema.Name) + if requiresInjection { + fmt.Printf("Registered dynamic tool with security injection: %s\n", toolSchema.Name) + } else { + fmt.Printf("Registered dynamic tool: %s\n", toolSchema.Name) + } } return nil } +// checks if a tool schema contains parameters that should be inejected instead of LLM-generated +func (loader *XtraMCPLoader) requiresSecurityInjection(schema ToolSchema) bool { + properties, ok := schema.InputSchema["properties"].(map[string]interface{}) + if !ok { + return false + } + + // injected parameters + _, hasUserId := properties["user_id"] + _, hasProjectId := properties["project_id"] + + return hasUserId || hasProjectId +} + // InitializeMCP performs the full MCP initialization handshake, stores session ID, and returns it func (loader *XtraMCPLoader) InitializeMCP() (string, error) { // Step 1: Initialize From dee784333a9b45c60fd97205960ce50b36204b76 Mon Sep 17 00:00:00 2001 From: 4ndrelim Date: Mon, 29 Dec 2025 03:21:30 +0800 Subject: [PATCH 2/7] feat: implement security context filter checks and injection for specialised tools --- .../toolkit/tools/xtramcp/schema_filter.go | 65 +++++++++++++ .../services/toolkit/tools/xtramcp/tool.go | 97 ++++++++++++++----- .../message-entry-container/tools/tools.tsx | 2 +- 3 files changed, 141 insertions(+), 23 deletions(-) create mode 100644 internal/services/toolkit/tools/xtramcp/schema_filter.go diff --git a/internal/services/toolkit/tools/xtramcp/schema_filter.go b/internal/services/toolkit/tools/xtramcp/schema_filter.go new file mode 100644 index 0000000..08ecd9b --- /dev/null +++ b/internal/services/toolkit/tools/xtramcp/schema_filter.go @@ -0,0 +1,65 @@ +package xtramcp + +import "encoding/json" + +// parameters that should be injected server-side +var securityParameters = []string{"user_id", "project_id"} + +// removes security parameters from schema shown to LLM so LLM does not need to generate / fill +func filterSecurityParameters(schema map[string]interface{}) map[string]interface{} { + filtered := deepCopySchema(schema) + + // Remove from properties + if properties, ok := filtered["properties"].(map[string]interface{}); ok { + for _, param := range securityParameters { + delete(properties, param) + } + } + + // Remove from required array + if required, ok := filtered["required"].([]interface{}); ok { + filtered["required"] = filterRequiredArray(required, securityParameters) + } + + return filtered +} + +// creates a deep copy of the schema using JSON marshal/unmarshal +func deepCopySchema(schema map[string]interface{}) map[string]interface{} { + // Use JSON marshal/unmarshal for deep copy + jsonBytes, err := json.Marshal(schema) + if err != nil { + // If marshaling fails, return original schema + return schema + } + + var copy map[string]interface{} + err = json.Unmarshal(jsonBytes, ©) + if err != nil { + // If unmarshaling fails, return original schema + return schema + } + + return copy +} + +// removes security parameters from the required array +func filterRequiredArray(required []interface{}, toRemove []string) []interface{} { + filtered := []interface{}{} + removeMap := make(map[string]bool) + + for _, r := range toRemove { + removeMap[r] = true + } + + // filter our security params + for _, item := range required { + if str, ok := item.(string); ok { + if !removeMap[str] { + filtered = append(filtered, item) + } + } + } + + return filtered +} diff --git a/internal/services/toolkit/tools/xtramcp/tool.go b/internal/services/toolkit/tools/xtramcp/tool.go index f9a4e47..2b04c1e 100644 --- a/internal/services/toolkit/tools/xtramcp/tool.go +++ b/internal/services/toolkit/tools/xtramcp/tool.go @@ -9,12 +9,14 @@ import ( "net/http" "paperdebugger/internal/libs/db" "paperdebugger/internal/services" + "paperdebugger/internal/services/toolkit" toolCallRecordDB "paperdebugger/internal/services/toolkit/db" "time" "github.com/openai/openai-go/v2" "github.com/openai/openai-go/v2/packages/param" "github.com/openai/openai-go/v2/responses" + "go.mongodb.org/mongo-driver/v2/mongo" ) // ToolSchema represents the schema from your backend @@ -41,39 +43,47 @@ type MCPParams struct { // DynamicTool represents a generic tool that can handle any schema type DynamicTool struct { - Name string - Description responses.ToolUnionParam - toolCallRecordDB *toolCallRecordDB.ToolCallRecordDB - projectService *services.ProjectService - coolDownTime time.Duration - baseURL string - client *http.Client - schema map[string]interface{} - sessionID string // Reuse the session ID from initialization + Name string + Description responses.ToolUnionParam + toolCallRecordDB *toolCallRecordDB.ToolCallRecordDB + projectService *services.ProjectService + coolDownTime time.Duration + baseURL string + client *http.Client + schema map[string]interface{} + sessionID string // Reuse the session ID from initialization + requiresInjection bool // Indicates if this tool needs user/project injection } // NewDynamicTool creates a new dynamic tool from a schema -func NewDynamicTool(db *db.DB, projectService *services.ProjectService, toolSchema ToolSchema, baseURL string, sessionID string) *DynamicTool { - // Create tool description with the schema +func NewDynamicTool(db *db.DB, projectService *services.ProjectService, toolSchema ToolSchema, baseURL string, sessionID string, requiresInjection bool) *DynamicTool { + // filter schema if injection is required (hide security context like user_id/project_id from LLM) + schemaForLLM := toolSchema.InputSchema + if requiresInjection { + schemaForLLM = filterSecurityParameters(toolSchema.InputSchema) + } + description := responses.ToolUnionParam{ OfFunction: &responses.FunctionToolParam{ Name: toolSchema.Name, Description: param.NewOpt(toolSchema.Description), - Parameters: openai.FunctionParameters(toolSchema.InputSchema), + Parameters: openai.FunctionParameters(schemaForLLM), // Use filtered schema }, } toolCallRecordDB := toolCallRecordDB.NewToolCallRecordDB(db) + //TODO: consider letting llm client know of output schema too return &DynamicTool{ - Name: toolSchema.Name, - Description: description, - toolCallRecordDB: toolCallRecordDB, - projectService: projectService, - coolDownTime: 5 * time.Minute, - baseURL: baseURL, - client: &http.Client{}, - schema: toolSchema.InputSchema, - sessionID: sessionID, // Store the session ID for reuse + Name: toolSchema.Name, + Description: description, + toolCallRecordDB: toolCallRecordDB, + projectService: projectService, + coolDownTime: 5 * time.Minute, + baseURL: baseURL, + client: &http.Client{}, + schema: toolSchema.InputSchema, // Store original schema for validation + sessionID: sessionID, // Store the session ID for reuse + requiresInjection: requiresInjection, } } @@ -86,7 +96,14 @@ func (t *DynamicTool) Call(ctx context.Context, toolCallId string, args json.Raw return "", "", err } - // Create function call record + // inject user/project context if required + if t.requiresInjection { + err := t.injectSecurityContext(ctx, argsMap) + if err != nil { + return "", "", fmt.Errorf("security context injection failed: %w", err) + } + } + record, err := t.toolCallRecordDB.Create(ctx, toolCallId, t.Name, argsMap) if err != nil { return "", "", err @@ -111,6 +128,42 @@ func (t *DynamicTool) Call(ctx context.Context, toolCallId string, args json.Raw return respStr, "", nil } +// extracts user/project from context and injects into arguments +func (t *DynamicTool) injectSecurityContext(ctx context.Context, argsMap map[string]interface{}) error { + // 1. Extract from context + actor, projectId, _ := toolkit.GetActorProjectConversationID(ctx) + if actor == nil || projectId == "" { + return fmt.Errorf("authentication required: user context not found") + } + + // 2. Validate user owns the project + _, err := t.projectService.GetProject(ctx, actor.ID, projectId) + if err != nil { + if err == mongo.ErrNoDocuments { + return fmt.Errorf("authorization failed: project not found or access denied") + } + return fmt.Errorf("authorization check failed: %w", err) + } + + // 3. Check if tool schema expects these parameters + properties, ok := t.schema["properties"].(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid tool schema: properties not found") + } + + // 4. Inject user_id if expected by tool + if _, hasUserId := properties["user_id"]; hasUserId { + argsMap["user_id"] = actor.ID.Hex() + } + + // 5. Inject project_id if expected by tool + if _, hasProjectId := properties["project_id"]; hasProjectId { + argsMap["project_id"] = projectId + } + + return nil +} + // executeTool makes the MCP request (generic for any tool) func (t *DynamicTool) executeTool(args map[string]interface{}) (string, error) { diff --git a/webapp/_webapp/src/components/message-entry-container/tools/tools.tsx b/webapp/_webapp/src/components/message-entry-container/tools/tools.tsx index 3f4b4c8..dc43d54 100644 --- a/webapp/_webapp/src/components/message-entry-container/tools/tools.tsx +++ b/webapp/_webapp/src/components/message-entry-container/tools/tools.tsx @@ -25,7 +25,7 @@ const XTRA_MCP_TOOL_NAMES = [ // "deep_research", // REVIEWER TOOLS "review_paper", - // "verify_citations" + "verify_citations", // ENHANCER TOOLS // "enhance_academic_writing", // OPENREVIEW ONLINE TOOLS From 99b1483c2511beac807f621810d9c54b2ea55551 Mon Sep 17 00:00:00 2001 From: 4ndrelim Date: Mon, 29 Dec 2025 03:31:09 +0800 Subject: [PATCH 3/7] nit --- internal/services/toolkit/client/utils.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/services/toolkit/client/utils.go b/internal/services/toolkit/client/utils.go index 9df994f..c372876 100644 --- a/internal/services/toolkit/client/utils.go +++ b/internal/services/toolkit/client/utils.go @@ -111,17 +111,17 @@ func initializeToolkit( // initialize MCP session first and log session ID sessionID, err := xtraMCPLoader.InitializeMCP() if err != nil { - logger.Errorf("[AI Client] Failed to initialize XtraMCP session: %v", err) + logger.Errorf("[XtraMCP Client] Failed to initialize XtraMCP session: %v", err) // TODO: Fallback to static tools or exit? } else { - logger.Info("[AI Client] XtraMCP session initialized", "sessionID", sessionID) + logger.Info("[XtraMCP Client] XtraMCP session initialized", "sessionID", sessionID) // dynamically load all tools from XtraMCP backend err = xtraMCPLoader.LoadToolsFromBackend(toolRegistry) if err != nil { - logger.Errorf("[AI Client] Failed to load XtraMCP tools: %v", err) + logger.Errorf("[XtraMCP Client] Failed to load XtraMCP tools: %v", err) } else { - logger.Info("[AI Client] Successfully loaded XtraMCP tools") + logger.Info("[XtraMCP Client] Successfully loaded XtraMCP tools") } } From 9dc904800b2ffd5cf7fd079b8b382c171a67652e Mon Sep 17 00:00:00 2001 From: 4ndrelim Date: Sat, 3 Jan 2026 03:51:19 +0800 Subject: [PATCH 4/7] feat: add context injection for xtramcp tools --- .../toolkit/tools/xtramcp/loader_v2.go | 32 +++++- .../services/toolkit/tools/xtramcp/tool_v2.go | 97 ++++++++++++++----- 2 files changed, 105 insertions(+), 24 deletions(-) diff --git a/internal/services/toolkit/tools/xtramcp/loader_v2.go b/internal/services/toolkit/tools/xtramcp/loader_v2.go index b7662c4..7362a1f 100644 --- a/internal/services/toolkit/tools/xtramcp/loader_v2.go +++ b/internal/services/toolkit/tools/xtramcp/loader_v2.go @@ -53,17 +53,45 @@ func (loader *XtraMCPLoaderV2) LoadToolsFromBackend(toolRegistry *registry.ToolR // Register each tool dynamically, passing the session ID for _, toolSchema := range toolSchemas { - dynamicTool := NewDynamicToolV2(loader.db, loader.projectService, toolSchema, loader.baseURL, loader.sessionID) + // some tools require secrutiy context injection e.g. user_id to authenticate + requiresInjection := loader.requiresSecurityInjection(toolSchema) + + dynamicTool := NewDynamicToolV2( + loader.db, + loader.projectService, + toolSchema, + loader.baseURL, + loader.sessionID, + requiresInjection, + ) // Register the tool with the registry toolRegistry.Register(toolSchema.Name, dynamicTool.Description, dynamicTool.Call) - fmt.Printf("Registered dynamic tool: %s\n", toolSchema.Name) + if requiresInjection { + fmt.Printf("Registered dynamic tool with security injection: %s\n", toolSchema.Name) + } else { + fmt.Printf("Registered dynamic tool: %s\n", toolSchema.Name) + } } return nil } +// checks if a tool schema contains parameters that should be inejected instead of LLM-generated +func (loader *XtraMCPLoaderV2) requiresSecurityInjection(schema ToolSchemaV2) bool { + properties, ok := schema.InputSchema["properties"].(map[string]interface{}) + if !ok { + return false + } + + // injected parameters + _, hasUserId := properties["user_id"] + _, hasProjectId := properties["project_id"] + + return hasUserId || hasProjectId +} + // InitializeMCP performs the full MCP initialization handshake, stores session ID, and returns it func (loader *XtraMCPLoaderV2) InitializeMCP() (string, error) { // Step 1: Initialize diff --git a/internal/services/toolkit/tools/xtramcp/tool_v2.go b/internal/services/toolkit/tools/xtramcp/tool_v2.go index a63a5a3..69a9019 100644 --- a/internal/services/toolkit/tools/xtramcp/tool_v2.go +++ b/internal/services/toolkit/tools/xtramcp/tool_v2.go @@ -9,11 +9,13 @@ import ( "net/http" "paperdebugger/internal/libs/db" "paperdebugger/internal/services" + "paperdebugger/internal/services/toolkit" toolCallRecordDB "paperdebugger/internal/services/toolkit/db" "time" "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/packages/param" + "go.mongodb.org/mongo-driver/v2/mongo" ) // ToolSchema represents the schema from your backend @@ -40,41 +42,49 @@ type MCPParamsV2 struct { // DynamicTool represents a generic tool that can handle any schema type DynamicToolV2 struct { - Name string - Description openai.ChatCompletionToolUnionParam - toolCallRecordDB *toolCallRecordDB.ToolCallRecordDB - projectService *services.ProjectService - coolDownTime time.Duration - baseURL string - client *http.Client - schema map[string]interface{} - sessionID string // Reuse the session ID from initialization + Name string + Description openai.ChatCompletionToolUnionParam + toolCallRecordDB *toolCallRecordDB.ToolCallRecordDB + projectService *services.ProjectService + coolDownTime time.Duration + baseURL string + client *http.Client + schema map[string]interface{} + sessionID string // Reuse the session ID from initialization + requiresInjection bool // Indicates if this tool needs user/project injection } // NewDynamicTool creates a new dynamic tool from a schema -func NewDynamicToolV2(db *db.DB, projectService *services.ProjectService, toolSchema ToolSchemaV2, baseURL string, sessionID string) *DynamicToolV2 { - // Create tool description with the schema +func NewDynamicToolV2(db *db.DB, projectService *services.ProjectService, toolSchema ToolSchemaV2, baseURL string, sessionID string, requiresInjection bool) *DynamicToolV2 { + // filter schema if injection is required (hide security context like user_id/project_id from LLM) + schemaForLLM := toolSchema.InputSchema + if requiresInjection { + schemaForLLM = filterSecurityParameters(toolSchema.InputSchema) + } + description := openai.ChatCompletionToolUnionParam{ OfFunction: &openai.ChatCompletionFunctionToolParam{ Function: openai.FunctionDefinitionParam{ Name: toolSchema.Name, Description: param.NewOpt(toolSchema.Description), - Parameters: openai.FunctionParameters(toolSchema.InputSchema), + Parameters: openai.FunctionParameters(schemaForLLM), // Use filtered schema }, }, } toolCallRecordDB := toolCallRecordDB.NewToolCallRecordDB(db) + //TODO: consider letting llm client know of output schema too return &DynamicToolV2{ - Name: toolSchema.Name, - Description: description, - toolCallRecordDB: toolCallRecordDB, - projectService: projectService, - coolDownTime: 5 * time.Minute, - baseURL: baseURL, - client: &http.Client{}, - schema: toolSchema.InputSchema, - sessionID: sessionID, // Store the session ID for reuse + Name: toolSchema.Name, + Description: description, + toolCallRecordDB: toolCallRecordDB, + projectService: projectService, + coolDownTime: 5 * time.Minute, + baseURL: baseURL, + client: &http.Client{}, + schema: toolSchema.InputSchema, // Store original schema for validation + sessionID: sessionID, // Store the session ID for reuse + requiresInjection: requiresInjection, } } @@ -87,7 +97,14 @@ func (t *DynamicToolV2) Call(ctx context.Context, toolCallId string, args json.R return "", "", err } - // Create function call record + // inject user/project context if required + if t.requiresInjection { + err := t.injectSecurityContext(ctx, argsMap) + if err != nil { + return "", "", fmt.Errorf("security context injection failed: %w", err) + } + } + record, err := t.toolCallRecordDB.Create(ctx, toolCallId, t.Name, argsMap) if err != nil { return "", "", err @@ -112,6 +129,42 @@ func (t *DynamicToolV2) Call(ctx context.Context, toolCallId string, args json.R return respStr, "", nil } +// extracts user/project from context and injects into arguments +func (t *DynamicToolV2) injectSecurityContext(ctx context.Context, argsMap map[string]interface{}) error { + // 1. Extract from context + actor, projectId, _ := toolkit.GetActorProjectConversationID(ctx) + if actor == nil || projectId == "" { + return fmt.Errorf("authentication required: user context not found") + } + + // 2. Validate user owns the project + _, err := t.projectService.GetProject(ctx, actor.ID, projectId) + if err != nil { + if err == mongo.ErrNoDocuments { + return fmt.Errorf("authorization failed: project not found or access denied") + } + return fmt.Errorf("authorization check failed: %w", err) + } + + // 3. Check if tool schema expects these parameters + properties, ok := t.schema["properties"].(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid tool schema: properties not found") + } + + // 4. Inject user_id if expected by tool + if _, hasUserId := properties["user_id"]; hasUserId { + argsMap["user_id"] = actor.ID.Hex() + } + + // 5. Inject project_id if expected by tool + if _, hasProjectId := properties["project_id"]; hasProjectId { + argsMap["project_id"] = projectId + } + + return nil +} + // executeTool makes the MCP request (generic for any tool) func (t *DynamicToolV2) executeTool(args map[string]interface{}) (string, error) { From 0d70e17cefd44ac848b045d874d033cf6b365813 Mon Sep 17 00:00:00 2001 From: 4ndrelim Date: Sat, 3 Jan 2026 03:51:34 +0800 Subject: [PATCH 5/7] support xtramcp v2 integration --- internal/services/toolkit/client/utils_v2.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/internal/services/toolkit/client/utils_v2.go b/internal/services/toolkit/client/utils_v2.go index d4be3cf..2ee837a 100644 --- a/internal/services/toolkit/client/utils_v2.go +++ b/internal/services/toolkit/client/utils_v2.go @@ -144,23 +144,20 @@ func initializeToolkitV2( logger.Info("[AI Client V2] Registered static LaTeX tools", "count", 0) - // Load tools dynamically from backend + // // Load tools dynamically from backend // xtraMCPLoader := xtramcp.NewXtraMCPLoaderV2(db, projectService, cfg.XtraMCPURI) - // initialize MCP session first and log session ID + // // initialize MCP session first and log session ID // sessionID, err := xtraMCPLoader.InitializeMCP() // if err != nil { - // logger.Errorf("[AI Client V2] Failed to initialize XtraMCP session: %v", err) - // // TODO: Fallback to static tools or exit? + // logger.Errorf("[XtraMCP Client] Failed to initialize XtraMCP session: %v", err) // } else { - // logger.Info("[AI Client V2] XtraMCP session initialized", "sessionID", sessionID) + // logger.Info("[XtraMCP Client] XtraMCP session initialized", "sessionID", sessionID) // // dynamically load all tools from XtraMCP backend // err = xtraMCPLoader.LoadToolsFromBackend(toolRegistry) // if err != nil { - // logger.Errorf("[AI Client V2] Failed to load XtraMCP tools: %v", err) - // } else { - // logger.Info("[AI Client V2] Successfully loaded XtraMCP tools") + // logger.Errorf("[XtraMCP Client] Failed to load XtraMCP tools: %v", err) // } // } From 9646d0b51ba09b4d7f722ba3dca13d1f4fddb4e6 Mon Sep 17 00:00:00 2001 From: 4ndrelim Date: Sat, 3 Jan 2026 04:33:31 +0800 Subject: [PATCH 6/7] fix mongodb error and some nits --- internal/services/toolkit/tools/xtramcp/loader_v2.go | 4 ++-- internal/services/toolkit/tools/xtramcp/schema_filter.go | 9 ++++++--- internal/services/toolkit/tools/xtramcp/tool.go | 3 ++- internal/services/toolkit/tools/xtramcp/tool_v2.go | 3 ++- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/internal/services/toolkit/tools/xtramcp/loader_v2.go b/internal/services/toolkit/tools/xtramcp/loader_v2.go index 7362a1f..add5dde 100644 --- a/internal/services/toolkit/tools/xtramcp/loader_v2.go +++ b/internal/services/toolkit/tools/xtramcp/loader_v2.go @@ -53,7 +53,7 @@ func (loader *XtraMCPLoaderV2) LoadToolsFromBackend(toolRegistry *registry.ToolR // Register each tool dynamically, passing the session ID for _, toolSchema := range toolSchemas { - // some tools require secrutiy context injection e.g. user_id to authenticate + // some tools require security context injection e.g. user_id to authenticate requiresInjection := loader.requiresSecurityInjection(toolSchema) dynamicTool := NewDynamicToolV2( @@ -78,7 +78,7 @@ func (loader *XtraMCPLoaderV2) LoadToolsFromBackend(toolRegistry *registry.ToolR return nil } -// checks if a tool schema contains parameters that should be inejected instead of LLM-generated +// checks if a tool schema contains parameters that should be injected instead of LLM-generated func (loader *XtraMCPLoaderV2) requiresSecurityInjection(schema ToolSchemaV2) bool { properties, ok := schema.InputSchema["properties"].(map[string]interface{}) if !ok { diff --git a/internal/services/toolkit/tools/xtramcp/schema_filter.go b/internal/services/toolkit/tools/xtramcp/schema_filter.go index 08ecd9b..34b6280 100644 --- a/internal/services/toolkit/tools/xtramcp/schema_filter.go +++ b/internal/services/toolkit/tools/xtramcp/schema_filter.go @@ -25,18 +25,21 @@ func filterSecurityParameters(schema map[string]interface{}) map[string]interfac } // creates a deep copy of the schema using JSON marshal/unmarshal +// Uses JSON round-trip because map[string]interface{} contains nested structures +// This ensures modifications to the copy don't affect the original schema. func deepCopySchema(schema map[string]interface{}) map[string]interface{} { // Use JSON marshal/unmarshal for deep copy jsonBytes, err := json.Marshal(schema) if err != nil { - // If marshaling fails, return original schema + // Extremely unlikely with valid JSON schemas (MCP schemas are JSON-compatible) + // // If marshaling fails, return original schema return schema } var copy map[string]interface{} err = json.Unmarshal(jsonBytes, ©) if err != nil { - // If unmarshaling fails, return original schema + // Should never happen if marshal succeeded return schema } @@ -52,7 +55,7 @@ func filterRequiredArray(required []interface{}, toRemove []string) []interface{ removeMap[r] = true } - // filter our security params + // filter out security params for _, item := range required { if str, ok := item.(string); ok { if !removeMap[str] { diff --git a/internal/services/toolkit/tools/xtramcp/tool.go b/internal/services/toolkit/tools/xtramcp/tool.go index 2b04c1e..1dc8962 100644 --- a/internal/services/toolkit/tools/xtramcp/tool.go +++ b/internal/services/toolkit/tools/xtramcp/tool.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -139,7 +140,7 @@ func (t *DynamicTool) injectSecurityContext(ctx context.Context, argsMap map[str // 2. Validate user owns the project _, err := t.projectService.GetProject(ctx, actor.ID, projectId) if err != nil { - if err == mongo.ErrNoDocuments { + if errors.Is(err, mongo.ErrNoDocuments) { return fmt.Errorf("authorization failed: project not found or access denied") } return fmt.Errorf("authorization check failed: %w", err) diff --git a/internal/services/toolkit/tools/xtramcp/tool_v2.go b/internal/services/toolkit/tools/xtramcp/tool_v2.go index 69a9019..bd6d604 100644 --- a/internal/services/toolkit/tools/xtramcp/tool_v2.go +++ b/internal/services/toolkit/tools/xtramcp/tool_v2.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -140,7 +141,7 @@ func (t *DynamicToolV2) injectSecurityContext(ctx context.Context, argsMap map[s // 2. Validate user owns the project _, err := t.projectService.GetProject(ctx, actor.ID, projectId) if err != nil { - if err == mongo.ErrNoDocuments { + if errors.Is(err, mongo.ErrNoDocuments) { return fmt.Errorf("authorization failed: project not found or access denied") } return fmt.Errorf("authorization check failed: %w", err) From 418274aec1ca5d78887d52c6e8e182375884b070 Mon Sep 17 00:00:00 2001 From: 4ndrelim Date: Mon, 5 Jan 2026 05:01:24 +0800 Subject: [PATCH 7/7] deprecate xtramcp toolcall for v1 api --- .../services/toolkit/tools/xtramcp/tool.go | 78 +------------------ 1 file changed, 2 insertions(+), 76 deletions(-) diff --git a/internal/services/toolkit/tools/xtramcp/tool.go b/internal/services/toolkit/tools/xtramcp/tool.go index 1dc8962..efcbe58 100644 --- a/internal/services/toolkit/tools/xtramcp/tool.go +++ b/internal/services/toolkit/tools/xtramcp/tool.go @@ -4,20 +4,17 @@ import ( "bytes" "context" "encoding/json" - "errors" "fmt" "io" "net/http" "paperdebugger/internal/libs/db" "paperdebugger/internal/services" - "paperdebugger/internal/services/toolkit" toolCallRecordDB "paperdebugger/internal/services/toolkit/db" "time" "github.com/openai/openai-go/v2" "github.com/openai/openai-go/v2/packages/param" "github.com/openai/openai-go/v2/responses" - "go.mongodb.org/mongo-driver/v2/mongo" ) // ToolSchema represents the schema from your backend @@ -89,80 +86,9 @@ func NewDynamicTool(db *db.DB, projectService *services.ProjectService, toolSche } // Call handles the tool execution (generic for any tool) +// DEPRECATED: v1 API is no longer supported. This method should not be called. func (t *DynamicTool) Call(ctx context.Context, toolCallId string, args json.RawMessage) (string, string, error) { - // Parse arguments as generic map since we don't know the structure - var argsMap map[string]interface{} - err := json.Unmarshal(args, &argsMap) - if err != nil { - return "", "", err - } - - // inject user/project context if required - if t.requiresInjection { - err := t.injectSecurityContext(ctx, argsMap) - if err != nil { - return "", "", fmt.Errorf("security context injection failed: %w", err) - } - } - - record, err := t.toolCallRecordDB.Create(ctx, toolCallId, t.Name, argsMap) - if err != nil { - return "", "", err - } - - // Execute the tool via MCP - respStr, err := t.executeTool(argsMap) - if err != nil { - err = fmt.Errorf("failed to execute tool %s: %v", t.Name, err) - t.toolCallRecordDB.OnError(ctx, record, err) - return "", "", err - } - - rawJson, err := json.Marshal(respStr) - if err != nil { - err = fmt.Errorf("failed to marshal tool result: %v", err) - t.toolCallRecordDB.OnError(ctx, record, err) - return "", "", err - } - t.toolCallRecordDB.OnSuccess(ctx, record, string(rawJson)) - - return respStr, "", nil -} - -// extracts user/project from context and injects into arguments -func (t *DynamicTool) injectSecurityContext(ctx context.Context, argsMap map[string]interface{}) error { - // 1. Extract from context - actor, projectId, _ := toolkit.GetActorProjectConversationID(ctx) - if actor == nil || projectId == "" { - return fmt.Errorf("authentication required: user context not found") - } - - // 2. Validate user owns the project - _, err := t.projectService.GetProject(ctx, actor.ID, projectId) - if err != nil { - if errors.Is(err, mongo.ErrNoDocuments) { - return fmt.Errorf("authorization failed: project not found or access denied") - } - return fmt.Errorf("authorization check failed: %w", err) - } - - // 3. Check if tool schema expects these parameters - properties, ok := t.schema["properties"].(map[string]interface{}) - if !ok { - return fmt.Errorf("invalid tool schema: properties not found") - } - - // 4. Inject user_id if expected by tool - if _, hasUserId := properties["user_id"]; hasUserId { - argsMap["user_id"] = actor.ID.Hex() - } - - // 5. Inject project_id if expected by tool - if _, hasProjectId := properties["project_id"]; hasProjectId { - argsMap["project_id"] = projectId - } - - return nil + return "", "", fmt.Errorf("v1 API is deprecated and no longer supported. Please use v2 API instead") } // executeTool makes the MCP request (generic for any tool)