cli/pkg/mcp/mcp.go

1550 lines
45 KiB
Go
Raw Normal View History

// Package mcp provides an MCP (Model Context Protocol) server for Core.
// This allows Claude Code and other MCP clients to interact with Core's
// IDE, file system, and display services.
package mcp
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/host-uk/core/pkg/core"
"github.com/host-uk/core/pkg/display"
"github.com/host-uk/core/pkg/ide"
"github.com/host-uk/core/pkg/process"
"github.com/host-uk/core/pkg/webview"
"github.com/host-uk/core/pkg/ws"
)
// Service provides an MCP server that exposes Core functionality.
type Service struct {
core *core.Core
server *mcp.Server
ide *ide.Service
display *display.Service
process *process.Service
webview *webview.Service
wsHub *ws.Hub
wsPort int
wsRunning bool
}
// New creates a new MCP service.
func New(c *core.Core) *Service {
impl := &mcp.Implementation{
Name: "core",
Version: "0.1.0",
}
server := mcp.NewServer(impl, nil)
s := &Service{
core: c,
server: server,
process: process.New(),
}
// Try to get the IDE service if available
if c != nil {
ideSvc, _ := core.ServiceFor[*ide.Service](c, "github.com/host-uk/core/ide")
s.ide = ideSvc
}
s.registerTools()
return s
}
// NewStandalone creates an MCP service without a Core instance.
// This allows running the MCP server independently with basic file operations.
func NewStandalone() *Service {
return NewStandaloneWithPort(9876)
}
// NewStandaloneWithPort creates an MCP service with a specific WebSocket port.
func NewStandaloneWithPort(wsPort int) *Service {
impl := &mcp.Implementation{
Name: "core",
Version: "0.1.0",
}
server := mcp.NewServer(impl, nil)
hub := ws.NewHub()
proc := process.New()
s := &Service{
server: server,
process: proc,
wsHub: hub,
wsPort: wsPort,
}
// Wire process output to WebSocket
proc.OnOutput(func(processID string, output string) {
hub.SendProcessOutput(processID, output)
})
proc.OnStatusChange(func(processID string, status process.Status, exitCode int) {
hub.SendProcessStatus(processID, string(status), exitCode)
})
s.registerTools()
return s
}
// registerTools adds all Core tools to the MCP server.
// Naming convention: prefix_action for discoverability
// file_* dir_* lang_* process_*
func (s *Service) registerTools() {
// File operations
mcp.AddTool(s.server, &mcp.Tool{
Name: "file_read",
Description: "Read the contents of a file",
}, s.readFile)
mcp.AddTool(s.server, &mcp.Tool{
Name: "file_write",
Description: "Write content to a file",
}, s.writeFile)
mcp.AddTool(s.server, &mcp.Tool{
Name: "file_delete",
Description: "Delete a file or empty directory",
}, s.deleteFile)
mcp.AddTool(s.server, &mcp.Tool{
Name: "file_rename",
Description: "Rename or move a file",
}, s.renameFile)
mcp.AddTool(s.server, &mcp.Tool{
Name: "file_exists",
Description: "Check if a file or directory exists",
}, s.fileExists)
mcp.AddTool(s.server, &mcp.Tool{
Name: "file_edit",
Description: "Edit a file by replacing old_string with new_string. Use replace_all=true to replace all occurrences.",
}, s.editDiff)
// Directory operations
mcp.AddTool(s.server, &mcp.Tool{
Name: "dir_list",
Description: "List contents of a directory",
}, s.listDirectory)
mcp.AddTool(s.server, &mcp.Tool{
Name: "dir_create",
Description: "Create a new directory",
}, s.createDirectory)
// Language detection
mcp.AddTool(s.server, &mcp.Tool{
Name: "lang_detect",
Description: "Detect the programming language of a file",
}, s.detectLanguage)
mcp.AddTool(s.server, &mcp.Tool{
Name: "lang_list",
Description: "Get list of supported programming languages",
}, s.getSupportedLanguages)
// Process management
mcp.AddTool(s.server, &mcp.Tool{
Name: "process_start",
Description: "Start a new process with the given command and arguments",
}, s.processStart)
mcp.AddTool(s.server, &mcp.Tool{
Name: "process_stop",
Description: "Stop a running process gracefully",
}, s.processStop)
mcp.AddTool(s.server, &mcp.Tool{
Name: "process_kill",
Description: "Forcefully kill a process",
}, s.processKill)
mcp.AddTool(s.server, &mcp.Tool{
Name: "process_list",
Description: "List all managed processes",
}, s.processList)
mcp.AddTool(s.server, &mcp.Tool{
Name: "process_output",
Description: "Get the output of a process",
}, s.processOutput)
mcp.AddTool(s.server, &mcp.Tool{
Name: "process_input",
Description: "Send input to a running process stdin",
}, s.processSendInput)
// WebSocket streaming
mcp.AddTool(s.server, &mcp.Tool{
Name: "ws_start",
Description: "Start WebSocket server for real-time streaming",
}, s.wsStart)
mcp.AddTool(s.server, &mcp.Tool{
Name: "ws_info",
Description: "Get WebSocket server info (port, connected clients)",
}, s.wsInfo)
// WebView interaction (only available when embedded in GUI app)
mcp.AddTool(s.server, &mcp.Tool{
Name: "webview_list",
Description: "List all open windows in the application",
}, s.webviewList)
mcp.AddTool(s.server, &mcp.Tool{
Name: "webview_eval",
Description: "Execute JavaScript in a window and return the result",
}, s.webviewEval)
mcp.AddTool(s.server, &mcp.Tool{
Name: "webview_console",
Description: "Get captured console messages (log, warn, error) from the WebView",
}, s.webviewConsole)
mcp.AddTool(s.server, &mcp.Tool{
Name: "webview_click",
Description: "Click an element by CSS selector",
}, s.webviewClick)
mcp.AddTool(s.server, &mcp.Tool{
Name: "webview_type",
Description: "Type text into an element by CSS selector",
}, s.webviewType)
mcp.AddTool(s.server, &mcp.Tool{
Name: "webview_query",
Description: "Query elements by CSS selector and return info about matches",
}, s.webviewQuery)
mcp.AddTool(s.server, &mcp.Tool{
Name: "webview_navigate",
Description: "Navigate to a URL or Angular route",
}, s.webviewNavigate)
mcp.AddTool(s.server, &mcp.Tool{
Name: "webview_source",
Description: "Get the current page HTML source",
}, s.webviewSource)
// Window/Display management (the unique value-add for native app control)
mcp.AddTool(s.server, &mcp.Tool{
Name: "window_list",
Description: "List all windows with their positions and sizes",
}, s.windowList)
mcp.AddTool(s.server, &mcp.Tool{
Name: "window_get",
Description: "Get detailed info about a specific window",
}, s.windowGet)
mcp.AddTool(s.server, &mcp.Tool{
Name: "window_position",
Description: "Move a window to a specific position (x, y coordinates)",
}, s.windowPosition)
mcp.AddTool(s.server, &mcp.Tool{
Name: "window_size",
Description: "Resize a window to specific dimensions",
}, s.windowSize)
mcp.AddTool(s.server, &mcp.Tool{
Name: "window_bounds",
Description: "Set both position and size of a window in one call",
}, s.windowBounds)
mcp.AddTool(s.server, &mcp.Tool{
Name: "window_maximize",
Description: "Maximize a window",
}, s.windowMaximize)
mcp.AddTool(s.server, &mcp.Tool{
Name: "window_minimize",
Description: "Minimize a window",
}, s.windowMinimize)
mcp.AddTool(s.server, &mcp.Tool{
Name: "window_restore",
Description: "Restore a window from maximized/minimized state",
}, s.windowRestore)
mcp.AddTool(s.server, &mcp.Tool{
Name: "window_focus",
Description: "Bring a window to the front and focus it",
}, s.windowFocus)
mcp.AddTool(s.server, &mcp.Tool{
Name: "screen_list",
Description: "List all available screens/monitors with their dimensions",
}, s.screenList)
}
// SetWebView sets the WebView service for GUI interaction.
// This must be called when running embedded in the GUI app.
func (s *Service) SetWebView(wv *webview.Service) {
s.webview = wv
}
// SetDisplay sets the Display service for window management.
// This must be called when running embedded in the GUI app.
func (s *Service) SetDisplay(d *display.Service) {
s.display = d
}
// Tool input/output types
// ReadFileInput contains parameters for reading a file.
type ReadFileInput struct {
// Absolute path to the file to read.
Path string `json:"path"`
}
// ReadFileOutput contains the result of reading a file.
type ReadFileOutput struct {
Content string `json:"content"`
Language string `json:"language"`
Path string `json:"path"`
}
// WriteFileInput contains parameters for writing a file.
type WriteFileInput struct {
// Absolute path to the file to write.
Path string `json:"path"`
// Content to write to the file.
Content string `json:"content"`
}
// WriteFileOutput contains the result of writing a file.
type WriteFileOutput struct {
Success bool `json:"success"`
Path string `json:"path"`
}
// ListDirectoryInput contains parameters for listing a directory.
type ListDirectoryInput struct {
// Absolute path to the directory to list.
Path string `json:"path"`
}
// ListDirectoryOutput contains the result of listing a directory.
type ListDirectoryOutput struct {
Entries []DirectoryEntry `json:"entries"`
Path string `json:"path"`
}
// DirectoryEntry represents a file or directory entry.
type DirectoryEntry struct {
Name string `json:"name"`
Path string `json:"path"`
IsDir bool `json:"isDir"`
Size int64 `json:"size"`
}
// CreateDirectoryInput contains parameters for creating a directory.
type CreateDirectoryInput struct {
// Absolute path to the directory to create.
Path string `json:"path"`
}
// CreateDirectoryOutput contains the result of creating a directory.
type CreateDirectoryOutput struct {
Success bool `json:"success"`
Path string `json:"path"`
}
// DeleteFileInput contains parameters for deleting a file.
type DeleteFileInput struct {
// Absolute path to the file to delete.
Path string `json:"path"`
}
// DeleteFileOutput contains the result of deleting a file.
type DeleteFileOutput struct {
Success bool `json:"success"`
Path string `json:"path"`
}
// RenameFileInput contains parameters for renaming a file.
type RenameFileInput struct {
// Current path of the file.
OldPath string `json:"oldPath"`
// New path for the file.
NewPath string `json:"newPath"`
}
// RenameFileOutput contains the result of renaming a file.
type RenameFileOutput struct {
Success bool `json:"success"`
OldPath string `json:"oldPath"`
NewPath string `json:"newPath"`
}
// FileExistsInput contains parameters for checking if a file exists.
type FileExistsInput struct {
// Absolute path to check.
Path string `json:"path"`
}
// FileExistsOutput contains the result of checking file existence.
type FileExistsOutput struct {
Exists bool `json:"exists"`
IsDir bool `json:"isDir"`
Path string `json:"path"`
}
// DetectLanguageInput contains parameters for detecting file language.
type DetectLanguageInput struct {
// File path to detect language for.
Path string `json:"path"`
}
type DetectLanguageOutput struct {
Language string `json:"language"`
Path string `json:"path"`
}
type GetSupportedLanguagesInput struct{}
type GetSupportedLanguagesOutput struct {
Languages []LanguageInfo `json:"languages"`
}
type LanguageInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Extensions []string `json:"extensions"`
}
// EditDiffInput contains parameters for diff-based editing.
type EditDiffInput struct {
// Absolute path to the file to edit.
Path string `json:"path"`
// The text to find and replace.
OldString string `json:"old_string"`
// The replacement text.
NewString string `json:"new_string"`
// Replace all occurrences if true, otherwise only the first.
ReplaceAll bool `json:"replace_all,omitempty"`
}
// EditDiffOutput contains the result of the edit.
type EditDiffOutput struct {
Path string `json:"path"`
Success bool `json:"success"`
Replacements int `json:"replacements"`
}
// Tool handlers
func (s *Service) readFile(ctx context.Context, req *mcp.CallToolRequest, input ReadFileInput) (*mcp.CallToolResult, ReadFileOutput, error) {
if s.ide != nil {
info, err := s.ide.OpenFile(input.Path)
if err != nil {
return nil, ReadFileOutput{}, fmt.Errorf("failed to read file: %w", err)
}
return nil, ReadFileOutput{
Content: info.Content,
Language: info.Language,
Path: info.Path,
}, nil
}
// Fallback to direct file read
content, err := os.ReadFile(input.Path)
if err != nil {
return nil, ReadFileOutput{}, fmt.Errorf("failed to read file: %w", err)
}
return nil, ReadFileOutput{
Content: string(content),
Language: detectLanguage(input.Path),
Path: input.Path,
}, nil
}
func (s *Service) writeFile(ctx context.Context, req *mcp.CallToolRequest, input WriteFileInput) (*mcp.CallToolResult, WriteFileOutput, error) {
if s.ide != nil {
err := s.ide.SaveFile(input.Path, input.Content)
if err != nil {
return nil, WriteFileOutput{}, fmt.Errorf("failed to write file: %w", err)
}
return nil, WriteFileOutput{Success: true, Path: input.Path}, nil
}
// Fallback to direct file write
dir := filepath.Dir(input.Path)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, WriteFileOutput{}, fmt.Errorf("failed to create directory: %w", err)
}
err := os.WriteFile(input.Path, []byte(input.Content), 0644)
if err != nil {
return nil, WriteFileOutput{}, fmt.Errorf("failed to write file: %w", err)
}
return nil, WriteFileOutput{Success: true, Path: input.Path}, nil
}
func (s *Service) listDirectory(ctx context.Context, req *mcp.CallToolRequest, input ListDirectoryInput) (*mcp.CallToolResult, ListDirectoryOutput, error) {
if s.ide != nil {
entries, err := s.ide.ListDirectory(input.Path)
if err != nil {
return nil, ListDirectoryOutput{}, fmt.Errorf("failed to list directory: %w", err)
}
result := make([]DirectoryEntry, 0, len(entries))
for _, e := range entries {
result = append(result, DirectoryEntry{
Name: e.Name,
Path: e.Path,
IsDir: e.IsDir,
Size: e.Size,
})
}
return nil, ListDirectoryOutput{Entries: result, Path: input.Path}, nil
}
// Fallback to direct directory listing
entries, err := os.ReadDir(input.Path)
if err != nil {
return nil, ListDirectoryOutput{}, fmt.Errorf("failed to list directory: %w", err)
}
result := make([]DirectoryEntry, 0, len(entries))
for _, e := range entries {
info, _ := e.Info()
var size int64
if info != nil {
size = info.Size()
}
result = append(result, DirectoryEntry{
Name: e.Name(),
Path: filepath.Join(input.Path, e.Name()),
IsDir: e.IsDir(),
Size: size,
})
}
return nil, ListDirectoryOutput{Entries: result, Path: input.Path}, nil
}
func (s *Service) createDirectory(ctx context.Context, req *mcp.CallToolRequest, input CreateDirectoryInput) (*mcp.CallToolResult, CreateDirectoryOutput, error) {
if s.ide != nil {
err := s.ide.CreateDirectory(input.Path)
if err != nil {
return nil, CreateDirectoryOutput{}, fmt.Errorf("failed to create directory: %w", err)
}
return nil, CreateDirectoryOutput{Success: true, Path: input.Path}, nil
}
err := os.MkdirAll(input.Path, 0755)
if err != nil {
return nil, CreateDirectoryOutput{}, fmt.Errorf("failed to create directory: %w", err)
}
return nil, CreateDirectoryOutput{Success: true, Path: input.Path}, nil
}
func (s *Service) deleteFile(ctx context.Context, req *mcp.CallToolRequest, input DeleteFileInput) (*mcp.CallToolResult, DeleteFileOutput, error) {
if s.ide != nil {
err := s.ide.DeleteFile(input.Path)
if err != nil {
return nil, DeleteFileOutput{}, fmt.Errorf("failed to delete file: %w", err)
}
return nil, DeleteFileOutput{Success: true, Path: input.Path}, nil
}
err := os.Remove(input.Path)
if err != nil {
return nil, DeleteFileOutput{}, fmt.Errorf("failed to delete file: %w", err)
}
return nil, DeleteFileOutput{Success: true, Path: input.Path}, nil
}
func (s *Service) renameFile(ctx context.Context, req *mcp.CallToolRequest, input RenameFileInput) (*mcp.CallToolResult, RenameFileOutput, error) {
if s.ide != nil {
err := s.ide.RenameFile(input.OldPath, input.NewPath)
if err != nil {
return nil, RenameFileOutput{}, fmt.Errorf("failed to rename file: %w", err)
}
return nil, RenameFileOutput{Success: true, OldPath: input.OldPath, NewPath: input.NewPath}, nil
}
err := os.Rename(input.OldPath, input.NewPath)
if err != nil {
return nil, RenameFileOutput{}, fmt.Errorf("failed to rename file: %w", err)
}
return nil, RenameFileOutput{Success: true, OldPath: input.OldPath, NewPath: input.NewPath}, nil
}
func (s *Service) fileExists(ctx context.Context, req *mcp.CallToolRequest, input FileExistsInput) (*mcp.CallToolResult, FileExistsOutput, error) {
info, err := os.Stat(input.Path)
if os.IsNotExist(err) {
return nil, FileExistsOutput{Exists: false, IsDir: false, Path: input.Path}, nil
}
if err != nil {
return nil, FileExistsOutput{}, fmt.Errorf("failed to check file: %w", err)
}
return nil, FileExistsOutput{Exists: true, IsDir: info.IsDir(), Path: input.Path}, nil
}
func (s *Service) detectLanguage(ctx context.Context, req *mcp.CallToolRequest, input DetectLanguageInput) (*mcp.CallToolResult, DetectLanguageOutput, error) {
lang := detectLanguage(input.Path)
return nil, DetectLanguageOutput{Language: lang, Path: input.Path}, nil
}
func (s *Service) getSupportedLanguages(ctx context.Context, req *mcp.CallToolRequest, input GetSupportedLanguagesInput) (*mcp.CallToolResult, GetSupportedLanguagesOutput, error) {
languages := []LanguageInfo{
{ID: "typescript", Name: "TypeScript", Extensions: []string{".ts", ".tsx"}},
{ID: "javascript", Name: "JavaScript", Extensions: []string{".js", ".jsx"}},
{ID: "go", Name: "Go", Extensions: []string{".go"}},
{ID: "python", Name: "Python", Extensions: []string{".py"}},
{ID: "rust", Name: "Rust", Extensions: []string{".rs"}},
{ID: "java", Name: "Java", Extensions: []string{".java"}},
{ID: "csharp", Name: "C#", Extensions: []string{".cs"}},
{ID: "cpp", Name: "C++", Extensions: []string{".cpp", ".hpp", ".cc", ".cxx"}},
{ID: "c", Name: "C", Extensions: []string{".c", ".h"}},
{ID: "html", Name: "HTML", Extensions: []string{".html", ".htm"}},
{ID: "css", Name: "CSS", Extensions: []string{".css"}},
{ID: "scss", Name: "SCSS", Extensions: []string{".scss"}},
{ID: "json", Name: "JSON", Extensions: []string{".json"}},
{ID: "yaml", Name: "YAML", Extensions: []string{".yaml", ".yml"}},
{ID: "markdown", Name: "Markdown", Extensions: []string{".md", ".markdown"}},
{ID: "sql", Name: "SQL", Extensions: []string{".sql"}},
{ID: "shell", Name: "Shell", Extensions: []string{".sh", ".bash"}},
{ID: "xml", Name: "XML", Extensions: []string{".xml"}},
{ID: "swift", Name: "Swift", Extensions: []string{".swift"}},
{ID: "kotlin", Name: "Kotlin", Extensions: []string{".kt", ".kts"}},
{ID: "php", Name: "PHP", Extensions: []string{".php"}},
{ID: "ruby", Name: "Ruby", Extensions: []string{".rb"}},
}
return nil, GetSupportedLanguagesOutput{Languages: languages}, nil
}
func (s *Service) editDiff(ctx context.Context, req *mcp.CallToolRequest, input EditDiffInput) (*mcp.CallToolResult, EditDiffOutput, error) {
// Read the file
content, err := os.ReadFile(input.Path)
if err != nil {
return nil, EditDiffOutput{}, fmt.Errorf("failed to read file: %w", err)
}
fileContent := string(content)
count := 0
if input.ReplaceAll {
// Count occurrences
count = strings.Count(fileContent, input.OldString)
if count == 0 {
return nil, EditDiffOutput{}, fmt.Errorf("old_string not found in file")
}
fileContent = strings.ReplaceAll(fileContent, input.OldString, input.NewString)
} else {
// Replace only first occurrence
if !strings.Contains(fileContent, input.OldString) {
return nil, EditDiffOutput{}, fmt.Errorf("old_string not found in file")
}
fileContent = strings.Replace(fileContent, input.OldString, input.NewString, 1)
count = 1
}
// Write the file back
err = os.WriteFile(input.Path, []byte(fileContent), 0644)
if err != nil {
return nil, EditDiffOutput{}, fmt.Errorf("failed to write file: %w", err)
}
return nil, EditDiffOutput{
Path: input.Path,
Success: true,
Replacements: count,
}, nil
}
// detectLanguage maps file extensions to Monaco editor languages.
func detectLanguage(path string) string {
ext := filepath.Ext(path)
switch ext {
case ".ts", ".tsx":
return "typescript"
case ".js", ".jsx":
return "javascript"
case ".go":
return "go"
case ".py":
return "python"
case ".rs":
return "rust"
case ".rb":
return "ruby"
case ".java":
return "java"
case ".c", ".h":
return "c"
case ".cpp", ".hpp", ".cc", ".cxx":
return "cpp"
case ".cs":
return "csharp"
case ".html", ".htm":
return "html"
case ".css":
return "css"
case ".scss":
return "scss"
case ".less":
return "less"
case ".json":
return "json"
case ".yaml", ".yml":
return "yaml"
case ".xml":
return "xml"
case ".md", ".markdown":
return "markdown"
case ".sql":
return "sql"
case ".sh", ".bash":
return "shell"
case ".ps1":
return "powershell"
case ".toml":
return "toml"
case ".ini", ".cfg":
return "ini"
case ".swift":
return "swift"
case ".kt", ".kts":
return "kotlin"
case ".php":
return "php"
case ".r":
return "r"
case ".lua":
return "lua"
case ".pl", ".pm":
return "perl"
default:
if filepath.Base(path) == "Dockerfile" {
return "dockerfile"
}
return "plaintext"
}
}
// Run starts the MCP server on stdio.
func (s *Service) Run(ctx context.Context) error {
return s.server.Run(ctx, &mcp.StdioTransport{})
}
// Server returns the underlying MCP server for advanced configuration.
func (s *Service) Server() *mcp.Server {
return s.server
}
// Process management types
// ProcessStartInput contains parameters for starting a process.
type ProcessStartInput struct {
// Command to execute.
Command string `json:"command"`
// Arguments for the command.
Args []string `json:"args,omitempty"`
// Working directory for the process.
Dir string `json:"dir,omitempty"`
}
// ProcessStartOutput contains the result of starting a process.
type ProcessStartOutput struct {
ID string `json:"id"`
Command string `json:"command"`
Args []string `json:"args"`
Dir string `json:"dir"`
PID int `json:"pid"`
StartedAt time.Time `json:"startedAt"`
}
// ProcessIDInput contains a process ID parameter.
type ProcessIDInput struct {
// Process ID to operate on.
ID string `json:"id"`
}
// ProcessStopOutput contains the result of stopping a process.
type ProcessStopOutput struct {
ID string `json:"id"`
Success bool `json:"success"`
}
// ProcessListInput is empty but required for the handler signature.
type ProcessListInput struct{}
// ProcessInfo represents process information.
type ProcessInfo struct {
ID string `json:"id"`
Command string `json:"command"`
Args []string `json:"args"`
Dir string `json:"dir"`
Status string `json:"status"`
ExitCode int `json:"exitCode"`
PID int `json:"pid"`
StartedAt time.Time `json:"startedAt"`
}
// ProcessListOutput contains the list of processes.
type ProcessListOutput struct {
Processes []ProcessInfo `json:"processes"`
}
// ProcessOutputOutput contains the captured output of a process.
type ProcessOutputOutput struct {
ID string `json:"id"`
Output string `json:"output"`
Length int `json:"length"`
}
// ProcessSendInputInput contains input to send to a process.
type ProcessSendInputInput struct {
// Process ID to send input to.
ID string `json:"id"`
// Input text to send.
Input string `json:"input"`
}
// ProcessSendInputOutput contains the result of sending input.
type ProcessSendInputOutput struct {
ID string `json:"id"`
Success bool `json:"success"`
}
// Process management handlers
func (s *Service) processStart(ctx context.Context, req *mcp.CallToolRequest, input ProcessStartInput) (*mcp.CallToolResult, ProcessStartOutput, error) {
dir := input.Dir
if dir == "" {
var err error
dir, err = os.Getwd()
if err != nil {
dir = "."
}
}
proc, err := s.process.Start(input.Command, input.Args, dir)
if err != nil {
return nil, ProcessStartOutput{}, fmt.Errorf("failed to start process: %w", err)
}
info := proc.Info()
return nil, ProcessStartOutput{
ID: info.ID,
Command: info.Command,
Args: info.Args,
Dir: info.Dir,
PID: info.PID,
StartedAt: info.StartedAt,
}, nil
}
func (s *Service) processStop(ctx context.Context, req *mcp.CallToolRequest, input ProcessIDInput) (*mcp.CallToolResult, ProcessStopOutput, error) {
err := s.process.Stop(input.ID)
if err != nil {
return nil, ProcessStopOutput{}, fmt.Errorf("failed to stop process: %w", err)
}
return nil, ProcessStopOutput{ID: input.ID, Success: true}, nil
}
func (s *Service) processKill(ctx context.Context, req *mcp.CallToolRequest, input ProcessIDInput) (*mcp.CallToolResult, ProcessStopOutput, error) {
err := s.process.Kill(input.ID)
if err != nil {
return nil, ProcessStopOutput{}, fmt.Errorf("failed to kill process: %w", err)
}
return nil, ProcessStopOutput{ID: input.ID, Success: true}, nil
}
func (s *Service) processList(ctx context.Context, req *mcp.CallToolRequest, input ProcessListInput) (*mcp.CallToolResult, ProcessListOutput, error) {
procs := s.process.List()
result := make([]ProcessInfo, 0, len(procs))
for _, p := range procs {
info := p.Info()
result = append(result, ProcessInfo{
ID: info.ID,
Command: info.Command,
Args: info.Args,
Dir: info.Dir,
Status: string(info.Status),
ExitCode: info.ExitCode,
PID: info.PID,
StartedAt: info.StartedAt,
})
}
return nil, ProcessListOutput{Processes: result}, nil
}
func (s *Service) processOutput(ctx context.Context, req *mcp.CallToolRequest, input ProcessIDInput) (*mcp.CallToolResult, ProcessOutputOutput, error) {
output, err := s.process.Output(input.ID)
if err != nil {
return nil, ProcessOutputOutput{}, fmt.Errorf("failed to get process output: %w", err)
}
return nil, ProcessOutputOutput{
ID: input.ID,
Output: output,
Length: len(output),
}, nil
}
func (s *Service) processSendInput(ctx context.Context, req *mcp.CallToolRequest, input ProcessSendInputInput) (*mcp.CallToolResult, ProcessSendInputOutput, error) {
err := s.process.SendInput(input.ID, input.Input)
if err != nil {
return nil, ProcessSendInputOutput{}, fmt.Errorf("failed to send input: %w", err)
}
return nil, ProcessSendInputOutput{ID: input.ID, Success: true}, nil
}
// WebSocket types
// WsStartInput contains parameters for starting the WebSocket server.
type WsStartInput struct {
// Port to run WebSocket server on. Defaults to 9876.
Port int `json:"port,omitempty"`
}
// WsStartOutput contains the result of starting the WebSocket server.
type WsStartOutput struct {
Port int `json:"port"`
URL string `json:"url"`
Started bool `json:"started"`
}
// WsInfoInput is empty but required for handler signature.
type WsInfoInput struct{}
// WsInfoOutput contains WebSocket server status.
type WsInfoOutput struct {
Running bool `json:"running"`
Port int `json:"port"`
URL string `json:"url"`
Clients int `json:"clients"`
Channels int `json:"channels"`
}
// WebSocket handlers
func (s *Service) wsStart(ctx context.Context, req *mcp.CallToolRequest, input WsStartInput) (*mcp.CallToolResult, WsStartOutput, error) {
if s.wsHub == nil {
return nil, WsStartOutput{}, fmt.Errorf("WebSocket not available in this configuration")
}
// Already running?
if s.wsRunning {
url := fmt.Sprintf("ws://localhost:%d/ws", s.wsPort)
return nil, WsStartOutput{
Port: s.wsPort,
URL: url,
Started: true,
}, nil
}
port := input.Port
if port == 0 {
port = s.wsPort
}
if port == 0 {
port = 9876
}
// Start the hub event loop
hubCtx := context.Background()
go s.wsHub.Run(hubCtx)
// Start HTTP server for WebSocket
go func() {
mux := http.NewServeMux()
mux.HandleFunc("/ws", s.wsHub.HandleWebSocket)
addr := fmt.Sprintf(":%d", port)
http.ListenAndServe(addr, mux)
}()
s.wsPort = port
s.wsRunning = true
url := fmt.Sprintf("ws://localhost:%d/ws", port)
return nil, WsStartOutput{
Port: port,
URL: url,
Started: true,
}, nil
}
func (s *Service) wsInfo(ctx context.Context, req *mcp.CallToolRequest, input WsInfoInput) (*mcp.CallToolResult, WsInfoOutput, error) {
if s.wsHub == nil {
return nil, WsInfoOutput{Running: false}, nil
}
stats := s.wsHub.Stats()
url := ""
if s.wsRunning {
url = fmt.Sprintf("ws://localhost:%d/ws", s.wsPort)
}
return nil, WsInfoOutput{
Running: s.wsRunning,
Port: s.wsPort,
URL: url,
Clients: stats.Clients,
Channels: stats.Channels,
}, nil
}
// WebView types
// WebviewListInput is empty.
type WebviewListInput struct{}
// WebviewListOutput contains the list of windows.
type WebviewListOutput struct {
Windows []WebviewWindowInfo `json:"windows"`
}
// WebviewWindowInfo contains window information.
type WebviewWindowInfo struct {
Name string `json:"name"`
}
// WebviewEvalInput contains parameters for JS evaluation.
type WebviewEvalInput struct {
// Window name (empty for first window).
Window string `json:"window,omitempty"`
// JavaScript code to execute.
Code string `json:"code"`
}
// WebviewEvalOutput contains the evaluation result.
type WebviewEvalOutput struct {
Result string `json:"result"`
}
// WebviewConsoleInput contains parameters for console retrieval.
type WebviewConsoleInput struct {
// Filter by level: log, warn, error, info, debug (empty for all).
Level string `json:"level,omitempty"`
// Maximum messages to return.
Limit int `json:"limit,omitempty"`
// Clear buffer after reading.
Clear bool `json:"clear,omitempty"`
}
// WebviewConsoleOutput contains console messages.
type WebviewConsoleOutput struct {
Messages []webview.ConsoleMessage `json:"messages"`
Count int `json:"count"`
}
// WebviewClickInput contains parameters for clicking.
type WebviewClickInput struct {
// Window name (empty for first window).
Window string `json:"window,omitempty"`
// CSS selector for the element to click.
Selector string `json:"selector"`
}
// WebviewClickOutput contains the click result.
type WebviewClickOutput struct {
Success bool `json:"success"`
}
// WebviewTypeInput contains parameters for typing.
type WebviewTypeInput struct {
// Window name (empty for first window).
Window string `json:"window,omitempty"`
// CSS selector for the input element.
Selector string `json:"selector"`
// Text to type.
Text string `json:"text"`
}
// WebviewTypeOutput contains the type result.
type WebviewTypeOutput struct {
Success bool `json:"success"`
}
// WebviewQueryInput contains parameters for querying.
type WebviewQueryInput struct {
// Window name (empty for first window).
Window string `json:"window,omitempty"`
// CSS selector to query.
Selector string `json:"selector"`
}
// WebviewQueryOutput contains query results.
type WebviewQueryOutput struct {
Elements []map[string]any `json:"elements"`
Count int `json:"count"`
}
// WebviewNavigateInput contains parameters for navigation.
type WebviewNavigateInput struct {
// Window name (empty for first window).
Window string `json:"window,omitempty"`
// URL or route to navigate to.
URL string `json:"url"`
}
// WebviewNavigateOutput contains navigation result.
type WebviewNavigateOutput struct {
Success bool `json:"success"`
}
// WebviewSourceInput contains parameters for getting source.
type WebviewSourceInput struct {
// Window name (empty for first window).
Window string `json:"window,omitempty"`
}
// WebviewSourceOutput contains the page source.
type WebviewSourceOutput struct {
HTML string `json:"html"`
Length int `json:"length"`
}
// WebView handlers
func (s *Service) webviewList(ctx context.Context, req *mcp.CallToolRequest, input WebviewListInput) (*mcp.CallToolResult, WebviewListOutput, error) {
if s.webview == nil {
return nil, WebviewListOutput{}, fmt.Errorf("WebView not available (MCP server running standalone)")
}
windows := s.webview.ListWindows()
result := make([]WebviewWindowInfo, len(windows))
for i, w := range windows {
result[i] = WebviewWindowInfo{Name: w.Name}
}
return nil, WebviewListOutput{Windows: result}, nil
}
func (s *Service) webviewEval(ctx context.Context, req *mcp.CallToolRequest, input WebviewEvalInput) (*mcp.CallToolResult, WebviewEvalOutput, error) {
if s.webview == nil {
return nil, WebviewEvalOutput{}, fmt.Errorf("WebView not available (MCP server running standalone)")
}
result, err := s.webview.ExecJS(input.Window, input.Code)
if err != nil {
return nil, WebviewEvalOutput{}, fmt.Errorf("failed to execute JS: %w", err)
}
return nil, WebviewEvalOutput{Result: result}, nil
}
func (s *Service) webviewConsole(ctx context.Context, req *mcp.CallToolRequest, input WebviewConsoleInput) (*mcp.CallToolResult, WebviewConsoleOutput, error) {
if s.webview == nil {
return nil, WebviewConsoleOutput{}, fmt.Errorf("WebView not available (MCP server running standalone)")
}
messages := s.webview.GetConsoleMessages(input.Level, input.Limit)
if input.Clear {
s.webview.ClearConsole()
}
return nil, WebviewConsoleOutput{
Messages: messages,
Count: len(messages),
}, nil
}
func (s *Service) webviewClick(ctx context.Context, req *mcp.CallToolRequest, input WebviewClickInput) (*mcp.CallToolResult, WebviewClickOutput, error) {
if s.webview == nil {
return nil, WebviewClickOutput{}, fmt.Errorf("WebView not available (MCP server running standalone)")
}
err := s.webview.Click(input.Window, input.Selector)
if err != nil {
return nil, WebviewClickOutput{}, fmt.Errorf("failed to click: %w", err)
}
return nil, WebviewClickOutput{Success: true}, nil
}
func (s *Service) webviewType(ctx context.Context, req *mcp.CallToolRequest, input WebviewTypeInput) (*mcp.CallToolResult, WebviewTypeOutput, error) {
if s.webview == nil {
return nil, WebviewTypeOutput{}, fmt.Errorf("WebView not available (MCP server running standalone)")
}
err := s.webview.Type(input.Window, input.Selector, input.Text)
if err != nil {
return nil, WebviewTypeOutput{}, fmt.Errorf("failed to type: %w", err)
}
return nil, WebviewTypeOutput{Success: true}, nil
}
func (s *Service) webviewQuery(ctx context.Context, req *mcp.CallToolRequest, input WebviewQueryInput) (*mcp.CallToolResult, WebviewQueryOutput, error) {
if s.webview == nil {
return nil, WebviewQueryOutput{}, fmt.Errorf("WebView not available (MCP server running standalone)")
}
result, err := s.webview.QuerySelector(input.Window, input.Selector)
if err != nil {
return nil, WebviewQueryOutput{}, fmt.Errorf("failed to query: %w", err)
}
// Parse result as JSON array
var elements []map[string]any
if err := json.Unmarshal([]byte(result), &elements); err != nil {
// Return raw result if not valid JSON
return nil, WebviewQueryOutput{
Elements: []map[string]any{{"raw": result}},
Count: 1,
}, nil
}
return nil, WebviewQueryOutput{
Elements: elements,
Count: len(elements),
}, nil
}
func (s *Service) webviewNavigate(ctx context.Context, req *mcp.CallToolRequest, input WebviewNavigateInput) (*mcp.CallToolResult, WebviewNavigateOutput, error) {
if s.webview == nil {
return nil, WebviewNavigateOutput{}, fmt.Errorf("WebView not available (MCP server running standalone)")
}
err := s.webview.Navigate(input.Window, input.URL)
if err != nil {
return nil, WebviewNavigateOutput{}, fmt.Errorf("failed to navigate: %w", err)
}
return nil, WebviewNavigateOutput{Success: true}, nil
}
func (s *Service) webviewSource(ctx context.Context, req *mcp.CallToolRequest, input WebviewSourceInput) (*mcp.CallToolResult, WebviewSourceOutput, error) {
if s.webview == nil {
return nil, WebviewSourceOutput{}, fmt.Errorf("WebView not available (MCP server running standalone)")
}
html, err := s.webview.GetPageSource(input.Window)
if err != nil {
return nil, WebviewSourceOutput{}, fmt.Errorf("failed to get source: %w", err)
}
return nil, WebviewSourceOutput{
HTML: html,
Length: len(html),
}, nil
}
// Window/Display management types
// WindowListInput is empty.
type WindowListInput struct{}
// WindowListOutput contains the list of windows with positions.
type WindowListOutput struct {
Windows []WindowInfo `json:"windows"`
}
// WindowInfo contains detailed window information.
type WindowInfo struct {
Name string `json:"name"`
X int `json:"x"`
Y int `json:"y"`
Width int `json:"width"`
Height int `json:"height"`
Maximized bool `json:"maximized"`
}
// WindowGetInput contains the window name to get.
type WindowGetInput struct {
// Window name to get info for.
Name string `json:"name"`
}
// WindowGetOutput contains the window information.
type WindowGetOutput struct {
Window *WindowInfo `json:"window"`
}
// WindowPositionInput contains parameters for moving a window.
type WindowPositionInput struct {
// Window name to move.
Name string `json:"name"`
// X coordinate (pixels from left edge of screen).
X int `json:"x"`
// Y coordinate (pixels from top edge of screen).
Y int `json:"y"`
}
// WindowPositionOutput contains the result of moving a window.
type WindowPositionOutput struct {
Success bool `json:"success"`
Name string `json:"name"`
X int `json:"x"`
Y int `json:"y"`
}
// WindowSizeInput contains parameters for resizing a window.
type WindowSizeInput struct {
// Window name to resize.
Name string `json:"name"`
// Width in pixels.
Width int `json:"width"`
// Height in pixels.
Height int `json:"height"`
}
// WindowSizeOutput contains the result of resizing a window.
type WindowSizeOutput struct {
Success bool `json:"success"`
Name string `json:"name"`
Width int `json:"width"`
Height int `json:"height"`
}
// WindowBoundsInput contains parameters for setting window bounds.
type WindowBoundsInput struct {
// Window name to modify.
Name string `json:"name"`
// X coordinate.
X int `json:"x"`
// Y coordinate.
Y int `json:"y"`
// Width in pixels.
Width int `json:"width"`
// Height in pixels.
Height int `json:"height"`
}
// WindowBoundsOutput contains the result of setting window bounds.
type WindowBoundsOutput struct {
Success bool `json:"success"`
Name string `json:"name"`
X int `json:"x"`
Y int `json:"y"`
Width int `json:"width"`
Height int `json:"height"`
}
// WindowNameInput contains just a window name.
type WindowNameInput struct {
// Window name to operate on.
Name string `json:"name"`
}
// WindowActionOutput contains the result of a window action.
type WindowActionOutput struct {
Success bool `json:"success"`
Name string `json:"name"`
Action string `json:"action"`
}
// ScreenListInput is empty.
type ScreenListInput struct{}
// ScreenListOutput contains the list of screens.
type ScreenListOutput struct {
Screens []ScreenInfo `json:"screens"`
}
// ScreenInfo contains screen/monitor information.
type ScreenInfo struct {
ID string `json:"id"`
Name string `json:"name"`
X int `json:"x"`
Y int `json:"y"`
Width int `json:"width"`
Height int `json:"height"`
Primary bool `json:"primary"`
}
// Window/Display handlers
func (s *Service) windowList(ctx context.Context, req *mcp.CallToolRequest, input WindowListInput) (*mcp.CallToolResult, WindowListOutput, error) {
if s.display == nil {
return nil, WindowListOutput{}, fmt.Errorf("display service not available (MCP server running standalone)")
}
windows := s.display.ListWindowInfos()
result := make([]WindowInfo, len(windows))
for i, w := range windows {
result[i] = WindowInfo{
Name: w.Name,
X: w.X,
Y: w.Y,
Width: w.Width,
Height: w.Height,
Maximized: w.Maximized,
}
}
return nil, WindowListOutput{Windows: result}, nil
}
func (s *Service) windowGet(ctx context.Context, req *mcp.CallToolRequest, input WindowGetInput) (*mcp.CallToolResult, WindowGetOutput, error) {
if s.display == nil {
return nil, WindowGetOutput{}, fmt.Errorf("display service not available (MCP server running standalone)")
}
info, err := s.display.GetWindowInfo(input.Name)
if err != nil {
return nil, WindowGetOutput{}, fmt.Errorf("failed to get window info: %w", err)
}
return nil, WindowGetOutput{
Window: &WindowInfo{
Name: info.Name,
X: info.X,
Y: info.Y,
Width: info.Width,
Height: info.Height,
Maximized: info.Maximized,
},
}, nil
}
func (s *Service) windowPosition(ctx context.Context, req *mcp.CallToolRequest, input WindowPositionInput) (*mcp.CallToolResult, WindowPositionOutput, error) {
if s.display == nil {
return nil, WindowPositionOutput{}, fmt.Errorf("display service not available (MCP server running standalone)")
}
err := s.display.SetWindowPosition(input.Name, input.X, input.Y)
if err != nil {
return nil, WindowPositionOutput{}, fmt.Errorf("failed to move window: %w", err)
}
return nil, WindowPositionOutput{
Success: true,
Name: input.Name,
X: input.X,
Y: input.Y,
}, nil
}
func (s *Service) windowSize(ctx context.Context, req *mcp.CallToolRequest, input WindowSizeInput) (*mcp.CallToolResult, WindowSizeOutput, error) {
if s.display == nil {
return nil, WindowSizeOutput{}, fmt.Errorf("display service not available (MCP server running standalone)")
}
err := s.display.SetWindowSize(input.Name, input.Width, input.Height)
if err != nil {
return nil, WindowSizeOutput{}, fmt.Errorf("failed to resize window: %w", err)
}
return nil, WindowSizeOutput{
Success: true,
Name: input.Name,
Width: input.Width,
Height: input.Height,
}, nil
}
func (s *Service) windowBounds(ctx context.Context, req *mcp.CallToolRequest, input WindowBoundsInput) (*mcp.CallToolResult, WindowBoundsOutput, error) {
if s.display == nil {
return nil, WindowBoundsOutput{}, fmt.Errorf("display service not available (MCP server running standalone)")
}
err := s.display.SetWindowBounds(input.Name, input.X, input.Y, input.Width, input.Height)
if err != nil {
return nil, WindowBoundsOutput{}, fmt.Errorf("failed to set window bounds: %w", err)
}
return nil, WindowBoundsOutput{
Success: true,
Name: input.Name,
X: input.X,
Y: input.Y,
Width: input.Width,
Height: input.Height,
}, nil
}
func (s *Service) windowMaximize(ctx context.Context, req *mcp.CallToolRequest, input WindowNameInput) (*mcp.CallToolResult, WindowActionOutput, error) {
if s.display == nil {
return nil, WindowActionOutput{}, fmt.Errorf("display service not available (MCP server running standalone)")
}
err := s.display.MaximizeWindow(input.Name)
if err != nil {
return nil, WindowActionOutput{}, fmt.Errorf("failed to maximize window: %w", err)
}
return nil, WindowActionOutput{
Success: true,
Name: input.Name,
Action: "maximize",
}, nil
}
func (s *Service) windowMinimize(ctx context.Context, req *mcp.CallToolRequest, input WindowNameInput) (*mcp.CallToolResult, WindowActionOutput, error) {
if s.display == nil {
return nil, WindowActionOutput{}, fmt.Errorf("display service not available (MCP server running standalone)")
}
err := s.display.MinimizeWindow(input.Name)
if err != nil {
return nil, WindowActionOutput{}, fmt.Errorf("failed to minimize window: %w", err)
}
return nil, WindowActionOutput{
Success: true,
Name: input.Name,
Action: "minimize",
}, nil
}
func (s *Service) windowRestore(ctx context.Context, req *mcp.CallToolRequest, input WindowNameInput) (*mcp.CallToolResult, WindowActionOutput, error) {
if s.display == nil {
return nil, WindowActionOutput{}, fmt.Errorf("display service not available (MCP server running standalone)")
}
err := s.display.RestoreWindow(input.Name)
if err != nil {
return nil, WindowActionOutput{}, fmt.Errorf("failed to restore window: %w", err)
}
return nil, WindowActionOutput{
Success: true,
Name: input.Name,
Action: "restore",
}, nil
}
func (s *Service) windowFocus(ctx context.Context, req *mcp.CallToolRequest, input WindowNameInput) (*mcp.CallToolResult, WindowActionOutput, error) {
if s.display == nil {
return nil, WindowActionOutput{}, fmt.Errorf("display service not available (MCP server running standalone)")
}
err := s.display.FocusWindow(input.Name)
if err != nil {
return nil, WindowActionOutput{}, fmt.Errorf("failed to focus window: %w", err)
}
return nil, WindowActionOutput{
Success: true,
Name: input.Name,
Action: "focus",
}, nil
}
func (s *Service) screenList(ctx context.Context, req *mcp.CallToolRequest, input ScreenListInput) (*mcp.CallToolResult, ScreenListOutput, error) {
if s.display == nil {
return nil, ScreenListOutput{}, fmt.Errorf("display service not available (MCP server running standalone)")
}
screens := s.display.GetScreens()
result := make([]ScreenInfo, len(screens))
for i, sc := range screens {
result[i] = ScreenInfo{
ID: sc.ID,
Name: sc.Name,
X: sc.X,
Y: sc.Y,
Width: sc.Width,
Height: sc.Height,
Primary: sc.Primary,
}
}
return nil, ScreenListOutput{Screens: result}, nil
}