diff --git a/core-test b/core-test new file mode 100755 index 00000000..7a211f99 Binary files /dev/null and b/core-test differ diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index 2e4d7b5c..4f25fe64 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -82,61 +82,61 @@ func New(opts ...Option) (*Service, error) { } } - s.registerTools() + s.registerTools(s.server) return s, nil } // registerTools adds file operation tools to the MCP server. -func (s *Service) registerTools() { +func (s *Service) registerTools(server *mcp.Server) { // File operations - mcp.AddTool(s.server, &mcp.Tool{ + mcp.AddTool(server, &mcp.Tool{ Name: "file_read", Description: "Read the contents of a file", }, s.readFile) - mcp.AddTool(s.server, &mcp.Tool{ + mcp.AddTool(server, &mcp.Tool{ Name: "file_write", Description: "Write content to a file", }, s.writeFile) - mcp.AddTool(s.server, &mcp.Tool{ + mcp.AddTool(server, &mcp.Tool{ Name: "file_delete", Description: "Delete a file or empty directory", }, s.deleteFile) - mcp.AddTool(s.server, &mcp.Tool{ + mcp.AddTool(server, &mcp.Tool{ Name: "file_rename", Description: "Rename or move a file", }, s.renameFile) - mcp.AddTool(s.server, &mcp.Tool{ + mcp.AddTool(server, &mcp.Tool{ Name: "file_exists", Description: "Check if a file or directory exists", }, s.fileExists) - mcp.AddTool(s.server, &mcp.Tool{ + mcp.AddTool(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{ + mcp.AddTool(server, &mcp.Tool{ Name: "dir_list", Description: "List contents of a directory", }, s.listDirectory) - mcp.AddTool(s.server, &mcp.Tool{ + mcp.AddTool(server, &mcp.Tool{ Name: "dir_create", Description: "Create a new directory", }, s.createDirectory) // Language detection - mcp.AddTool(s.server, &mcp.Tool{ + mcp.AddTool(server, &mcp.Tool{ Name: "lang_detect", Description: "Detect the programming language of a file", }, s.detectLanguage) - mcp.AddTool(s.server, &mcp.Tool{ + mcp.AddTool(server, &mcp.Tool{ Name: "lang_list", Description: "Get list of supported programming languages", }, s.getSupportedLanguages) @@ -566,8 +566,14 @@ func detectLanguageFromPath(path string) string { } } -// Run starts the MCP server on stdio. +// Run starts the MCP server. +// If MCP_ADDR is set, it starts a TCP server. +// Otherwise, it starts a Stdio server. func (s *Service) Run(ctx context.Context) error { + addr := os.Getenv("MCP_ADDR") + if addr != "" { + return s.ServeTCP(ctx, addr) + } return s.server.Run(ctx, &mcp.StdioTransport{}) } diff --git a/pkg/mcp/transport_tcp.go b/pkg/mcp/transport_tcp.go new file mode 100644 index 00000000..f7b5f1e5 --- /dev/null +++ b/pkg/mcp/transport_tcp.go @@ -0,0 +1,131 @@ +package mcp + +import ( + "bufio" + "context" + "fmt" + "net" + "os" + + "github.com/modelcontextprotocol/go-sdk/jsonrpc" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// TCPTransport manages a TCP listener for MCP. +type TCPTransport struct { + addr string + listener net.Listener +} + +// NewTCPTransport creates a new TCP transport listener. +// It listens on the provided address (e.g. "localhost:9100"). +func NewTCPTransport(addr string) (*TCPTransport, error) { + listener, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + return &TCPTransport{addr: addr, listener: listener}, nil +} + +// ServeTCP starts a TCP server for the MCP service. +// It accepts connections and spawns a new MCP server session for each connection. +func (s *Service) ServeTCP(ctx context.Context, addr string) error { + t, err := NewTCPTransport(addr) + if err != nil { + return err + } + defer t.listener.Close() + + if addr == "" { + addr = t.listener.Addr().String() + } + fmt.Fprintf(os.Stderr, "MCP TCP server listening on %s\n", addr) + + for { + conn, err := t.listener.Accept() + if err != nil { + select { + case <-ctx.Done(): + return nil + default: + fmt.Fprintf(os.Stderr, "Accept error: %v\n", err) + continue + } + } + + go s.handleConnection(ctx, conn) + } +} + +func (s *Service) handleConnection(ctx context.Context, conn net.Conn) { + // Note: We don't defer conn.Close() here because it's closed by the Server/Transport + + // Create new server instance for this connection + impl := &mcp.Implementation{ + Name: "core-cli", + Version: "0.1.0", + } + server := mcp.NewServer(impl, nil) + s.registerTools(server) + + // Create transport for this connection + transport := &connTransport{conn: conn} + + // Run server (blocks until connection closed) + // Server.Run calls Connect, then Read loop. + if err := server.Run(ctx, transport); err != nil { + fmt.Fprintf(os.Stderr, "Connection error: %v\n", err) + } +} + +// connTransport adapts net.Conn to mcp.Transport +type connTransport struct { + conn net.Conn +} + +func (t *connTransport) Connect(ctx context.Context) (mcp.Connection, error) { + return &connConnection{ + conn: t.conn, + scanner: bufio.NewScanner(t.conn), + }, nil +} + +// connConnection implements mcp.Connection +type connConnection struct { + conn net.Conn + scanner *bufio.Scanner +} + +func (c *connConnection) Read(ctx context.Context) (jsonrpc.Message, error) { + // Blocks until line is read + if !c.scanner.Scan() { + if err := c.scanner.Err(); err != nil { + return nil, err + } + // EOF + // Return error to signal closure, as per Scanner contract? + // SDK usually expects error on close. + return nil, fmt.Errorf("EOF") + } + line := c.scanner.Bytes() + return jsonrpc.DecodeMessage(line) +} + +func (c *connConnection) Write(ctx context.Context, msg jsonrpc.Message) error { + data, err := jsonrpc.EncodeMessage(msg) + if err != nil { + return err + } + // Append newline for line-delimited JSON + data = append(data, '\n') + _, err = c.conn.Write(data) + return err +} + +func (c *connConnection) Close() error { + return c.conn.Close() +} + +func (c *connConnection) SessionID() string { + return "tcp-session" // Unique ID might be better, but optional +}