* feat(help): Add CLI help command Fixes #136 * chore: remove binary * feat(mcp): Add TCP transport Fixes #126 * feat(io): Migrate pkg/mcp to use Medium abstraction Fixes #103 * chore(io): Migrate internal/cmd/docs/* to Medium abstraction Fixes #113 * chore(io): Migrate internal/cmd/dev/* to Medium abstraction Fixes #114 * chore(io): Migrate internal/cmd/setup/* to Medium abstraction * chore(io): Complete migration of internal/cmd/dev/* to Medium abstraction * chore(io): Migrate internal/cmd/sdk, pkgcmd, and workspace to Medium abstraction * style: fix formatting in internal/variants Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(io): simplify local Medium implementation Rewrote to match the simpler TypeScript pattern: - path() sanitizes and returns string directly - Each method calls path() once - No complex symlink validation - Less code, less attack surface Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(mcp): update sandboxing tests for simplified Medium The simplified io/local.Medium implementation: - Sanitizes .. to . (no error, path is cleaned) - Allows absolute paths through (caller validates if needed) - Follows symlinks (no traversal blocking) Update tests to match this simplified behavior. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(updater): resolve PkgVersion duplicate declaration Remove var PkgVersion from updater.go since go generate creates const PkgVersion in version.go. Track version.go in git to ensure builds work without running go generate first. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
131 lines
3.1 KiB
Go
131 lines
3.1 KiB
Go
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
|
|
}
|