Files
MuyueWorkspace/internal/mcpserver/server.go
Augustin 4523bbd42c
All checks were successful
Stable Release / stable (push) Successful in 1m34s
feat: RAG, memory, plugins, lessons, file editor, split panes, Markdown rendering, PWA + UI overhaul
Major additions:
- RAG pipeline (indexing, chunking, search) with sidebar upload button
- Memory system with CRUD API
- Plugins and lessons modules
- MCP discovery and MCP server
- Advanced skills (auto-create, conditional, improver)
- Agent browser/image support, delegate, sessions
- File editor with CodeMirror in split panes
- Markdown rendering via react-markdown + KaTeX + highlight.js
- Raw markdown toggle
- PWA manifest + service worker
- Extension UI redesign with new design tokens and studio-style chat
- Pipeline API for chat streaming
- Mobile responsive layout

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-27 21:04:11 +02:00

557 lines
15 KiB
Go

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, &params); 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, &params); 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, &params); 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, &params); 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, &params); 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, &params); 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)
}