Clean up documentation and remove temporary files

- Remove all phase completion summaries and temporary development docs
- Remove test files and backup directories
- Update README.md to document select dropdown functionality
- Add comprehensive select action documentation with examples
- Clean project structure for production readiness

The project now has clean, production-ready documentation with:
- Main README with complete CLI documentation including select actions
- MCP server documentation with 27 comprehensive tools
- LLM usage guides and best practices
- All temporary and ancillary files removed
This commit is contained in:
Josh at WLTechBlog
2025-08-19 10:15:11 -05:00
parent 63860db70b
commit 1651c4312e
36 changed files with 15 additions and 5161 deletions

View File

@@ -1,205 +0,0 @@
# Phase 6: Documentation Updates - Completion Summary
**Date Completed**: August 17, 2025
**Version**: 2.0.0
**Status**: ✅ **COMPLETE** - Production Ready
## 🎉 Phase 6 Deliverables Completed
### ✅ 1. Updated README.md with Complete Tool List
**File**: `mcp/README.md`
**Status**: ✅ Complete
**Key Updates:**
- Updated header to reflect **27 comprehensive tools** across 5 phases
- Reorganized tools by category (Core, Phase 1-5)
- Added comprehensive capability matrix
- Updated tool numbering (1-27) with proper categorization
- Added enhanced workflow examples
- Updated benefits section with 10x efficiency metrics
- Added production readiness indicators
**New Sections Added:**
- 🎉 Complete Web Automation Platform overview
- Tool categorization by enhancement phases
- Advanced workflow examples (Basic + E-commerce)
- Key Benefits for LLM Agents section
- Production Ready status with capability matrix
### ✅ 2. Updated LLM_USAGE_GUIDE.md with Complete Documentation
**File**: `mcp/LLM_USAGE_GUIDE.md`
**Status**: ✅ Complete
**Key Updates:**
- Updated introduction to reflect **27 tools** across 5 phases
- Verified all 27 tools are documented with complete examples
- Added advanced workflow examples section
- Added comprehensive best practices for LLM agents
- Added production readiness guidelines
**New Sections Added:**
- 🚀 Advanced Workflow Examples (Form completion, Data extraction)
- 🎯 Best Practices for LLM Agents (Batch operations, Element checking)
- Enhanced debugging guidelines
- Production optimization tips
### ✅ 3. Updated QUICK_REFERENCE.md with All Tools
**File**: `mcp/QUICK_REFERENCE.md`
**Status**: ✅ Complete
**Key Updates:**
- Updated header to reflect complete platform status
- Reorganized tools by category for easy lookup
- Added efficiency tips section
- Enhanced error handling guidelines
- Added production readiness summary
**New Sections Added:**
- Tool categorization by enhancement phases
- 🚀 Efficiency Tips (10x faster operations)
- Smart Element Checking guidelines
- Enhanced Debugging practices
- Production Ready capability matrix
### ✅ 4. Created Comprehensive Workflow Examples
**File**: `mcp/WORKFLOW_EXAMPLES.md` *(New)*
**Status**: ✅ Complete
**Content Created:**
- 9 comprehensive workflow examples
- Form automation workflows (Traditional vs Enhanced)
- Data extraction workflows (E-commerce, Contact info)
- Page analysis workflows (Health check, Form validation)
- File management workflows
- Advanced automation patterns
- Performance optimization examples
**Key Features:**
- Side-by-side comparison of traditional vs enhanced approaches
- Real-world use cases with complete code examples
- Error handling and conditional logic examples
- Best practices summary
### ✅ 5. Added Performance and Best Practices Section
**File**: `mcp/PERFORMANCE_BEST_PRACTICES.md` *(New)*
**Status**: ✅ Complete
**Content Created:**
- Performance optimization guidelines
- Batch operations best practices
- Error prevention strategies
- Timeout management guidelines
- Resource management practices
- Performance monitoring techniques
- Debugging best practices
- Production deployment guidelines
**Key Metrics Documented:**
- **10x Form Efficiency**: Complete forms in 1-2 calls instead of 10+
- **5x Data Extraction**: Batch extraction vs individual calls
- **3x File Operations**: Bulk operations vs individual transfers
- Real-world performance benchmarks
### ✅ 6. Updated Version Numbers and Completion Status
**Files Updated**: `mcp/main.go`, All documentation files
**Status**: ✅ Complete
**Version Updates:**
- Updated MCP server version from "1.0.0" to "2.0.0"
- Reflects major enhancement completion across all 5 phases
- Updated all documentation to reflect production-ready status
## 📊 Final Documentation Portfolio
### Core Documentation (Updated)
1. **README.md** - Main project documentation with 27 tools
2. **LLM_USAGE_GUIDE.md** - Comprehensive usage guide for LLM agents
3. **QUICK_REFERENCE.md** - Quick lookup reference for all tools
### New Documentation (Created)
4. **WORKFLOW_EXAMPLES.md** - Comprehensive workflow examples
5. **PERFORMANCE_BEST_PRACTICES.md** - Performance optimization guide
6. **PHASE6_COMPLETION_SUMMARY.md** - This completion summary
### Configuration Files
7. **claude_desktop_config.json** - Claude Desktop configuration
8. **go.mod** - Go module configuration
## 🎯 Key Achievements
### Documentation Quality
- **Comprehensive Coverage**: All 27 tools fully documented
- **LLM Optimized**: Specifically designed for AI agent consumption
- **Production Ready**: Complete deployment and optimization guides
- **Real-World Examples**: Practical workflows for common use cases
### Performance Documentation
- **Efficiency Metrics**: Documented 10x performance improvements
- **Best Practices**: Comprehensive optimization guidelines
- **Error Prevention**: Smart element checking strategies
- **Resource Management**: Production deployment considerations
### User Experience
- **Multiple Formats**: Quick reference, detailed guide, and examples
- **Categorized Organization**: Tools organized by capability and phase
- **Progressive Complexity**: From basic usage to advanced patterns
- **Production Focus**: Ready for real-world deployment
## 🚀 Production Readiness Indicators
### ✅ Complete Feature Set
- **27 Tools**: Comprehensive web automation capabilities
- **5 Enhancement Phases**: Systematic capability building
- **Batch Operations**: 10x efficiency improvements
- **Smart Element Checking**: Error prevention and conditional logic
### ✅ Comprehensive Documentation
- **Multiple Documentation Types**: Reference, guide, examples, best practices
- **LLM Optimized**: Designed for AI agent consumption
- **Production Guidelines**: Deployment and optimization instructions
- **Performance Benchmarks**: Real-world efficiency metrics
### ✅ Quality Assurance
- **All Tools Documented**: Complete coverage of 27 tools
- **Consistent Formatting**: Standardized documentation structure
- **Version Control**: Updated to v2.0.0 reflecting completion
- **Cross-Referenced**: Consistent information across all documents
## 📈 Impact Summary
### For LLM Agents
- **10x Form Efficiency**: Complete forms in 1-2 calls instead of 10+
- **Batch Operations**: Multiple data extractions in single calls
- **Smart Element Checking**: Conditional logic without timing issues
- **Rich Context**: Page state, performance metrics, content verification
### For Developers
- **Production Ready**: Complete deployment and optimization guides
- **Best Practices**: Comprehensive performance optimization guidelines
- **Error Prevention**: Smart strategies for reliable automation
- **Resource Management**: Efficient file and memory management
### For Organizations
- **Scalable Solution**: Production-ready web automation platform
- **Cost Effective**: Significant efficiency improvements reduce resource usage
- **Reliable**: Error prevention and smart checking strategies
- **Maintainable**: Comprehensive documentation and best practices
## 🎉 Final Status
**Phase 6 Status**: ✅ **COMPLETE**
**Overall Project Status**: ✅ **PRODUCTION READY**
**Documentation Status**: ✅ **COMPREHENSIVE**
**Version**: 2.0.0
### Ready for Production Deployment
The cremote MCP server is now a **complete web automation platform** with:
- **27 comprehensive tools** across 5 enhancement phases
- **Complete documentation** optimized for LLM agents
- **Production deployment guides** with performance optimization
- **Real-world workflow examples** for common automation tasks
- **Best practices documentation** for reliable operation
---
**🚀 Mission Accomplished**: Phase 6 documentation updates complete. The cremote MCP server is now production-ready with comprehensive documentation, delivering 10x efficiency improvements for LLM-driven web automation workflows.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,923 +0,0 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"io"
"log"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"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-stdio-%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 STDIO 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 STDIO 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
screenshots []string
}
// MCPRequest represents an incoming MCP request
type MCPRequest struct {
JSONRPC string `json:"jsonrpc,omitempty"`
Method string `json:"method"`
Params map[string]interface{} `json:"params"`
ID interface{} `json:"id"`
}
// MCPResponse represents an MCP response
type MCPResponse struct {
JSONRPC string `json:"jsonrpc,omitempty"`
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 {
c := client.NewClient(host, port)
return &MCPServer{
client: c,
tabHistory: make([]string, 0),
screenshots: make([]string, 0),
}
}
// HandleRequest processes an MCP request and returns a response
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: fmt.Sprintf("Method not found: %s", req.Method),
},
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 {
debugLogger.Log("handleInitialize: Processing initialize request")
debugLogger.LogJSON("Initialize request params", req.Params)
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",
},
}
debugLogger.LogJSON("Initialize response result", result)
return MCPResponse{
Result: result,
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 and returns the result
func (s *MCPServer) handleToolCall(req MCPRequest) MCPResponse {
debugLogger.Log("handleToolCall: Processing tool call request")
debugLogger.LogJSON("Tool call request params", req.Params)
params := req.Params
toolName, ok := params["name"].(string)
if !ok || toolName == "" {
debugLogger.Log("handleToolCall: Tool name missing or invalid")
return MCPResponse{
Error: &MCPError{Code: -32602, Message: "Missing tool name"},
ID: req.ID,
}
}
debugLogger.Log("handleToolCall: Tool name extracted: %s", toolName)
arguments, _ := params["arguments"].(map[string]interface{})
if arguments == nil {
debugLogger.Log("handleToolCall: No arguments provided, using empty map")
arguments = make(map[string]interface{})
} else {
debugLogger.LogJSON("Tool arguments", arguments)
}
var result ToolResult
var err error
debugLogger.Log("handleToolCall: Dispatching to tool handler: %s", toolName)
switch toolName {
case "web_navigate":
result, err = s.handleNavigate(arguments)
case "web_interact":
result, err = s.handleInteract(arguments)
case "web_extract":
result, err = s.handleExtract(arguments)
case "web_screenshot":
result, err = s.handleScreenshot(arguments)
case "web_manage_tabs":
result, err = s.handleManageTabs(arguments)
case "web_iframe":
result, err = s.handleIframe(arguments)
default:
debugLogger.Log("handleToolCall: Unknown tool: %s", toolName)
return MCPResponse{
Error: &MCPError{Code: -32601, Message: fmt.Sprintf("Unknown tool: %s", toolName)},
ID: req.ID,
}
}
debugLogger.LogJSON("Tool execution result", result)
if err != nil {
debugLogger.Log("handleToolCall: Tool execution error: %v", err)
return MCPResponse{
Error: &MCPError{Code: -32603, Message: err.Error()},
ID: req.ID,
}
}
// Always include current state in response
result.CurrentTab = s.currentTab
result.TabHistory = s.tabHistory
result.IframeMode = s.iframeMode
response := MCPResponse{Result: result, ID: req.ID}
debugLogger.LogJSON("Final tool call response", response)
debugLogger.Log("handleToolCall: Completed successfully for tool: %s", toolName)
return response
}
// Helper functions for parameter extraction
func getStringParam(params map[string]interface{}, key, defaultValue string) string {
if val, ok := params[key].(string); ok {
return val
}
return defaultValue
}
func getIntParam(params map[string]interface{}, key string, defaultValue int) int {
if val, ok := params[key].(float64); ok {
return int(val)
}
return defaultValue
}
func getBoolParam(params map[string]interface{}, key string, defaultValue bool) bool {
if val, ok := params[key].(bool); ok {
return val
}
return defaultValue
}
// resolveTabID returns the tab ID to use, defaulting to current tab
func (s *MCPServer) resolveTabID(tabID string) string {
if tabID != "" {
return tabID
}
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", "")
screenshot := getBoolParam(params, "screenshot", false)
timeout := getIntParam(params, "timeout", 5)
// If no tab specified and no current tab, open a new 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
} else {
// Update current tab if specified
s.currentTab = tab
// Move to end of history if it exists, otherwise add it
s.removeTabFromHistory(tab)
s.tabHistory = append(s.tabHistory, tab)
}
// Navigate to URL
err := s.client.LoadURL(tab, url, timeout)
if err != nil {
return ToolResult{}, fmt.Errorf("failed to navigate to %s: %w", url, err)
}
result := ToolResult{
Success: true,
Data: map[string]string{
"url": url,
"tab": tab,
},
}
// Take screenshot if requested
if screenshot {
screenshotPath := fmt.Sprintf("/tmp/navigate-%d.png", time.Now().Unix())
err = s.client.TakeScreenshot(tab, screenshotPath, false, timeout)
if err != nil {
// Don't fail the whole operation for screenshot errors
result.Metadata = map[string]string{
"screenshot_error": err.Error(),
}
} else {
result.Screenshot = screenshotPath
s.screenshots = append(s.screenshots, screenshotPath)
}
}
return result, nil
}
// handleInteract handles 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 == "" {
return ToolResult{}, fmt.Errorf("action parameter is required")
}
if selector == "" {
return ToolResult{}, fmt.Errorf("selector parameter is 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 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
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 string
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 type")
}
data, err = s.client.GetElementHTML(tab, selector, timeout)
case "javascript":
if code == "" {
return ToolResult{}, fmt.Errorf("code parameter is required for javascript type")
}
data, err = s.client.EvalJS(tab, code, timeout)
default:
return ToolResult{}, fmt.Errorf("unknown extract type: %s", extractType)
}
if err != nil {
return ToolResult{}, fmt.Errorf("failed to extract %s: %w", extractType, err)
}
return ToolResult{
Success: true,
Data: data,
}, 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)
}
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
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, 5) // Default 5 second timeout
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
}
// removeTabFromHistory removes a tab from history and updates current tab
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 s.currentTab == tabID {
if len(s.tabHistory) > 0 {
s.currentTab = s.tabHistory[len(s.tabHistory)-1]
} else {
s.currentTab = ""
}
}
}
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 STDIO Server starting up")
}
// Set up signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
host := os.Getenv("CREMOTE_HOST")
if host == "" {
host = "localhost"
}
portStr := os.Getenv("CREMOTE_PORT")
port := 8989
if portStr != "" {
if p, err := strconv.Atoi(portStr); err == nil {
port = p
}
}
debugLogger.Log("Connecting to cremote daemon at %s:%d", host, port)
log.Printf("Starting MCP stdio server, connecting to cremote daemon at %s:%d", host, port)
server := NewMCPServer(host, port)
// Create a buffered reader for better EOF handling
reader := bufio.NewReader(os.Stdin)
log.Printf("MCP stdio server ready, waiting for requests...")
// Channel to signal when to stop reading
done := make(chan bool)
// Goroutine to handle stdin reading
go func() {
defer close(done)
for {
// Read headers for Content-Length framing (use the same reader for headers and body)
contentLength := -1
for {
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
log.Printf("Input stream closed, shutting down MCP server")
return
}
log.Printf("Error reading header: %v", err)
return
}
line = strings.TrimRight(line, "\r\n")
if line == "" {
break // end of headers
}
const prefix = "content-length: "
low := strings.ToLower(line)
if strings.HasPrefix(low, prefix) {
var err error
contentLength, err = strconv.Atoi(strings.TrimSpace(low[len(prefix):]))
if err != nil {
log.Printf("Invalid Content-Length: %v", err)
return
}
}
}
if contentLength < 0 {
log.Printf("Missing Content-Length header")
return
}
// Read body of specified length
debugLogger.Log("Reading request body of length: %d", contentLength)
body := make([]byte, contentLength)
if _, err := io.ReadFull(reader, body); err != nil {
debugLogger.Log("Error reading body: %v", err)
log.Printf("Error reading body: %v", err)
return
}
debugLogger.Log("Raw request body: %s", string(body))
var req MCPRequest
if err := json.Unmarshal(body, &req); err != nil {
debugLogger.Log("Error decoding request body: %v", err)
log.Printf("Error decoding request body: %v", err)
continue
}
debugLogger.Log("Successfully parsed request: %s (ID: %v)", req.Method, req.ID)
log.Printf("Processing request: %s (ID: %v)", req.Method, req.ID)
resp := server.HandleRequest(req)
resp.JSONRPC = "2.0"
// Write Content-Length framed response
responseBytes, err := json.Marshal(resp)
if err != nil {
debugLogger.Log("Error marshaling response: %v", err)
log.Printf("Error marshaling response: %v", err)
continue
}
debugLogger.Log("Sending response with Content-Length: %d", len(responseBytes))
debugLogger.Log("Raw response: %s", string(responseBytes))
fmt.Fprintf(os.Stdout, "Content-Length: %d\r\n\r\n", len(responseBytes))
os.Stdout.Write(responseBytes)
debugLogger.Log("Response sent successfully for request: %s", req.Method)
log.Printf("Completed request: %s", req.Method)
}
}()
// Wait for either completion or signal
select {
case <-done:
log.Printf("Input processing completed")
case sig := <-sigChan:
log.Printf("Received signal %v, shutting down", sig)
}
log.Printf("MCP stdio server shutdown complete")
}

View File

@@ -1,822 +0,0 @@
//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, 5) // Default 5 second timeout
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)
}
}

View File

@@ -1,11 +0,0 @@
{
"mcpServers": {
"cremote": {
"command": "/path/to/cremote/mcp/cremote-mcp",
"env": {
"CREMOTE_HOST": "localhost",
"CREMOTE_PORT": "8989"
}
}
}
}

View File

@@ -1,5 +0,0 @@
#!/bin/bash
(
echo '{"method":"tools/list","params":{},"id":1}'
sleep 1
) | ./cremote-mcp