@@ -3,6 +3,7 @@ package daemon
3
3
import (
4
4
"bufio"
5
5
"context"
6
+ "errors"
6
7
"fmt"
7
8
"io"
8
9
"net"
@@ -31,8 +32,9 @@ type Daemon struct {
31
32
apiServer * ApiServer
32
33
logger hclog.Logger
33
34
clientManager * ClientManager
35
+ healthTracker * HealthTracker
34
36
supportedRuntimes map [runtime.Runtime ]struct {}
35
- cfgLoader config. Loader
37
+ runtimeCfg []runtime. Server
36
38
}
37
39
38
40
func NewDaemon (logger hclog.Logger , cfgLoader config.Loader , apiAddr string ) (* Daemon , error ) {
@@ -46,48 +48,36 @@ func NewDaemon(logger hclog.Logger, cfgLoader config.Loader, apiAddr string) (*D
46
48
return nil , fmt .Errorf ("invalid api address '%s': %w" , apiAddr , err )
47
49
}
48
50
49
- clientManager := NewClientManager ()
51
+ // Load config.
52
+ cfg , err := loadConfig (cfgLoader )
53
+ if err != nil {
54
+ return nil , err
55
+ }
50
56
51
- apiServer , err := NewApiServer (logger , clientManager , apiAddr )
57
+ var serverNames []string
58
+ for _ , r := range cfg {
59
+ serverNames = append (serverNames , r .Name )
60
+ }
61
+
62
+ healthTracker := NewHealthTracker (serverNames )
63
+ clientManager := NewClientManager ()
64
+ apiServer , err := NewApiServer (logger , clientManager , healthTracker , apiAddr )
52
65
if err != nil {
53
66
return nil , fmt .Errorf ("failed to create daemon API server: %w" , err )
54
67
}
55
68
56
69
return & Daemon {
57
70
logger : logger .Named ("daemon" ),
58
71
clientManager : clientManager ,
72
+ healthTracker : healthTracker ,
59
73
apiServer : apiServer ,
60
74
supportedRuntimes : runtime .DefaultSupportedRuntimes (),
61
- cfgLoader : cfgLoader ,
75
+ runtimeCfg : cfg ,
62
76
}, nil
63
77
}
64
78
65
- func (d * Daemon ) LoadConfig () ([]runtime.Server , error ) {
66
- cfgPath := flags .ConfigFile
67
- cfg , err := d .cfgLoader .Load (cfgPath )
68
- if err != nil {
69
- return nil , err
70
- }
71
-
72
- // Use the home directory to load the execution context config data (for now).
73
- home , err := os .UserHomeDir ()
74
- if err != nil {
75
- return nil , fmt .Errorf ("could not determine home directory: %w" , err )
76
- }
77
- executionCtxPath := filepath .Join (home , ".mcpd" , "secrets.dev.toml" )
78
- execCtx , err := configcontext .LoadExecutionContextConfig (executionCtxPath )
79
- if err != nil {
80
- return nil , err
81
- }
82
-
83
- return runtime .AggregateConfigs (cfg , execCtx )
84
- }
85
-
86
79
func (d * Daemon ) StartAndManage (ctx context.Context ) error {
87
- runtimeCfg , err := d .LoadConfig ()
88
- if err != nil {
89
- return err
90
- }
80
+ runtimeCfg := d .runtimeCfg
91
81
92
82
d .logger .Info (fmt .Sprintf ("loaded config for %d daemon(s)" , len (runtimeCfg )))
93
83
fmt .Println (fmt .Sprintf ("Attempting to start %d MCP server(s)" , len (runtimeCfg )))
@@ -221,7 +211,7 @@ func (d *Daemon) launchServer(ctx context.Context, server runtime.Server, wg *sy
221
211
initializeCtx ,
222
212
mcp.InitializeRequest {
223
213
Params : mcp.InitializeParams {
224
- ProtocolVersion : "latest" ,
214
+ ProtocolVersion : mcp . LATEST_PROTOCOL_VERSION ,
225
215
ClientInfo : mcp.Implementation {Name : cmd .AppName (), Version : cmd .Version ()},
226
216
},
227
217
})
@@ -240,52 +230,6 @@ func (d *Daemon) launchServer(ctx context.Context, server runtime.Server, wg *sy
240
230
return nil
241
231
}
242
232
243
- // parseAndLogMCPMessage parses a log line from the MCP server's stderr and logs it with the corresponding level.
244
- func parseAndLogMCPMessage (logger hclog.Logger , line string ) {
245
- trimmed := strings .TrimSpace (line )
246
- if trimmed == "" {
247
- return
248
- }
249
-
250
- // TODO: This format may change based on the runtime that spawned the MCP Server.
251
- // Attempt to parse the log format: LEVEL:LOGGER:MESSAGE.
252
- parts := strings .SplitN (trimmed , ":" , 3 )
253
-
254
- if len (parts ) < 2 {
255
- logger .Info (trimmed )
256
- return
257
- }
258
-
259
- lvl := normalizeLogLevel (parts [0 ])
260
- message := parts [len (parts )- 1 ]
261
-
262
- if lvl == hclog .NoLevel {
263
- logger .Info (trimmed )
264
- return
265
- }
266
-
267
- if lvl >= logger .GetLevel () {
268
- // The level is valid and at or above our logger's configured level.
269
- logger .Log (lvl , message )
270
- }
271
-
272
- // Either no logging (off) or a level we're not configured to log at.
273
- return
274
- }
275
-
276
- func normalizeLogLevel (level string ) hclog.Level {
277
- level = strings .TrimSpace (strings .ToLower (level ))
278
-
279
- switch level {
280
- case "warning" :
281
- return hclog .Warn // Normalize to warn
282
- case "fatal" , "critical" :
283
- return hclog .Error // Normalize to error
284
- default :
285
- return hclog .LevelFromString (level )
286
- }
287
- }
288
-
289
233
func (d * Daemon ) healthCheckLoop (
290
234
ctx context.Context ,
291
235
interval time.Duration ,
@@ -320,13 +264,32 @@ func (d *Daemon) pingAllServers(ctx context.Context, timeout time.Duration) {
320
264
pingCtx , cancel := context .WithTimeout (ctx , timeout )
321
265
defer cancel ()
322
266
323
- if err := mcpClient .Ping (pingCtx ); err != nil {
324
- d .logger .Error (fmt .Sprintf ("Error pinging MCP server: '%s'" , name ), "error" , err )
325
- return
267
+ start := time .Now ()
268
+ err := mcpClient .Ping (pingCtx )
269
+ duration := time .Since (start )
270
+
271
+ status := HealthStatusUnknown
272
+ var latency * time.Duration
273
+
274
+ switch {
275
+ case err == nil :
276
+ status = HealthStatusOK
277
+ latency = & duration
278
+ d .logger .Debug ("Ping successful" , "server" , name , "latency" , duration )
279
+ case errors .Is (err , context .DeadlineExceeded ):
280
+ status = HealthStatusTimeout
281
+ d .logger .Error ("Ping timed out" , "server" , name , "error" , err )
282
+ case errors .Is (err , context .Canceled ):
283
+ status = HealthStatusTimeout
284
+ d .logger .Warn ("Ping context canceled" , "server" , name )
285
+ default :
286
+ status = HealthStatusUnreachable
287
+ d .logger .Error ("Ping unreachable" , "server" , name , "error" , err )
326
288
}
327
289
328
- // TODO: Store health state for servers, and expose HTTP API route for /heath
329
- d .logger .Debug ("Ping successful" , "server" , name )
290
+ if updateErr := d .healthTracker .Update (name , status , latency ); updateErr != nil {
291
+ d .logger .Error ("Failed to record health" , "server" , name , "error" , updateErr )
292
+ }
330
293
}(name , c )
331
294
}
332
295
}
@@ -354,3 +317,71 @@ func IsValidAddr(addr string) error {
354
317
355
318
return nil
356
319
}
320
+
321
+ // parseAndLogMCPMessage parses a log line from the MCP server's stderr and logs it with the corresponding level.
322
+ func parseAndLogMCPMessage (logger hclog.Logger , line string ) {
323
+ trimmed := strings .TrimSpace (line )
324
+ if trimmed == "" {
325
+ return
326
+ }
327
+
328
+ // TODO: This format may change based on the runtime that spawned the MCP Server.
329
+ // Attempt to parse the log format: LEVEL:LOGGER:MESSAGE.
330
+ parts := strings .SplitN (trimmed , ":" , 3 )
331
+
332
+ if len (parts ) < 2 {
333
+ logger .Info (trimmed )
334
+ return
335
+ }
336
+
337
+ lvl := normalizeLogLevel (parts [0 ])
338
+ message := parts [len (parts )- 1 ]
339
+
340
+ if lvl == hclog .NoLevel {
341
+ logger .Info (trimmed )
342
+ return
343
+ }
344
+
345
+ if lvl >= logger .GetLevel () {
346
+ // The level is valid and at or above our logger's configured level.
347
+ logger .Log (lvl , message )
348
+ }
349
+
350
+ // Either no logging (off) or a level we're not configured to log at.
351
+ return
352
+ }
353
+
354
+ func normalizeLogLevel (level string ) hclog.Level {
355
+ level = strings .TrimSpace (strings .ToLower (level ))
356
+
357
+ switch level {
358
+ case "warning" :
359
+ return hclog .Warn // Normalize to warn
360
+ case "fatal" , "critical" :
361
+ return hclog .Error // Normalize to error
362
+ default :
363
+ return hclog .LevelFromString (level )
364
+ }
365
+ }
366
+
367
+ func loadConfig (cfgLoader config.Loader ) ([]runtime.Server , error ) {
368
+ cfgPath := flags .ConfigFile
369
+
370
+ cfg , err := cfgLoader .Load (cfgPath )
371
+ if err != nil {
372
+ return nil , err
373
+ }
374
+
375
+ // Use the home directory to load the execution context config data (for now).
376
+ home , err := os .UserHomeDir ()
377
+ if err != nil {
378
+ return nil , fmt .Errorf ("could not determine home directory: %w" , err )
379
+ }
380
+ executionCtxPath := filepath .Join (home , ".mcpd" , "secrets.dev.toml" )
381
+ execCtx , err := configcontext .LoadExecutionContextConfig (executionCtxPath )
382
+ if err != nil {
383
+ return nil , err
384
+ }
385
+
386
+ return runtime .AggregateConfigs (cfg , execCtx )
387
+ }
0 commit comments