From efd36ffbb7c8fe9dca9d9edeeba076201f55dee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Thu, 22 Jan 2026 02:27:14 +0000 Subject: [PATCH] feat: http logging middleware + mcp random port (#74) --- apps/backend/cmd/kandev/main.go | 2 + .../agentctl/server/api/control_server.go | 3 ++ .../internal/agentctl/server/api/server.go | 3 ++ apps/backend/internal/common/config/config.go | 2 + .../backend/internal/common/httpmw/logging.go | 38 +++++++++++++++++++ apps/backend/internal/mcpserver/server.go | 4 +- apps/cli/src/constants.ts | 1 + apps/cli/src/dev.ts | 8 ++++ apps/web/app/layout.tsx | 8 ++++ .../[agentId]/profile-mcp-config-card.tsx | 27 +++++++++---- apps/web/global.d.ts | 1 + apps/web/lib/config.ts | 9 ++++- 12 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 apps/backend/internal/common/httpmw/logging.go diff --git a/apps/backend/cmd/kandev/main.go b/apps/backend/cmd/kandev/main.go index ae209da8f2..a606825fad 100644 --- a/apps/backend/cmd/kandev/main.go +++ b/apps/backend/cmd/kandev/main.go @@ -14,6 +14,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/kandev/kandev/internal/common/httpmw" "go.uber.org/zap" // Common packages @@ -648,6 +649,7 @@ func main() { gin.SetMode(gin.ReleaseMode) } router := gin.New() + router.Use(httpmw.RequestLogger(log, "kandev")) router.Use(gin.Recovery()) router.Use(corsMiddleware()) diff --git a/apps/backend/internal/agentctl/server/api/control_server.go b/apps/backend/internal/agentctl/server/api/control_server.go index c352be2be2..a5ffe3d41c 100644 --- a/apps/backend/internal/agentctl/server/api/control_server.go +++ b/apps/backend/internal/agentctl/server/api/control_server.go @@ -8,6 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/kandev/kandev/internal/agentctl/server/config" "github.com/kandev/kandev/internal/agentctl/server/instance" + "github.com/kandev/kandev/internal/common/httpmw" "github.com/kandev/kandev/internal/common/logger" "go.uber.org/zap" ) @@ -33,6 +34,8 @@ func NewControlServer(cfg *config.Config, instMgr *instance.Manager, log *logger router: gin.New(), } + cs.router.Use(httpmw.RequestLogger(cs.logger, "agentctl-control")) + cs.setupRoutes() return cs } diff --git a/apps/backend/internal/agentctl/server/api/server.go b/apps/backend/internal/agentctl/server/api/server.go index 4f44cf4214..ce15ba7825 100644 --- a/apps/backend/internal/agentctl/server/api/server.go +++ b/apps/backend/internal/agentctl/server/api/server.go @@ -9,6 +9,7 @@ import ( "github.com/gorilla/websocket" "github.com/kandev/kandev/internal/agentctl/server/config" "github.com/kandev/kandev/internal/agentctl/server/process" + "github.com/kandev/kandev/internal/common/httpmw" "github.com/kandev/kandev/internal/common/logger" "go.uber.org/zap" ) @@ -39,6 +40,8 @@ func NewServer(cfg *config.InstanceConfig, procMgr *process.Manager, log *logger }, } + s.router.Use(httpmw.RequestLogger(s.logger, "agentctl-instance")) + s.setupRoutes() return s } diff --git a/apps/backend/internal/common/config/config.go b/apps/backend/internal/common/config/config.go index 7077193c5e..c7b28c1aa6 100644 --- a/apps/backend/internal/common/config/config.go +++ b/apps/backend/internal/common/config/config.go @@ -212,6 +212,8 @@ func LoadWithPath(configPath string) (*Config, error) { // so we explicitly bind keys where env var naming differs from config key naming. _ = v.BindEnv("agent.standalonePort", "AGENTCTL_PORT", "KANDEV_AGENT_STANDALONE_PORT") _ = v.BindEnv("agent.standaloneHost", "KANDEV_AGENT_STANDALONE_HOST") + _ = v.BindEnv("agent.mcpServerPort", "KANDEV_AGENT_MCP_SERVER_PORT") + _ = v.BindEnv("agent.mcpServerUrl", "KANDEV_AGENT_MCP_SERVER_URL") // Configure config file v.SetConfigName("config") diff --git a/apps/backend/internal/common/httpmw/logging.go b/apps/backend/internal/common/httpmw/logging.go new file mode 100644 index 0000000000..2316fe7b76 --- /dev/null +++ b/apps/backend/internal/common/httpmw/logging.go @@ -0,0 +1,38 @@ +package httpmw + +import ( + "time" + + "github.com/gin-gonic/gin" + "github.com/kandev/kandev/internal/common/logger" + "go.uber.org/zap" +) + +// RequestLogger logs HTTP request details after the handler completes. +func RequestLogger(log *logger.Logger, serverName string) gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.FullPath() + if path == "" { + path = c.Request.URL.Path + } + + c.Next() + + latency := time.Since(start) + status := c.Writer.Status() + size := c.Writer.Size() + if size < 0 { + size = 0 + } + + log.Info("http", + zap.String("server", serverName), + zap.String("method", c.Request.Method), + zap.String("path", path), + zap.Int("status", status), + zap.Int64("duration_ms", latency.Milliseconds()), + zap.Int("bytes", size), + ) + } +} diff --git a/apps/backend/internal/mcpserver/server.go b/apps/backend/internal/mcpserver/server.go index 5126bd5dca..9b06ec2895 100644 --- a/apps/backend/internal/mcpserver/server.go +++ b/apps/backend/internal/mcpserver/server.go @@ -88,6 +88,9 @@ func (s *Server) Start(ctx context.Context) error { if err != nil { return fmt.Errorf("failed to listen on %s: %w", addr, err) } + if tcpAddr, ok := listener.Addr().(*net.TCPAddr); ok { + s.cfg.Port = tcpAddr.Port + } // Create HTTP server with the mux s.httpServer = &http.Server{ @@ -174,4 +177,3 @@ func (s *Server) SSEEndpoint() string { func (s *Server) StreamableHTTPEndpoint() string { return fmt.Sprintf("http://localhost:%d/mcp", s.cfg.Port) } - diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index ad72f9af6f..cb26eed2c0 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -5,6 +5,7 @@ import path from "node:path"; export const DEFAULT_BACKEND_PORT = 8080; export const DEFAULT_WEB_PORT = 3000; export const DEFAULT_AGENTCTL_PORT = 9999; +export const DEFAULT_MCP_PORT = 9090; // Random fallback range for port selection. export const RANDOM_PORT_MIN = 10000; diff --git a/apps/cli/src/dev.ts b/apps/cli/src/dev.ts index 0fda42645c..6f943ba908 100644 --- a/apps/cli/src/dev.ts +++ b/apps/cli/src/dev.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { DEFAULT_AGENTCTL_PORT, DEFAULT_BACKEND_PORT, + DEFAULT_MCP_PORT, DEFAULT_WEB_PORT, HEALTH_TIMEOUT_MS, } from "./constants"; @@ -22,17 +23,22 @@ export async function runDev({ repoRoot, backendPort, webPort }: DevOptions): Pr const actualBackendPort = backendPort ?? (await pickAvailablePort(DEFAULT_BACKEND_PORT)); const actualWebPort = webPort ?? (await pickAvailablePort(DEFAULT_WEB_PORT)); const agentctlPort = await pickAvailablePort(DEFAULT_AGENTCTL_PORT); + const mcpPort = await pickAvailablePort(DEFAULT_MCP_PORT); const backendUrl = `http://localhost:${actualBackendPort}`; + const mcpUrl = `http://localhost:${mcpPort}/sse`; const backendEnv = { ...process.env, KANDEV_SERVER_PORT: String(actualBackendPort), KANDEV_AGENT_STANDALONE_PORT: String(agentctlPort), + KANDEV_AGENT_MCP_SERVER_PORT: String(mcpPort), }; const webEnv = { ...process.env, KANDEV_API_BASE_URL: backendUrl, NEXT_PUBLIC_KANDEV_API_BASE_URL: backendUrl, + KANDEV_MCP_SERVER_URL: mcpUrl, + NEXT_PUBLIC_KANDEV_MCP_SERVER_URL: mcpUrl, PORT: String(actualWebPort), NEXT_PUBLIC_KANDEV_DEBUG: "true", }; @@ -41,6 +47,8 @@ export async function runDev({ repoRoot, backendPort, webPort }: DevOptions): Pr console.log("[kandev] backend port:", actualBackendPort); console.log("[kandev] web port:", actualWebPort); console.log("[kandev] agentctl port:", agentctlPort); + console.log("[kandev] mcp port:", mcpPort); + console.log("[kandev] mcp url:", mcpUrl); const supervisor = createProcessSupervisor(); supervisor.attachSignalHandlers(); diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 06f90fcc53..1b2f928adc 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -17,6 +17,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { const runtimeApiBaseUrl = process.env.KANDEV_API_BASE_URL ?? ""; + const runtimeMcpServerUrl = process.env.KANDEV_MCP_SERVER_URL ?? ""; return ( @@ -28,6 +29,13 @@ export default function RootLayout({ }} /> ) : null} + {runtimeMcpServerUrl ? ( +