Skip to content

Commit 844eba2

Browse files
committed
✨ feat(mcp): add structured request logging with timing and error tracking
- Add request logging middleware to MCP SDK server - Log PID, log file path, and log level at server startup - Track tool call duration and result status - Include error logging for failed tool calls and encoding errors
1 parent 47c23d9 commit 844eba2

4 files changed

Lines changed: 442 additions & 2 deletions

File tree

internal/app/run.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"io"
77
"log/slog"
8+
"os"
89

910
"codex-mem/internal/buildinfo"
1011
"codex-mem/internal/config"
@@ -97,7 +98,11 @@ func Run(ctx context.Context, cfg config.Config, args []string, stdin io.Reader,
9798
defer func() {
9899
_ = instance.Close()
99100
}()
100-
logger.Info("starting MCP stdio server")
101+
logger.Info("starting MCP stdio server",
102+
"pid", os.Getpid(),
103+
"log_file", cfg.File.LogFilePath,
104+
"log_level", cfg.File.LogLevel.String(),
105+
)
101106
return mcp.ServeStdio(ctx, mcp.NewSDKServer(instance.Handlers), stdin, stdout)
102107
case "serve-http":
103108
options, err := parseServeHTTPOptions(commandArgs)
@@ -112,9 +117,12 @@ func Run(ctx context.Context, cfg config.Config, args []string, stdin io.Reader,
112117
_ = instance.Close()
113118
}()
114119
logger.Info("starting MCP HTTP server",
120+
"pid", os.Getpid(),
115121
"listen", options.ListenAddr,
116122
"path", options.EndpointPath,
117123
"allowed_origins", options.AllowedOrigins,
124+
"log_file", cfg.File.LogFilePath,
125+
"log_level", cfg.File.LogLevel.String(),
118126
)
119127
return mcp.ServeHTTP(ctx, options.ListenAddr, mcp.NewSDKHTTPHandler(
120128
mcp.NewSDKServer(instance.Handlers),

internal/mcp/logging.go

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
package mcp
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"log/slog"
7+
"sort"
8+
"strings"
9+
"time"
10+
11+
sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp"
12+
)
13+
14+
func newServerLogger() *slog.Logger {
15+
return slog.Default().With("component", "mcp")
16+
}
17+
18+
func requestLoggingMiddleware(logger *slog.Logger) sdkmcp.Middleware {
19+
return func(next sdkmcp.MethodHandler) sdkmcp.MethodHandler {
20+
return func(ctx context.Context, method string, req sdkmcp.Request) (sdkmcp.Result, error) {
21+
started := time.Now()
22+
result, err := next(ctx, method, req)
23+
if method == "tools/call" {
24+
return result, err
25+
}
26+
27+
attrs := append(requestLogAttrs(req), "method", method, "duration_ms", time.Since(started).Milliseconds())
28+
if err != nil {
29+
logger.Error("MCP request failed", appendAttrs(attrs, "err", err)...)
30+
return nil, err
31+
}
32+
if result == nil {
33+
return nil, nil
34+
}
35+
36+
logger.Info("MCP request completed", appendAttrs(attrs, summarizeMethodParams(method, req.GetParams())...)...)
37+
return result, nil
38+
}
39+
}
40+
}
41+
42+
func requestLogAttrs(req sdkmcp.Request) []any {
43+
attrs := make([]any, 0, 10)
44+
if req == nil {
45+
return attrs
46+
}
47+
48+
if session := req.GetSession(); session != nil && session.ID() != "" {
49+
attrs = append(attrs, "session_id", session.ID())
50+
}
51+
52+
return attrs
53+
}
54+
55+
func summarizeMethodParams(method string, params sdkmcp.Params) []any {
56+
if method != "initialize" {
57+
return nil
58+
}
59+
60+
initParams, ok := params.(*sdkmcp.InitializeParams)
61+
if !ok || initParams == nil {
62+
return nil
63+
}
64+
65+
attrs := make([]any, 0, 6)
66+
if initParams.ClientInfo != nil {
67+
if initParams.ClientInfo.Name != "" {
68+
attrs = append(attrs, "client_name", initParams.ClientInfo.Name)
69+
}
70+
if initParams.ClientInfo.Version != "" {
71+
attrs = append(attrs, "client_version", initParams.ClientInfo.Version)
72+
}
73+
}
74+
if initParams.ProtocolVersion != "" {
75+
attrs = append(attrs, "protocol_version", initParams.ProtocolVersion)
76+
}
77+
return attrs
78+
}
79+
80+
func toolLogAttrs(request *sdkmcp.CallToolRequest, toolName string, raw json.RawMessage) []any {
81+
attrs := make([]any, 0, 12)
82+
if request != nil && request.Session != nil {
83+
if sessionID := request.Session.ID(); sessionID != "" {
84+
attrs = append(attrs, "session_id", sessionID)
85+
}
86+
if initParams := request.Session.InitializeParams(); initParams != nil {
87+
if initParams.ClientInfo != nil {
88+
if initParams.ClientInfo.Name != "" {
89+
attrs = append(attrs, "client_name", initParams.ClientInfo.Name)
90+
}
91+
if initParams.ClientInfo.Version != "" {
92+
attrs = append(attrs, "client_version", initParams.ClientInfo.Version)
93+
}
94+
}
95+
if initParams.ProtocolVersion != "" {
96+
attrs = append(attrs, "protocol_version", initParams.ProtocolVersion)
97+
}
98+
}
99+
}
100+
101+
attrs = append(attrs, "tool_name", toolName)
102+
if summary := summarizeToolArguments(raw); len(summary) > 0 {
103+
attrs = append(attrs, "args_summary", summary)
104+
}
105+
return attrs
106+
}
107+
108+
func summarizeToolArguments(raw json.RawMessage) map[string]any {
109+
if len(raw) == 0 || string(raw) == "null" {
110+
return nil
111+
}
112+
113+
var payload map[string]any
114+
if err := json.Unmarshal(raw, &payload); err != nil {
115+
return map[string]any{"raw": compactString(string(raw), 160)}
116+
}
117+
118+
summary := map[string]any{
119+
"keys": sortedKeys(payload),
120+
}
121+
addStringSummary(summary, "cwd", payload, "cwd")
122+
addStringSummary(summary, "task", payload, "task")
123+
addStringSummary(summary, "title", payload, "title")
124+
addStringSummary(summary, "query", payload, "query")
125+
addStringSummary(summary, "id", payload, "id")
126+
addStringSummary(summary, "kind", payload, "kind")
127+
addStringSummary(summary, "session_id", payload, "session_id")
128+
addStringSummary(summary, "source", payload, "source")
129+
addStringSummary(summary, "target", payload, "target")
130+
addStringSummary(summary, "mode", payload, "mode")
131+
addStringSummary(summary, "external_id", payload, "external_id")
132+
addStringSummary(summary, "payload_hash", payload, "payload_hash")
133+
134+
if value, ok := payload["content"].(string); ok {
135+
summary["content_chars"] = len(value)
136+
}
137+
if value, ok := payload["summary"].(string); ok {
138+
summary["summary_chars"] = len(value)
139+
}
140+
if value, ok := payload["importance"]; ok {
141+
summary["importance"] = value
142+
}
143+
if value, ok := payload["limit"]; ok {
144+
summary["limit"] = value
145+
}
146+
147+
addArrayCount(summary, "tags_count", payload, "tags")
148+
addArrayCount(summary, "file_paths_count", payload, "file_paths")
149+
addArrayCount(summary, "completed_count", payload, "completed")
150+
addArrayCount(summary, "next_steps_count", payload, "next_steps")
151+
addArrayCount(summary, "open_questions_count", payload, "open_questions")
152+
addArrayCount(summary, "risks_count", payload, "risks")
153+
addArrayCount(summary, "types_count", payload, "types")
154+
addArrayCount(summary, "states_count", payload, "states")
155+
156+
if scopeSummary := summarizeScope(payload["scope"]); len(scopeSummary) > 0 {
157+
summary["scope"] = scopeSummary
158+
}
159+
160+
return summary
161+
}
162+
163+
func summarizeScope(value any) map[string]any {
164+
scopeMap, ok := value.(map[string]any)
165+
if !ok || len(scopeMap) == 0 {
166+
return nil
167+
}
168+
169+
summary := map[string]any{}
170+
addStringSummary(summary, "system_id", scopeMap, "system_id")
171+
addStringSummary(summary, "project_id", scopeMap, "project_id")
172+
addStringSummary(summary, "workspace_id", scopeMap, "workspace_id")
173+
addStringSummary(summary, "branch_name", scopeMap, "branch_name")
174+
addStringSummary(summary, "resolved_by", scopeMap, "resolved_by")
175+
addStringSummary(summary, "workspace_root", scopeMap, "workspace_root")
176+
return summary
177+
}
178+
179+
func summarizeToolResult(payload any) []any {
180+
if payload == nil {
181+
return nil
182+
}
183+
184+
body, err := json.Marshal(payload)
185+
if err != nil {
186+
return nil
187+
}
188+
189+
var object map[string]any
190+
if err := json.Unmarshal(body, &object); err != nil {
191+
return nil
192+
}
193+
194+
attrs := make([]any, 0, 10)
195+
if okValue, exists := object["ok"].(bool); exists {
196+
attrs = append(attrs, "response_ok", okValue)
197+
}
198+
if warnings, ok := object["warnings"].([]any); ok && len(warnings) > 0 {
199+
attrs = append(attrs, "warning_count", len(warnings), "warning_codes", warningCodes(warnings))
200+
}
201+
if errValue, ok := object["error"].(map[string]any); ok {
202+
if code, ok := errValue["code"].(string); ok && code != "" {
203+
attrs = append(attrs, "error_code", code)
204+
}
205+
}
206+
if dataSummary := summarizeResultData(object["data"]); len(dataSummary) > 0 {
207+
attrs = append(attrs, "data_summary", dataSummary)
208+
}
209+
return attrs
210+
}
211+
212+
func summarizeResultData(value any) map[string]any {
213+
data, ok := value.(map[string]any)
214+
if !ok || len(data) == 0 {
215+
return nil
216+
}
217+
218+
summary := map[string]any{
219+
"keys": sortedKeys(data),
220+
}
221+
222+
if sessionValue, ok := data["session"].(map[string]any); ok {
223+
addStringSummary(summary, "session_id", sessionValue, "id")
224+
}
225+
if noteValue, ok := data["note"].(map[string]any); ok {
226+
addStringSummary(summary, "note_id", noteValue, "id")
227+
}
228+
if handoffValue, ok := data["handoff"].(map[string]any); ok {
229+
addStringSummary(summary, "handoff_id", handoffValue, "id")
230+
}
231+
if importValue, ok := data["import"].(map[string]any); ok {
232+
addStringSummary(summary, "import_id", importValue, "id")
233+
}
234+
if scopeSummary := summarizeScope(data["scope"]); len(scopeSummary) > 0 {
235+
summary["scope"] = scopeSummary
236+
}
237+
238+
addArrayCount(summary, "results_count", data, "results")
239+
addArrayCount(summary, "notes_count", data, "notes")
240+
addArrayCount(summary, "handoffs_count", data, "handoffs")
241+
addArrayCount(summary, "written_files_count", data, "written_files")
242+
addArrayCount(summary, "skipped_files_count", data, "skipped_files")
243+
addArrayCount(summary, "recent_notes_count", data, "recent_notes")
244+
addArrayCount(summary, "related_notes_count", data, "related_notes")
245+
246+
if value, ok := data["deduplicated"]; ok {
247+
summary["deduplicated"] = value
248+
}
249+
if value, ok := data["suppressed"]; ok {
250+
summary["suppressed"] = value
251+
}
252+
if value, ok := data["materialized"]; ok {
253+
summary["materialized"] = value
254+
}
255+
256+
return summary
257+
}
258+
259+
func warningCodes(warnings []any) []string {
260+
codes := make([]string, 0, len(warnings))
261+
for _, warning := range warnings {
262+
warningMap, ok := warning.(map[string]any)
263+
if !ok {
264+
continue
265+
}
266+
code, _ := warningMap["code"].(string)
267+
if code == "" {
268+
continue
269+
}
270+
codes = append(codes, code)
271+
}
272+
return codes
273+
}
274+
275+
func addStringSummary(summary map[string]any, outKey string, payload map[string]any, inKey string) {
276+
value, ok := payload[inKey].(string)
277+
if !ok || strings.TrimSpace(value) == "" {
278+
return
279+
}
280+
summary[outKey] = compactString(value, 120)
281+
}
282+
283+
func addArrayCount(summary map[string]any, outKey string, payload map[string]any, inKey string) {
284+
items, ok := payload[inKey].([]any)
285+
if !ok || len(items) == 0 {
286+
return
287+
}
288+
summary[outKey] = len(items)
289+
}
290+
291+
func sortedKeys(payload map[string]any) []string {
292+
keys := make([]string, 0, len(payload))
293+
for key := range payload {
294+
keys = append(keys, key)
295+
}
296+
sort.Strings(keys)
297+
return keys
298+
}
299+
300+
func compactString(value string, maxLen int) string {
301+
value = strings.Join(strings.Fields(value), " ")
302+
if maxLen <= 0 || len(value) <= maxLen {
303+
return value
304+
}
305+
if maxLen <= 3 {
306+
return value[:maxLen]
307+
}
308+
return value[:maxLen-3] + "..."
309+
}
310+
311+
func appendAttrs(base []any, extra ...any) []any {
312+
attrs := make([]any, 0, len(base)+len(extra))
313+
attrs = append(attrs, base...)
314+
attrs = append(attrs, extra...)
315+
return attrs
316+
}

0 commit comments

Comments
 (0)