feat: http logging middleware + mcp random port (#74)

This commit is contained in:
Carlos Florêncio
2026-01-22 02:27:14 +00:00
committed by GitHub
parent 89d7f10211
commit efd36ffbb7
12 changed files with 95 additions and 11 deletions
+2
View File
@@ -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())
@@ -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
}
@@ -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
}
@@ -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")
@@ -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),
)
}
}
+3 -1
View File
@@ -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)
}
+1
View File
@@ -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;
+8
View File
@@ -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();
+8
View File
@@ -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 (
<html lang="en" suppressHydrationWarning>
<body className="antialiased font-sans">
@@ -28,6 +29,13 @@ export default function RootLayout({
}}
/>
) : null}
{runtimeMcpServerUrl ? (
<script
dangerouslySetInnerHTML={{
__html: `window.__KANDEV_MCP_SERVER_URL = ${JSON.stringify(runtimeMcpServerUrl)};`,
}}
/>
) : null}
<StateProvider>
<ThemeProvider>
<TooltipProvider>
@@ -4,7 +4,9 @@ import { Card, CardContent, CardHeader, CardTitle } from '@kandev/ui/card';
import { Label } from '@kandev/ui/label';
import { Switch } from '@kandev/ui/switch';
import { Textarea } from '@kandev/ui/textarea';
import { Tooltip, TooltipContent, TooltipTrigger } from '@kandev/ui/tooltip';
import { UnsavedChangesBadge, UnsavedSaveButton } from '@/components/settings/unsaved-indicator';
import { getBackendConfig } from '@/lib/config';
import { useProfileMcpConfig } from './use-profile-mcp-config';
import type { AgentProfileMcpConfig } from '@/lib/types/http';
@@ -62,11 +64,13 @@ export function ProfileMcpConfigCard({
const currentServers = isDraft ? draftState?.servers ?? '' : mcpServers;
const currentError = isDraft ? draftState?.error ?? null : mcpError;
const currentDirty = isDraft ? draftState?.dirty ?? false : mcpDirty;
const { mcpServerUrl } = getBackendConfig();
const kandevTools = 'Tools: list_workspaces, list_boards, list_columns, list_tasks, create_task, update_task';
const popularServers: Record<string, Record<string, unknown>> = {
kandev: {
type: 'sse',
url: 'http://localhost:9090/sse',
url: mcpServerUrl,
},
playwright: {
type: 'stdio',
@@ -202,13 +206,20 @@ export function ProfileMcpConfigCard({
MCP definitions are stored in the database and resolved per executor at runtime. This does not override your local agent config.</p>
<p className="text-xs font-medium text-muted-foreground">Popular servers</p>
<div className="flex flex-wrap gap-2">
<button
type="button"
className="text-xs rounded-full border border-muted-foreground/30 px-2 py-1 hover:bg-muted cursor-pointer"
onClick={() => applyPopularServer('kandev')}
>
+ Kandev MCP
</button>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="text-xs rounded-full border border-muted-foreground/30 px-2 py-1 hover:bg-muted cursor-pointer"
onClick={() => applyPopularServer('kandev')}
>
+ Kandev MCP
</button>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-[320px] text-xs">
{kandevTools}
</TooltipContent>
</Tooltip>
<button
type="button"
className="text-xs rounded-full border border-muted-foreground/30 px-2 py-1 hover:bg-muted cursor-pointer"
+1
View File
@@ -3,5 +3,6 @@ export {};
declare global {
interface Window {
__KANDEV_API_BASE_URL?: string;
__KANDEV_MCP_SERVER_URL?: string;
}
}
+7 -2
View File
@@ -1,22 +1,27 @@
export type AppConfig = {
apiBaseUrl: string;
mcpServerUrl: string;
};
const DEFAULT_API_BASE_URL = 'http://localhost:8080';
const DEFAULT_MCP_SERVER_URL = 'http://localhost:9090/sse';
export const DEBUG_UI = process.env.NEXT_PUBLIC_KANDEV_DEBUG === 'true';
export function getBackendConfig(): AppConfig {
if (typeof window === 'undefined') {
return {
apiBaseUrl: process.env.KANDEV_API_BASE_URL ?? DEFAULT_API_BASE_URL,
mcpServerUrl: process.env.KANDEV_MCP_SERVER_URL ?? DEFAULT_MCP_SERVER_URL,
};
}
if (window.__KANDEV_API_BASE_URL) {
if (window.__KANDEV_API_BASE_URL || window.__KANDEV_MCP_SERVER_URL) {
return {
apiBaseUrl: window.__KANDEV_API_BASE_URL,
apiBaseUrl: window.__KANDEV_API_BASE_URL ?? DEFAULT_API_BASE_URL,
mcpServerUrl: window.__KANDEV_MCP_SERVER_URL ?? DEFAULT_MCP_SERVER_URL,
};
}
return {
apiBaseUrl: process.env.NEXT_PUBLIC_KANDEV_API_BASE_URL ?? DEFAULT_API_BASE_URL,
mcpServerUrl: process.env.NEXT_PUBLIC_KANDEV_MCP_SERVER_URL ?? DEFAULT_MCP_SERVER_URL,
};
}