feat(mcp): Add TCP transport

Fixes #126
This commit is contained in:
Snider 2026-02-02 00:22:06 +00:00
parent 316e8a7385
commit 126c799723
3 changed files with 150 additions and 13 deletions

BIN
core-test Executable file

Binary file not shown.

View file

@ -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{})
}

131
pkg/mcp/transport_tcp.go Normal file
View file

@ -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
}