package mcpserver import ( "encoding/json" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "runtime" "strings" "sync" "time" ) type Tool struct { Name string `json:"name"` Description string `json:"description"` InputSchema map[string]interface{} `json:"inputSchema"` } type ToolCall struct { Name string `json:"name"` Args json.RawMessage `json:"arguments"` } type ToolResult struct { Content []ContentBlock `json:"content"` IsError bool `json:"isError,omitempty"` } type ContentBlock struct { Type string `json:"type"` Text string `json:"text,omitempty"` } type JSONRPCRequest struct { JSONRPC string `json:"jsonrpc"` ID json.RawMessage `json:"id,omitempty"` Method string `json:"method"` Params json.RawMessage `json:"params,omitempty"` } type JSONRPCResponse struct { JSONRPC string `json:"jsonrpc"` ID json.RawMessage `json:"id"` Result interface{} `json:"result,omitempty"` Error *RPCError `json:"error,omitempty"` } type RPCError struct { Code int `json:"code"` Message string `json:"message"` } var tools = []Tool{ { Name: "terminal_exec", Description: "Execute a command in the terminal and return the output", InputSchema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "command": map[string]interface{}{"type": "string", "description": "The command to execute"}, "cwd": map[string]interface{}{"type": "string", "description": "Working directory (optional)"}, "timeout": map[string]interface{}{"type": "integer", "description": "Timeout in seconds (default 30)"}, }, "required": []string{"command"}, }, }, { Name: "file_read", Description: "Read the contents of a file", InputSchema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "path": map[string]interface{}{"type": "string", "description": "Path to the file"}, "offset": map[string]interface{}{"type": "integer", "description": "Line offset to start reading from (0-based)"}, "limit": map[string]interface{}{"type": "integer", "description": "Maximum number of lines to read"}, }, "required": []string{"path"}, }, }, { Name: "file_write", Description: "Write content to a file, creating it if needed", InputSchema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "path": map[string]interface{}{"type": "string", "description": "Path to the file"}, "content": map[string]interface{}{"type": "string", "description": "Content to write"}, }, "required": []string{"path", "content"}, }, }, { Name: "search", Description: "Search for files by name pattern", InputSchema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "path": map[string]interface{}{"type": "string", "description": "Directory to search in"}, "pattern": map[string]interface{}{"type": "string", "description": "Glob pattern to match filenames"}, }, "required": []string{"path", "pattern"}, }, }, { Name: "grep", Description: "Search file contents for a pattern", InputSchema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "path": map[string]interface{}{"type": "string", "description": "Directory to search in"}, "pattern": map[string]interface{}{"type": "string", "description": "Text or regex pattern to search for"}, }, "required": []string{"path", "pattern"}, }, }, { Name: "system_info", Description: "Get system information (OS, CPU, memory, disk)", InputSchema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{}, }, }, } type MCPServer struct { port int server *http.Server mu sync.Mutex sseClients map[string]chan SSEEvent sseClientsMu sync.Mutex } type SSEEvent struct { Event string Data string } func New(port int) *MCPServer { return &MCPServer{ port: port, sseClients: make(map[string]chan SSEEvent), } } func (m *MCPServer) Start() error { mux := http.NewServeMux() mux.HandleFunc("/", m.handleSSE) mux.HandleFunc("/message", m.handleHTTPMessage) mux.HandleFunc("/mcp", m.handleStreamableHTTP) m.server = &http.Server{ Addr: fmt.Sprintf("127.0.0.1:%d", m.port), Handler: mux, } go func() { if err := m.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { fmt.Printf("[MCP Server] Error: %v\n", err) } }() return nil } func (m *MCPServer) Stop() error { if m.server != nil { return m.server.Close() } return nil } func (m *MCPServer) Port() int { return m.port } func (m *MCPServer) handleSSE(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("Access-Control-Allow-Origin", "*") clientID := fmt.Sprintf("%d", time.Now().UnixNano()) ch := make(chan SSEEvent, 32) m.sseClientsMu.Lock() m.sseClients[clientID] = ch m.sseClientsMu.Unlock() defer func() { m.sseClientsMu.Lock() delete(m.sseClients, clientID) m.sseClientsMu.Unlock() close(ch) }() fmt.Fprintf(w, "event: endpoint\ndata: /message?clientId=%s\n\n", clientID) if f, ok := w.(http.Flusher); ok { f.Flush() } for { select { case evt, ok := <-ch: if !ok { return } fmt.Fprintf(w, "event: %s\ndata: %s\n\n", evt.Event, evt.Data) if f, ok := w.(http.Flusher); ok { f.Flush() } case <-r.Context().Done(): return } } } func (m *MCPServer) handleHTTPMessage(w http.ResponseWriter, r *http.Request) { if r.Method == "OPTIONS" { 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.WriteHeader(http.StatusOK) return } if r.Method != "POST" { http.Error(w, "POST only", http.StatusMethodNotAllowed) return } w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") body, err := io.ReadAll(r.Body) if err != nil { m.writeRPCError(w, nil, -32700, "Parse error") return } resp := m.handleJSONRPC(body) json.NewEncoder(w).Encode(resp) } func (m *MCPServer) handleStreamableHTTP(w http.ResponseWriter, r *http.Request) { if r.Method == "OPTIONS" { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") w.WriteHeader(http.StatusOK) return } if r.Method == "GET" { m.handleSSE(w, r) return } if r.Method != "POST" { http.Error(w, "POST only", http.StatusMethodNotAllowed) return } w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") body, err := io.ReadAll(r.Body) if err != nil { m.writeRPCError(w, nil, -32700, "Parse error") return } resp := m.handleJSONRPC(body) json.NewEncoder(w).Encode(resp) } func (m *MCPServer) handleJSONRPC(body []byte) JSONRPCResponse { var req JSONRPCRequest if err := json.Unmarshal(body, &req); err != nil { return JSONRPCResponse{ JSONRPC: "2.0", Error: &RPCError{Code: -32700, Message: "Parse error"}, } } switch req.Method { case "initialize": return JSONRPCResponse{ JSONRPC: "2.0", ID: req.ID, Result: map[string]interface{}{ "protocolVersion": "2024-11-05", "capabilities": map[string]interface{}{ "tools": map[string]interface{}{}, }, "serverInfo": map[string]interface{}{ "name": "muyue", "version": "0.9.0", }, }, } case "notifications/initialized": return JSONRPCResponse{JSONRPC: "2.0", ID: req.ID} case "tools/list": return JSONRPCResponse{ JSONRPC: "2.0", ID: req.ID, Result: map[string]interface{}{ "tools": tools, }, } case "tools/call": var params struct { Name string `json:"name"` Arguments json.RawMessage `json:"arguments"` } if err := json.Unmarshal(req.Params, ¶ms); err != nil { return JSONRPCResponse{ JSONRPC: "2.0", ID: req.ID, Error: &RPCError{Code: -32602, Message: "Invalid params"}, } } result := m.executeTool(params.Name, params.Arguments) return JSONRPCResponse{ JSONRPC: "2.0", ID: req.ID, Result: result, } default: return JSONRPCResponse{ JSONRPC: "2.0", ID: req.ID, Error: &RPCError{Code: -32601, Message: fmt.Sprintf("Method not found: %s", req.Method)}, } } } func (m *MCPServer) executeTool(name string, args json.RawMessage) ToolResult { switch name { case "terminal_exec": return m.toolTerminalExec(args) case "file_read": return m.toolFileRead(args) case "file_write": return m.toolFileWrite(args) case "search": return m.toolSearch(args) case "grep": return m.toolGrep(args) case "system_info": return m.toolSystemInfo() default: return ToolResult{ Content: []ContentBlock{{Type: "text", Text: fmt.Sprintf("Unknown tool: %s", name)}}, IsError: true, } } } func (m *MCPServer) toolTerminalExec(args json.RawMessage) ToolResult { var params struct { Command string `json:"command"` Cwd string `json:"cwd"` Timeout int `json:"timeout"` } if err := json.Unmarshal(args, ¶ms); err != nil { return ToolResult{Content: []ContentBlock{{Type: "text", Text: err.Error()}}, IsError: true} } timeout := params.Timeout if timeout <= 0 { timeout = 30 } ctx := exec.Command("sh", "-c", params.Command) if params.Cwd != "" { ctx.Dir = params.Cwd } var stdout, stderr strings.Builder ctx.Stdout = &stdout ctx.Stderr = &stderr done := make(chan error, 1) go func() { done <- ctx.Run() }() select { case err := <-done: output := stdout.String() if errMsg := stderr.String(); errMsg != "" { output += "\n" + errMsg } if err != nil { output += fmt.Sprintf("\nExit error: %v", err) } return ToolResult{Content: []ContentBlock{{Type: "text", Text: output}}} case <-time.After(time.Duration(timeout) * time.Second): ctx.Process.Kill() return ToolResult{ Content: []ContentBlock{{Type: "text", Text: fmt.Sprintf("Command timed out after %ds\n%s%s", timeout, stdout.String(), stderr.String())}}, IsError: true, } } } func (m *MCPServer) toolFileRead(args json.RawMessage) ToolResult { var params struct { Path string `json:"path"` Offset int `json:"offset"` Limit int `json:"limit"` } if err := json.Unmarshal(args, ¶ms); err != nil { return ToolResult{Content: []ContentBlock{{Type: "text", Text: err.Error()}}, IsError: true} } home, _ := os.UserHomeDir() path := strings.ReplaceAll(params.Path, "~", home) data, err := os.ReadFile(path) if err != nil { return ToolResult{Content: []ContentBlock{{Type: "text", Text: fmt.Sprintf("Error reading file: %v", err)}}, IsError: true} } lines := strings.Split(string(data), "\n") start := params.Offset if start < 0 { start = 0 } end := len(lines) if params.Limit > 0 && start+params.Limit < end { end = start + params.Limit } if start > len(lines) { start = len(lines) } return ToolResult{Content: []ContentBlock{{Type: "text", Text: strings.Join(lines[start:end], "\n")}}} } func (m *MCPServer) toolFileWrite(args json.RawMessage) ToolResult { var params struct { Path string `json:"path"` Content string `json:"content"` } if err := json.Unmarshal(args, ¶ms); err != nil { return ToolResult{Content: []ContentBlock{{Type: "text", Text: err.Error()}}, IsError: true} } home, _ := os.UserHomeDir() path := strings.ReplaceAll(params.Path, "~", home) dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return ToolResult{Content: []ContentBlock{{Type: "text", Text: fmt.Sprintf("Error creating directory: %v", err)}}, IsError: true} } if err := os.WriteFile(path, []byte(params.Content), 0644); err != nil { return ToolResult{Content: []ContentBlock{{Type: "text", Text: fmt.Sprintf("Error writing file: %v", err)}}, IsError: true} } return ToolResult{Content: []ContentBlock{{Type: "text", Text: fmt.Sprintf("Successfully wrote %d bytes to %s", len(params.Content), path)}}} } func (m *MCPServer) toolSearch(args json.RawMessage) ToolResult { var params struct { Path string `json:"path"` Pattern string `json:"pattern"` } if err := json.Unmarshal(args, ¶ms); err != nil { return ToolResult{Content: []ContentBlock{{Type: "text", Text: err.Error()}}, IsError: true} } home, _ := os.UserHomeDir() basePath := strings.ReplaceAll(params.Path, "~", home) cmd := exec.Command("find", basePath, "-name", params.Pattern, "-type", "f", "-not", "-path", "*/node_modules/*", "-not", "-path", "*/.git/*") output, err := cmd.CombinedOutput() if err != nil { return ToolResult{Content: []ContentBlock{{Type: "text", Text: string(output)}}} } lines := strings.Split(strings.TrimSpace(string(output)), "\n") if len(lines) > 100 { lines = lines[:100] } return ToolResult{Content: []ContentBlock{{Type: "text", Text: strings.Join(lines, "\n")}}} } func (m *MCPServer) toolGrep(args json.RawMessage) ToolResult { var params struct { Path string `json:"path"` Pattern string `json:"pattern"` } if err := json.Unmarshal(args, ¶ms); err != nil { return ToolResult{Content: []ContentBlock{{Type: "text", Text: err.Error()}}, IsError: true} } home, _ := os.UserHomeDir() basePath := strings.ReplaceAll(params.Path, "~", home) cmd := exec.Command("grep", "-rn", "--include=*", params.Pattern, basePath) output, _ := cmd.CombinedOutput() lines := strings.Split(strings.TrimSpace(string(output)), "\n") if len(lines) > 50 { lines = lines[:50] lines = append(lines, fmt.Sprintf("... (%d more results truncated)", len(strings.Split(string(output), "\n"))-50)) } return ToolResult{Content: []ContentBlock{{Type: "text", Text: strings.Join(lines, "\n")}}} } func (m *MCPServer) toolSystemInfo() ToolResult { var info strings.Builder info.WriteString(fmt.Sprintf("OS: %s/%s\n", runtime.GOOS, runtime.GOARCH)) info.WriteString(fmt.Sprintf("CPUs: %d\n", runtime.NumCPU())) if out, err := exec.Command("uname", "-a").Output(); err == nil { info.WriteString(fmt.Sprintf("Kernel: %s", string(out))) } if out, err := exec.Command("free", "-h").Output(); err == nil { info.WriteString(fmt.Sprintf("Memory:\n%s", string(out))) } if out, err := exec.Command("df", "-h", "/").Output(); err == nil { info.WriteString(fmt.Sprintf("Disk:\n%s", string(out))) } if out, err := exec.Command("uptime").Output(); err == nil { info.WriteString(fmt.Sprintf("Uptime: %s", string(out))) } return ToolResult{Content: []ContentBlock{{Type: "text", Text: info.String()}}} } func (m *MCPServer) writeRPCError(w http.ResponseWriter, id json.RawMessage, code int, msg string) { resp := JSONRPCResponse{ JSONRPC: "2.0", ID: id, Error: &RPCError{Code: code, Message: msg}, } json.NewEncoder(w).Encode(resp) }