files
This commit is contained in:
822
mcp/backup/server.go
Normal file
822
mcp/backup/server.go
Normal file
@@ -0,0 +1,822 @@
|
||||
//go:build mcp_http
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.teamworkapps.com/shortcut/cremote/client"
|
||||
)
|
||||
|
||||
// DebugLogger handles debug logging to file
|
||||
type DebugLogger struct {
|
||||
file *os.File
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NewDebugLogger creates a new debug logger
|
||||
func NewDebugLogger() (*DebugLogger, error) {
|
||||
logDir := "/tmp/cremote-mcp-logs"
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create log directory: %w", err)
|
||||
}
|
||||
|
||||
logFile := filepath.Join(logDir, fmt.Sprintf("mcp-http-%d.log", time.Now().Unix()))
|
||||
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open log file: %w", err)
|
||||
}
|
||||
|
||||
logger := log.New(file, "", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
|
||||
logger.Printf("=== MCP HTTP Server Debug Log Started ===")
|
||||
|
||||
return &DebugLogger{
|
||||
file: file,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Log writes a debug message
|
||||
func (d *DebugLogger) Log(format string, args ...interface{}) {
|
||||
if d != nil && d.logger != nil {
|
||||
d.logger.Printf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogJSON logs a JSON object with a label
|
||||
func (d *DebugLogger) LogJSON(label string, obj interface{}) {
|
||||
if d != nil && d.logger != nil {
|
||||
jsonBytes, err := json.MarshalIndent(obj, "", " ")
|
||||
if err != nil {
|
||||
d.logger.Printf("%s: JSON marshal error: %v", label, err)
|
||||
} else {
|
||||
d.logger.Printf("%s:\n%s", label, string(jsonBytes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the debug logger
|
||||
func (d *DebugLogger) Close() {
|
||||
if d != nil && d.file != nil {
|
||||
d.logger.Printf("=== MCP HTTP Server Debug Log Ended ===")
|
||||
d.file.Close()
|
||||
}
|
||||
}
|
||||
|
||||
var debugLogger *DebugLogger
|
||||
|
||||
// MCPServer wraps the cremote client with MCP protocol
|
||||
type MCPServer struct {
|
||||
client *client.Client
|
||||
currentTab string
|
||||
tabHistory []string
|
||||
iframeMode bool
|
||||
lastError string
|
||||
screenshots []string
|
||||
}
|
||||
|
||||
// MCPRequest represents an incoming MCP request
|
||||
type MCPRequest struct {
|
||||
Method string `json:"method"`
|
||||
Params map[string]interface{} `json:"params"`
|
||||
ID interface{} `json:"id"`
|
||||
}
|
||||
|
||||
// MCPResponse represents an MCP response
|
||||
type MCPResponse struct {
|
||||
Result interface{} `json:"result,omitempty"`
|
||||
Error *MCPError `json:"error,omitempty"`
|
||||
ID interface{} `json:"id"`
|
||||
}
|
||||
|
||||
// MCPError represents an MCP error
|
||||
type MCPError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ToolResult represents the result of a tool execution
|
||||
type ToolResult struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Screenshot string `json:"screenshot,omitempty"`
|
||||
CurrentTab string `json:"current_tab,omitempty"`
|
||||
TabHistory []string `json:"tab_history,omitempty"`
|
||||
IframeMode bool `json:"iframe_mode"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// NewMCPServer creates a new MCP server instance
|
||||
func NewMCPServer(host string, port int) *MCPServer {
|
||||
return &MCPServer{
|
||||
client: client.NewClient(host, port),
|
||||
tabHistory: make([]string, 0),
|
||||
screenshots: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// HandleRequest processes an MCP request
|
||||
func (s *MCPServer) HandleRequest(req MCPRequest) MCPResponse {
|
||||
debugLogger.Log("HandleRequest called with method: %s, ID: %v", req.Method, req.ID)
|
||||
debugLogger.LogJSON("Incoming Request", req)
|
||||
|
||||
var resp MCPResponse
|
||||
|
||||
switch req.Method {
|
||||
case "initialize":
|
||||
debugLogger.Log("Handling initialize request")
|
||||
resp = s.handleInitialize(req)
|
||||
case "tools/list":
|
||||
debugLogger.Log("Handling tools/list request")
|
||||
resp = s.handleToolsList(req)
|
||||
case "tools/call":
|
||||
debugLogger.Log("Handling tools/call request")
|
||||
resp = s.handleToolCall(req)
|
||||
default:
|
||||
debugLogger.Log("Unknown method: %s", req.Method)
|
||||
resp = MCPResponse{
|
||||
Error: &MCPError{Code: -32601, Message: "Method not found"},
|
||||
ID: req.ID,
|
||||
}
|
||||
}
|
||||
|
||||
debugLogger.LogJSON("Response", resp)
|
||||
debugLogger.Log("HandleRequest completed for method: %s", req.Method)
|
||||
return resp
|
||||
}
|
||||
|
||||
// handleInitialize handles the MCP initialize request
|
||||
func (s *MCPServer) handleInitialize(req MCPRequest) MCPResponse {
|
||||
return MCPResponse{
|
||||
Result: map[string]interface{}{
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": map[string]interface{}{
|
||||
"tools": map[string]interface{}{
|
||||
"listChanged": true,
|
||||
},
|
||||
},
|
||||
"serverInfo": map[string]interface{}{
|
||||
"name": "cremote-mcp",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
},
|
||||
ID: req.ID,
|
||||
}
|
||||
}
|
||||
|
||||
// handleToolsList returns the list of available tools
|
||||
func (s *MCPServer) handleToolsList(req MCPRequest) MCPResponse {
|
||||
tools := []map[string]interface{}{
|
||||
{
|
||||
"name": "web_navigate",
|
||||
"description": "Navigate to a URL and optionally take a screenshot",
|
||||
"inputSchema": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"url": map[string]interface{}{"type": "string", "description": "URL to navigate to"},
|
||||
"tab": map[string]interface{}{"type": "string", "description": "Tab ID (optional, uses current tab)"},
|
||||
"screenshot": map[string]interface{}{"type": "boolean", "description": "Take screenshot after navigation"},
|
||||
"timeout": map[string]interface{}{"type": "integer", "description": "Timeout in seconds", "default": 5},
|
||||
},
|
||||
"required": []string{"url"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "web_interact",
|
||||
"description": "Interact with web elements (click, fill, submit)",
|
||||
"inputSchema": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"action": map[string]interface{}{"type": "string", "enum": []string{"click", "fill", "submit", "upload"}},
|
||||
"selector": map[string]interface{}{"type": "string", "description": "CSS selector for the element"},
|
||||
"value": map[string]interface{}{"type": "string", "description": "Value to fill (for fill/upload actions)"},
|
||||
"tab": map[string]interface{}{"type": "string", "description": "Tab ID (optional)"},
|
||||
"timeout": map[string]interface{}{"type": "integer", "description": "Timeout in seconds", "default": 5},
|
||||
},
|
||||
"required": []string{"action", "selector"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "web_extract",
|
||||
"description": "Extract data from the page (source, element HTML, or execute JavaScript)",
|
||||
"inputSchema": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"type": map[string]interface{}{"type": "string", "enum": []string{"source", "element", "javascript"}},
|
||||
"selector": map[string]interface{}{"type": "string", "description": "CSS selector (for element type)"},
|
||||
"code": map[string]interface{}{"type": "string", "description": "JavaScript code (for javascript type)"},
|
||||
"tab": map[string]interface{}{"type": "string", "description": "Tab ID (optional)"},
|
||||
"timeout": map[string]interface{}{"type": "integer", "description": "Timeout in seconds", "default": 5},
|
||||
},
|
||||
"required": []string{"type"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "web_screenshot",
|
||||
"description": "Take a screenshot of the current page",
|
||||
"inputSchema": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"output": map[string]interface{}{"type": "string", "description": "Output file path"},
|
||||
"full_page": map[string]interface{}{"type": "boolean", "description": "Capture full page", "default": false},
|
||||
"tab": map[string]interface{}{"type": "string", "description": "Tab ID (optional)"},
|
||||
"timeout": map[string]interface{}{"type": "integer", "description": "Timeout in seconds", "default": 5},
|
||||
},
|
||||
"required": []string{"output"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "web_manage_tabs",
|
||||
"description": "Manage browser tabs (open, close, list, switch)",
|
||||
"inputSchema": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"action": map[string]interface{}{"type": "string", "enum": []string{"open", "close", "list", "switch"}},
|
||||
"tab": map[string]interface{}{"type": "string", "description": "Tab ID (for close/switch actions)"},
|
||||
"timeout": map[string]interface{}{"type": "integer", "description": "Timeout in seconds", "default": 5},
|
||||
},
|
||||
"required": []string{"action"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "web_iframe",
|
||||
"description": "Switch iframe context for subsequent operations",
|
||||
"inputSchema": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"action": map[string]interface{}{"type": "string", "enum": []string{"enter", "exit"}},
|
||||
"selector": map[string]interface{}{"type": "string", "description": "Iframe CSS selector (for enter action)"},
|
||||
"tab": map[string]interface{}{"type": "string", "description": "Tab ID (optional)"},
|
||||
},
|
||||
"required": []string{"action"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return MCPResponse{
|
||||
Result: map[string]interface{}{"tools": tools},
|
||||
ID: req.ID,
|
||||
}
|
||||
}
|
||||
|
||||
// handleToolCall executes a tool call
|
||||
func (s *MCPServer) handleToolCall(req MCPRequest) MCPResponse {
|
||||
params, ok := req.Params["arguments"].(map[string]interface{})
|
||||
if !ok {
|
||||
return MCPResponse{
|
||||
Error: &MCPError{Code: -32602, Message: "Invalid parameters"},
|
||||
ID: req.ID,
|
||||
}
|
||||
}
|
||||
|
||||
toolName, ok := req.Params["name"].(string)
|
||||
if !ok {
|
||||
return MCPResponse{
|
||||
Error: &MCPError{Code: -32602, Message: "Tool name required"},
|
||||
ID: req.ID,
|
||||
}
|
||||
}
|
||||
|
||||
var result ToolResult
|
||||
var err error
|
||||
|
||||
switch toolName {
|
||||
case "web_navigate":
|
||||
result, err = s.handleNavigate(params)
|
||||
case "web_interact":
|
||||
result, err = s.handleInteract(params)
|
||||
case "web_extract":
|
||||
result, err = s.handleExtract(params)
|
||||
case "web_screenshot":
|
||||
result, err = s.handleScreenshot(params)
|
||||
case "web_manage_tabs":
|
||||
result, err = s.handleManageTabs(params)
|
||||
case "web_iframe":
|
||||
result, err = s.handleIframe(params)
|
||||
default:
|
||||
return MCPResponse{
|
||||
Error: &MCPError{Code: -32601, Message: "Unknown tool: " + toolName},
|
||||
ID: req.ID,
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
result.Success = false
|
||||
result.Error = err.Error()
|
||||
s.lastError = err.Error()
|
||||
}
|
||||
|
||||
// Always include current state in response
|
||||
result.CurrentTab = s.currentTab
|
||||
result.TabHistory = s.tabHistory
|
||||
result.IframeMode = s.iframeMode
|
||||
|
||||
return MCPResponse{
|
||||
Result: result,
|
||||
ID: req.ID,
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get string parameter with default
|
||||
func getStringParam(params map[string]interface{}, key, defaultValue string) string {
|
||||
if val, ok := params[key].(string); ok {
|
||||
return val
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Helper function to get int parameter with default
|
||||
func getIntParam(params map[string]interface{}, key string, defaultValue int) int {
|
||||
if val, ok := params[key].(float64); ok {
|
||||
return int(val)
|
||||
}
|
||||
if val, ok := params[key].(int); ok {
|
||||
return val
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Helper function to get bool parameter with default
|
||||
func getBoolParam(params map[string]interface{}, key string, defaultValue bool) bool {
|
||||
if val, ok := params[key].(bool); ok {
|
||||
return val
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Helper function to resolve tab ID
|
||||
func (s *MCPServer) resolveTabID(tabParam string) string {
|
||||
if tabParam != "" {
|
||||
return tabParam
|
||||
}
|
||||
return s.currentTab
|
||||
}
|
||||
|
||||
// handleNavigate handles web navigation
|
||||
func (s *MCPServer) handleNavigate(params map[string]interface{}) (ToolResult, error) {
|
||||
url := getStringParam(params, "url", "")
|
||||
if url == "" {
|
||||
return ToolResult{}, fmt.Errorf("url parameter is required")
|
||||
}
|
||||
|
||||
tab := getStringParam(params, "tab", "")
|
||||
timeout := getIntParam(params, "timeout", 5)
|
||||
takeScreenshot := getBoolParam(params, "screenshot", false)
|
||||
|
||||
// If no tab specified and we don't have a current tab, open one
|
||||
if tab == "" && s.currentTab == "" {
|
||||
newTab, err := s.client.OpenTab(timeout)
|
||||
if err != nil {
|
||||
return ToolResult{}, fmt.Errorf("failed to open new tab: %w", err)
|
||||
}
|
||||
s.currentTab = newTab
|
||||
s.tabHistory = append(s.tabHistory, newTab)
|
||||
tab = newTab
|
||||
} else if tab == "" {
|
||||
tab = s.currentTab
|
||||
}
|
||||
|
||||
// Load the URL
|
||||
err := s.client.LoadURL(tab, url, timeout)
|
||||
if err != nil {
|
||||
return ToolResult{}, fmt.Errorf("failed to load URL: %w", err)
|
||||
}
|
||||
|
||||
result := ToolResult{
|
||||
Success: true,
|
||||
Data: map[string]string{"url": url, "tab": tab},
|
||||
}
|
||||
|
||||
// Take screenshot if requested
|
||||
if takeScreenshot {
|
||||
screenshotPath := fmt.Sprintf("/tmp/navigate-%d.png", time.Now().Unix())
|
||||
err = s.client.TakeScreenshot(tab, screenshotPath, false, timeout)
|
||||
if err == nil {
|
||||
result.Screenshot = screenshotPath
|
||||
s.screenshots = append(s.screenshots, screenshotPath)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// handleInteract handles web element interactions
|
||||
func (s *MCPServer) handleInteract(params map[string]interface{}) (ToolResult, error) {
|
||||
action := getStringParam(params, "action", "")
|
||||
selector := getStringParam(params, "selector", "")
|
||||
value := getStringParam(params, "value", "")
|
||||
tab := s.resolveTabID(getStringParam(params, "tab", ""))
|
||||
timeout := getIntParam(params, "timeout", 5)
|
||||
|
||||
if action == "" || selector == "" {
|
||||
return ToolResult{}, fmt.Errorf("action and selector parameters are required")
|
||||
}
|
||||
|
||||
if tab == "" {
|
||||
return ToolResult{}, fmt.Errorf("no active tab available")
|
||||
}
|
||||
|
||||
var err error
|
||||
result := ToolResult{Success: true}
|
||||
|
||||
switch action {
|
||||
case "click":
|
||||
err = s.client.ClickElement(tab, selector, timeout, timeout)
|
||||
result.Data = map[string]string{"action": "clicked", "selector": selector}
|
||||
|
||||
case "fill":
|
||||
if value == "" {
|
||||
return ToolResult{}, fmt.Errorf("value parameter is required for fill action")
|
||||
}
|
||||
err = s.client.FillFormField(tab, selector, value, timeout, timeout)
|
||||
result.Data = map[string]string{"action": "filled", "selector": selector, "value": value}
|
||||
|
||||
case "submit":
|
||||
err = s.client.SubmitForm(tab, selector, timeout, timeout)
|
||||
result.Data = map[string]string{"action": "submitted", "selector": selector}
|
||||
|
||||
case "upload":
|
||||
if value == "" {
|
||||
return ToolResult{}, fmt.Errorf("value parameter (file path) is required for upload action")
|
||||
}
|
||||
err = s.client.UploadFile(tab, selector, value, timeout, timeout)
|
||||
result.Data = map[string]string{"action": "uploaded", "selector": selector, "file": value}
|
||||
|
||||
default:
|
||||
return ToolResult{}, fmt.Errorf("unknown action: %s", action)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return ToolResult{}, fmt.Errorf("failed to %s element: %w", action, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// handleExtract handles data extraction from pages
|
||||
func (s *MCPServer) handleExtract(params map[string]interface{}) (ToolResult, error) {
|
||||
extractType := getStringParam(params, "type", "")
|
||||
selector := getStringParam(params, "selector", "")
|
||||
code := getStringParam(params, "code", "")
|
||||
tab := s.resolveTabID(getStringParam(params, "tab", ""))
|
||||
timeout := getIntParam(params, "timeout", 5)
|
||||
|
||||
if extractType == "" {
|
||||
return ToolResult{}, fmt.Errorf("type parameter is required")
|
||||
}
|
||||
|
||||
if tab == "" {
|
||||
return ToolResult{}, fmt.Errorf("no active tab available")
|
||||
}
|
||||
|
||||
var data interface{}
|
||||
var err error
|
||||
|
||||
switch extractType {
|
||||
case "source":
|
||||
data, err = s.client.GetPageSource(tab, timeout)
|
||||
|
||||
case "element":
|
||||
if selector == "" {
|
||||
return ToolResult{}, fmt.Errorf("selector parameter is required for element extraction")
|
||||
}
|
||||
data, err = s.client.GetElementHTML(tab, selector, timeout)
|
||||
|
||||
case "javascript":
|
||||
if code == "" {
|
||||
return ToolResult{}, fmt.Errorf("code parameter is required for javascript extraction")
|
||||
}
|
||||
data, err = s.client.EvalJS(tab, code, timeout)
|
||||
|
||||
default:
|
||||
return ToolResult{}, fmt.Errorf("unknown extraction type: %s", extractType)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return ToolResult{}, fmt.Errorf("failed to extract %s: %w", extractType, err)
|
||||
}
|
||||
|
||||
return ToolResult{
|
||||
Success: true,
|
||||
Data: data,
|
||||
Metadata: map[string]string{
|
||||
"type": extractType,
|
||||
"selector": selector,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// handleScreenshot handles screenshot capture
|
||||
func (s *MCPServer) handleScreenshot(params map[string]interface{}) (ToolResult, error) {
|
||||
output := getStringParam(params, "output", "")
|
||||
if output == "" {
|
||||
output = fmt.Sprintf("/tmp/screenshot-%d.png", time.Now().Unix())
|
||||
}
|
||||
|
||||
tab := s.resolveTabID(getStringParam(params, "tab", ""))
|
||||
fullPage := getBoolParam(params, "full_page", false)
|
||||
timeout := getIntParam(params, "timeout", 5)
|
||||
|
||||
if tab == "" {
|
||||
return ToolResult{}, fmt.Errorf("no active tab available")
|
||||
}
|
||||
|
||||
err := s.client.TakeScreenshot(tab, output, fullPage, timeout)
|
||||
if err != nil {
|
||||
return ToolResult{}, fmt.Errorf("failed to take screenshot: %w", err)
|
||||
}
|
||||
|
||||
s.screenshots = append(s.screenshots, output)
|
||||
|
||||
return ToolResult{
|
||||
Success: true,
|
||||
Screenshot: output,
|
||||
Data: map[string]interface{}{
|
||||
"output": output,
|
||||
"full_page": fullPage,
|
||||
"tab": tab,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// handleManageTabs handles tab management operations
|
||||
func (s *MCPServer) handleManageTabs(params map[string]interface{}) (ToolResult, error) {
|
||||
action := getStringParam(params, "action", "")
|
||||
tab := getStringParam(params, "tab", "")
|
||||
timeout := getIntParam(params, "timeout", 5)
|
||||
|
||||
if action == "" {
|
||||
return ToolResult{}, fmt.Errorf("action parameter is required")
|
||||
}
|
||||
|
||||
var data interface{}
|
||||
var err error
|
||||
|
||||
switch action {
|
||||
case "open":
|
||||
newTab, err := s.client.OpenTab(timeout)
|
||||
if err != nil {
|
||||
return ToolResult{}, fmt.Errorf("failed to open tab: %w", err)
|
||||
}
|
||||
s.currentTab = newTab
|
||||
s.tabHistory = append(s.tabHistory, newTab)
|
||||
data = map[string]string{"tab": newTab, "action": "opened"}
|
||||
|
||||
case "close":
|
||||
targetTab := s.resolveTabID(tab)
|
||||
if targetTab == "" {
|
||||
return ToolResult{}, fmt.Errorf("no tab to close")
|
||||
}
|
||||
err = s.client.CloseTab(targetTab, timeout)
|
||||
if err != nil {
|
||||
return ToolResult{}, fmt.Errorf("failed to close tab: %w", err)
|
||||
}
|
||||
// Remove from history and update current tab
|
||||
s.removeTabFromHistory(targetTab)
|
||||
data = map[string]string{"tab": targetTab, "action": "closed"}
|
||||
|
||||
case "list":
|
||||
tabs, err := s.client.ListTabs()
|
||||
if err != nil {
|
||||
return ToolResult{}, fmt.Errorf("failed to list tabs: %w", err)
|
||||
}
|
||||
data = tabs
|
||||
|
||||
case "switch":
|
||||
if tab == "" {
|
||||
return ToolResult{}, fmt.Errorf("tab parameter is required for switch action")
|
||||
}
|
||||
s.currentTab = tab
|
||||
// Move to end of history if it exists, otherwise add it
|
||||
s.removeTabFromHistory(tab)
|
||||
s.tabHistory = append(s.tabHistory, tab)
|
||||
data = map[string]string{"tab": tab, "action": "switched"}
|
||||
|
||||
default:
|
||||
return ToolResult{}, fmt.Errorf("unknown tab action: %s", action)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return ToolResult{}, err
|
||||
}
|
||||
|
||||
return ToolResult{
|
||||
Success: true,
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// handleIframe handles iframe context switching
|
||||
func (s *MCPServer) handleIframe(params map[string]interface{}) (ToolResult, error) {
|
||||
action := getStringParam(params, "action", "")
|
||||
selector := getStringParam(params, "selector", "")
|
||||
tab := s.resolveTabID(getStringParam(params, "tab", ""))
|
||||
|
||||
if action == "" {
|
||||
return ToolResult{}, fmt.Errorf("action parameter is required")
|
||||
}
|
||||
|
||||
if tab == "" {
|
||||
return ToolResult{}, fmt.Errorf("no active tab available")
|
||||
}
|
||||
|
||||
var err error
|
||||
var data map[string]string
|
||||
|
||||
switch action {
|
||||
case "enter":
|
||||
if selector == "" {
|
||||
return ToolResult{}, fmt.Errorf("selector parameter is required for enter action")
|
||||
}
|
||||
err = s.client.SwitchToIframe(tab, selector)
|
||||
s.iframeMode = true
|
||||
data = map[string]string{"action": "entered", "selector": selector}
|
||||
|
||||
case "exit":
|
||||
err = s.client.SwitchToMain(tab)
|
||||
s.iframeMode = false
|
||||
data = map[string]string{"action": "exited"}
|
||||
|
||||
default:
|
||||
return ToolResult{}, fmt.Errorf("unknown iframe action: %s", action)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return ToolResult{}, fmt.Errorf("failed to %s iframe: %w", action, err)
|
||||
}
|
||||
|
||||
return ToolResult{
|
||||
Success: true,
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Helper function to remove tab from history
|
||||
func (s *MCPServer) removeTabFromHistory(tabID string) {
|
||||
for i, id := range s.tabHistory {
|
||||
if id == tabID {
|
||||
s.tabHistory = append(s.tabHistory[:i], s.tabHistory[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
// If we removed the current tab, set current to the last in history
|
||||
if s.currentTab == tabID {
|
||||
if len(s.tabHistory) > 0 {
|
||||
s.currentTab = s.tabHistory[len(s.tabHistory)-1]
|
||||
} else {
|
||||
s.currentTab = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP handler for MCP requests
|
||||
func (s *MCPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
debugLogger.Log("ServeHTTP: Received %s request from %s", r.Method, r.RemoteAddr)
|
||||
debugLogger.Log("ServeHTTP: Request URL: %s", r.URL.String())
|
||||
debugLogger.Log("ServeHTTP: Request headers: %v", r.Header)
|
||||
|
||||
// Set CORS headers for browser compatibility
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// Handle preflight requests
|
||||
if r.Method == "OPTIONS" {
|
||||
debugLogger.Log("ServeHTTP: Handling OPTIONS preflight request")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Only accept POST requests
|
||||
if r.Method != "POST" {
|
||||
debugLogger.Log("ServeHTTP: Method not allowed: %s", r.Method)
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Read and decode the request
|
||||
var req MCPRequest
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
if err := decoder.Decode(&req); err != nil {
|
||||
debugLogger.Log("ServeHTTP: Error decoding request: %v", err)
|
||||
log.Printf("Error decoding request: %v", err)
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
debugLogger.LogJSON("HTTP Request", req)
|
||||
log.Printf("Processing HTTP request: %s (ID: %v)", req.Method, req.ID)
|
||||
|
||||
// Handle the request
|
||||
resp := s.HandleRequest(req)
|
||||
|
||||
debugLogger.LogJSON("HTTP Response", resp)
|
||||
|
||||
// Encode and send the response
|
||||
encoder := json.NewEncoder(w)
|
||||
if err := encoder.Encode(resp); err != nil {
|
||||
debugLogger.Log("ServeHTTP: Error encoding response: %v", err)
|
||||
log.Printf("Error encoding response: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
debugLogger.Log("ServeHTTP: Response sent successfully for request: %s", req.Method)
|
||||
log.Printf("Completed HTTP request: %s", req.Method)
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Initialize debug logger
|
||||
var err error
|
||||
debugLogger, err = NewDebugLogger()
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to initialize debug logger: %v", err)
|
||||
// Continue without debug logging
|
||||
} else {
|
||||
defer debugLogger.Close()
|
||||
debugLogger.Log("MCP HTTP Server starting up")
|
||||
}
|
||||
|
||||
// Get cremote daemon connection settings
|
||||
cremoteHost := os.Getenv("CREMOTE_HOST")
|
||||
if cremoteHost == "" {
|
||||
cremoteHost = "localhost"
|
||||
}
|
||||
|
||||
cremotePortStr := os.Getenv("CREMOTE_PORT")
|
||||
cremotePort := 8989
|
||||
if cremotePortStr != "" {
|
||||
if p, err := strconv.Atoi(cremotePortStr); err == nil {
|
||||
cremotePort = p
|
||||
}
|
||||
}
|
||||
|
||||
// Get HTTP server settings
|
||||
httpHost := os.Getenv("MCP_HOST")
|
||||
if httpHost == "" {
|
||||
httpHost = "localhost"
|
||||
}
|
||||
|
||||
httpPortStr := os.Getenv("MCP_PORT")
|
||||
httpPort := 8990
|
||||
if httpPortStr != "" {
|
||||
if p, err := strconv.Atoi(httpPortStr); err == nil {
|
||||
httpPort = p
|
||||
}
|
||||
}
|
||||
|
||||
debugLogger.Log("HTTP server will listen on %s:%d", httpHost, httpPort)
|
||||
debugLogger.Log("Connecting to cremote daemon at %s:%d", cremoteHost, cremotePort)
|
||||
|
||||
log.Printf("Starting MCP HTTP server on %s:%d", httpHost, httpPort)
|
||||
log.Printf("Connecting to cremote daemon at %s:%d", cremoteHost, cremotePort)
|
||||
|
||||
// Create the MCP server
|
||||
mcpServer := NewMCPServer(cremoteHost, cremotePort)
|
||||
|
||||
// Set up HTTP routes
|
||||
http.Handle("/mcp", mcpServer)
|
||||
|
||||
// Health check endpoint
|
||||
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "healthy",
|
||||
"cremote_host": cremoteHost,
|
||||
"cremote_port": strconv.Itoa(cremotePort),
|
||||
})
|
||||
})
|
||||
|
||||
// Root endpoint with basic info
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"service": "Cremote MCP Server",
|
||||
"version": "1.0.0",
|
||||
"endpoints": map[string]string{
|
||||
"mcp": "/mcp",
|
||||
"health": "/health",
|
||||
},
|
||||
"cremote_daemon": fmt.Sprintf("%s:%d", cremoteHost, cremotePort),
|
||||
})
|
||||
})
|
||||
|
||||
// Start the HTTP server
|
||||
addr := fmt.Sprintf("%s:%d", httpHost, httpPort)
|
||||
log.Printf("MCP HTTP server listening on http://%s", addr)
|
||||
log.Printf("MCP endpoint: http://%s/mcp", addr)
|
||||
log.Printf("Health check: http://%s/health", addr)
|
||||
|
||||
if err := http.ListenAndServe(addr, nil); err != nil {
|
||||
log.Fatalf("HTTP server failed: %v", err)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user