cli/pkg/mcp/transport_tcp.go
Snider 5dd581c3bf Log all errors at handling point with contextual information (#321)
* feat(log): log all errors at handling point with context

This change ensures all errors are logged at the point where they are
handled, including contextual information such as operations and
logical stack traces.

Key changes:
- Added `StackTrace` and `FormatStackTrace` to `pkg/log/errors.go`.
- Enhanced `Logger.log` in `pkg/log/log.go` to automatically extract
  and log `op` and `stack` keys when an error is passed in keyvals.
- Updated CLI logging and output helpers to support structured logging.
- Updated CLI fatal error handlers to log errors before exiting.
- Audited and updated error logging in MCP service (tool handlers and
  TCP transport), CLI background services (signal and health), and
  Agentic task handlers.

* feat(log): log all errors at handling point with context

This change ensures all errors are logged at the point where they are
handled, including contextual information such as operations and
logical stack traces.

Key changes:
- Added `StackTrace` and `FormatStackTrace` to `pkg/log/errors.go`.
- Enhanced `Logger.log` in `pkg/log/log.go` to automatically extract
  and log `op` and `stack` keys when an error is passed in keyvals.
- Updated CLI logging and output helpers to support structured logging.
- Updated CLI fatal error handlers to log errors before exiting.
- Audited and updated error logging in MCP service (tool handlers and
  TCP transport), CLI background services (signal and health), and
  Agentic task handlers.
- Fixed formatting in `pkg/mcp/mcp.go` and `pkg/io/local/client.go`.
- Removed unused `fmt` import in `pkg/cli/runtime.go`.

* feat(log): log all errors at handling point with context

This change ensures all errors are logged at the point where they are
handled, including contextual information such as operations and
logical stack traces.

Key changes:
- Added `StackTrace` and `FormatStackTrace` to `pkg/log/errors.go`.
- Enhanced `Logger.log` in `pkg/log/log.go` to automatically extract
  and log `op` and `stack` keys when an error is passed in keyvals.
- Updated CLI logging and output helpers to support structured logging.
- Updated CLI fatal error handlers to log errors before exiting.
- Audited and updated error logging in MCP service (tool handlers and
  TCP transport), CLI background services (signal and health), and
  Agentic task handlers.
- Fixed formatting in `pkg/mcp/mcp.go` and `pkg/io/local/client.go`.
- Removed unused `fmt` import in `pkg/cli/runtime.go`.
- Fixed CI failure in `auto-merge` workflow by providing explicit
  repository context to the GitHub CLI.

* feat(log): address PR feedback and improve error context extraction

Addressed feedback from PR review:
- Improved `Fatalf` and other fatal functions in `pkg/cli/errors.go` to
  use structured logging for the formatted message.
- Added direct unit tests for `StackTrace` and `FormatStackTrace` in
  `pkg/log/errors_test.go`, covering edge cases like plain errors,
  nil errors, and mixed error chains.
- Optimized the automatic context extraction loop in `pkg/log/log.go`
  by capturing the original length of keyvals.
- Fixed a bug in `StackTrace` where operations were duplicated when
  the error chain included non-`*log.Err` errors.
- Fixed formatting and unused imports from previous commits.

* fix: address code review comments

- Simplify Fatalf logging by removing redundant format parameter
  (the formatted message is already logged as "msg")
- Tests for StackTrace/FormatStackTrace edge cases already exist
- Loop optimization in pkg/log/log.go already implemented

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <developers@lethean.io>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 07:52:25 +00:00

140 lines
3.3 KiB
Go

package mcp
import (
"bufio"
"context"
"io"
"net"
"github.com/host-uk/core/pkg/log"
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// maxMCPMessageSize is the maximum size for MCP JSON-RPC messages (10 MB).
const maxMCPMessageSize = 10 * 1024 * 1024
// 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 func() { _ = t.listener.Close() }()
// Close listener when context is cancelled to unblock Accept
go func() {
<-ctx.Done()
_ = t.listener.Close()
}()
if addr == "" {
addr = t.listener.Addr().String()
}
log.Info("MCP TCP server listening", "addr", addr)
for {
conn, err := t.listener.Accept()
if err != nil {
select {
case <-ctx.Done():
return nil
default:
log.Error("mcp: accept error", "err", 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 {
log.Error("mcp: connection error", "err", err, "remote", conn.RemoteAddr())
}
}
// connTransport adapts net.Conn to mcp.Transport
type connTransport struct {
conn net.Conn
}
func (t *connTransport) Connect(ctx context.Context) (mcp.Connection, error) {
scanner := bufio.NewScanner(t.conn)
scanner.Buffer(make([]byte, 64*1024), maxMCPMessageSize)
return &connConnection{
conn: t.conn,
scanner: scanner,
}, 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 - connection closed cleanly
return nil, io.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
}