master
go 656 lines 17.7 KB
Raw
1 package main
2
3 import (
4 "bufio"
5 "context"
6 "crypto/rand"
7 "encoding/base64"
8 "encoding/json"
9 "fmt"
10 "log"
11 "math"
12 "math/big"
13 "net/http"
14 "os"
15 "os/signal"
16 "strings"
17 "sync"
18 "syscall"
19 "time"
20
21 "github.com/coder/websocket"
22 )
23
24 // Connection states
25 const (
26 stateDisconnected = iota
27 stateConnecting
28 stateConnected
29 )
30
31 // WebSocket message types (these are defined in the websocket package but we need to define them here)
32 const (
33 MessageText = websocket.MessageText
34 MessageBinary = websocket.MessageBinary
35 )
36
37 // JSON-RPC 2.0 related structures
38 type JsonRpcMessage struct {
39 JsonRpc string `json:"jsonrpc"`
40 Id interface{} `json:"id,omitempty"`
41 Method string `json:"method,omitempty"`
42 Result interface{} `json:"result,omitempty"`
43 Error *JsonRpcError `json:"error,omitempty"`
44 }
45
46 type JsonRpcError struct {
47 Code int `json:"code"`
48 Message string `json:"message"`
49 Data interface{} `json:"data,omitempty"`
50 }
51
52 // ConnectionTimeout is the timeout for initial connection attempts
53 const ConnectionTimeout = 5 * time.Second
54
55 // PendingRequest represents a message waiting for the connection to be established
56 type PendingRequest struct {
57 ID interface{}
58 Message string
59 Timer *time.Timer
60 }
61
62 func generateWebSocketKey() string {
63 key := make([]byte, 16)
64 _, err := rand.Read(key)
65 if err != nil {
66 log.Fatalf("failed to generate WebSocket key: %v", err)
67 }
68 return base64.StdEncoding.EncodeToString(key)
69 }
70
71 func main() {
72 // Get program name for logs
73 programName := "nd-mcp-golang"
74 if len(os.Args) > 0 {
75 programName = os.Args[0]
76 }
77
78 args := os.Args[1:]
79 var targetURL string
80 var bearerToken string
81
82 for len(args) > 0 {
83 arg := args[0]
84 switch {
85 case arg == "--bearer":
86 if len(args) < 2 {
87 fmt.Fprintf(os.Stderr, "%s: Usage: %s [--bearer TOKEN] ws://host/path\n", programName, programName)
88 os.Exit(1)
89 }
90 bearerToken = strings.TrimSpace(args[1])
91 args = args[2:]
92 case strings.HasPrefix(arg, "--bearer="):
93 bearerToken = strings.TrimSpace(strings.TrimPrefix(arg, "--bearer="))
94 args = args[1:]
95 default:
96 if targetURL != "" {
97 fmt.Fprintf(os.Stderr, "%s: Unexpected argument '%s'\n", programName, arg)
98 fmt.Fprintf(os.Stderr, "%s: Usage: %s [--bearer TOKEN] ws://host/path\n", programName, programName)
99 os.Exit(1)
100 }
101 targetURL = arg
102 args = args[1:]
103 }
104 }
105
106 if targetURL == "" {
107 fmt.Fprintf(os.Stderr, "%s: Usage: %s [--bearer TOKEN] ws://host/path\n", programName, programName)
108 os.Exit(1)
109 }
110
111 if bearerToken == "" {
112 bearerToken = strings.TrimSpace(os.Getenv("ND_MCP_BEARER_TOKEN"))
113 }
114
115 if bearerToken != "" {
116 fmt.Fprintf(os.Stderr, "%s: Authorization header enabled for MCP connection\n", programName)
117 }
118
119 // Set up channels for communication
120 stdinCh := make(chan string, 100) // Buffer stdin messages
121 reconnectCh := make(chan struct{}, 1) // Signal for immediate reconnection
122 doneCh := make(chan struct{}) // Signal for program termination
123 stdinClosedCh := make(chan struct{}) // Signal that stdin is closed
124
125 // Global state
126 var state int // Connection state
127 var stateMu sync.Mutex
128 var messageQueueMu sync.Mutex
129 messageQueue := []string{}
130 stdinActive := true
131
132 // Pending requests that are waiting for connection to be established
133 var pendingMu sync.Mutex
134 pendingRequests := make(map[interface{}]*PendingRequest)
135
136 // Parse a JSON-RPC message and extract the ID and method
137 parseJsonRpcMessage := func(message string) (interface{}, string) {
138 var msg JsonRpcMessage
139 err := json.Unmarshal([]byte(message), &msg)
140 if err != nil || msg.JsonRpc != "2.0" {
141 return nil, ""
142 }
143 return msg.Id, msg.Method
144 }
145
146 // Create a JSON-RPC error response
147 createJsonRpcError := func(id interface{}, code int, message string, data interface{}) string {
148 response := JsonRpcMessage{
149 JsonRpc: "2.0",
150 Id: id,
151 Error: &JsonRpcError{
152 Code: code,
153 Message: message,
154 Data: data,
155 },
156 }
157 responseJson, err := json.Marshal(response)
158 if err != nil {
159 fmt.Fprintf(os.Stderr, "%s: ERROR: Failed to marshal error response: %v\n", programName, err)
160 return fmt.Sprintf("{\"jsonrpc\":\"2.0\",\"id\":%v,\"error\":{\"code\":%d,\"message\":\"%s\"}}", id, code, message)
161 }
162 return string(responseJson)
163 }
164
165 // Handle a request timeout for connection establishment ONLY
166 handleRequestTimeout := func(msgId interface{}) {
167 stateMu.Lock()
168 currentState := state
169 stateMu.Unlock()
170
171 // This timeout ONLY applies if the connection is not established yet
172 if currentState != stateConnected {
173 pendingMu.Lock()
174 if req, exists := pendingRequests[msgId]; exists {
175 fmt.Fprintf(os.Stderr, "%s: Connection timeout for request ID %v, sending error response\n", programName, msgId)
176
177 // Create and send error response
178 errorResponse := createJsonRpcError(
179 msgId,
180 -32000, // Server error code
181 "MCP server connection failed",
182 map[string]string{"details": "Could not establish connection to Netdata within timeout period"},
183 )
184
185 fmt.Println(errorResponse)
186
187 // Get the original message
188 originalMessage := req.Message
189
190 // Remove request from pending
191 delete(pendingRequests, msgId)
192 pendingMu.Unlock()
193
194 // Also remove from messageQueue if it exists there
195 messageQueueMu.Lock()
196 for i, msg := range messageQueue {
197 if msg == originalMessage {
198 // Remove from queue by creating a new slice without this element
199 messageQueue = append(messageQueue[:i], messageQueue[i+1:]...)
200 fmt.Fprintf(os.Stderr, "%s: Removed timed-out request from message queue\n", programName)
201 break
202 }
203 }
204 messageQueueMu.Unlock()
205 return
206 }
207 pendingMu.Unlock()
208 } else {
209 // The connection was established before the timeout - just clean up
210 pendingMu.Lock()
211 if _, exists := pendingRequests[msgId]; exists {
212 delete(pendingRequests, msgId)
213 }
214 pendingMu.Unlock()
215 }
216 }
217
218 // Set up signal handling
219 sigCh := make(chan os.Signal, 1)
220 signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
221 go func() {
222 sig := <-sigCh
223 fmt.Fprintf(os.Stderr, "%s: Received signal %v, shutting down\n", programName, sig)
224 close(doneCh)
225 }()
226
227 // Start reading from stdin in a separate goroutine
228 go func() {
229 scanner := bufio.NewScanner(os.Stdin)
230 scannerRunning := true
231
232 // Make scanner buffer larger to handle large messages
233 const maxScanBufferSize = 1024 * 1024 // 1MB
234 buf := make([]byte, maxScanBufferSize)
235 scanner.Buffer(buf, maxScanBufferSize)
236
237 for scannerRunning && scanner.Scan() {
238 text := scanner.Text()
239
240 // Parse for JSON-RPC ID
241 msgId, _ := parseJsonRpcMessage(text)
242
243 // Check connection state
244 stateMu.Lock()
245 currentState := state
246 stateMu.Unlock()
247
248 if currentState != stateConnected && msgId != nil {
249 // Store as pending request with timeout
250 pendingMu.Lock()
251 req := &PendingRequest{
252 ID: msgId,
253 Message: text,
254 Timer: time.AfterFunc(ConnectionTimeout, func() { handleRequestTimeout(msgId) }),
255 }
256 pendingRequests[msgId] = req
257 pendingMu.Unlock()
258
259 fmt.Fprintf(os.Stderr, "%s: Received request with ID %v, setting response timeout\n", programName, msgId)
260 }
261
262 // Queue the message
263 stdinCh <- text
264
265 // If we're not connected, trigger immediate reconnection
266 if currentState != stateConnected {
267 select {
268 case reconnectCh <- struct{}{}:
269 fmt.Fprintf(os.Stderr, "%s: Received stdin data, attempting immediate reconnection\n", programName)
270 default:
271 // Channel already has reconnection request, ignore
272 }
273 }
274 }
275
276 // Check for scanner error
277 if err := scanner.Err(); err != nil {
278 fmt.Fprintf(os.Stderr, "%s: ERROR: stdin read error: %v\n", programName, err)
279 } else {
280 fmt.Fprintf(os.Stderr, "%s: End of stdin\n", programName)
281 }
282
283 // Signal that stdin is closed
284 stdinActive = false
285 close(stdinClosedCh)
286 }()
287
288 // Reconnection parameters
289 baseDelay := 1 * time.Second
290 maxDelay := 60 * time.Second
291 attempt := 0
292
293 // Timer for reconnection backoff
294 var timer *time.Timer
295
296 // Main connection loop
297 for {
298 select {
299 case <-doneCh:
300 // Program termination requested
301 return
302 case <-stdinClosedCh:
303 // Stdin closed, continue running until websocket disconnects
304 // and then exit on the next reconnection attempt
305 stdinClosedCh = nil // Prevent duplicate handling
306 case <-reconnectCh:
307 // Immediate reconnection requested (e.g. from stdin activity)
308 if timer != nil {
309 timer.Stop()
310 }
311
312 // Only proceed with immediate reconnection if we're not already connecting/connected
313 stateMu.Lock()
314 currentState := state
315 stateMu.Unlock()
316
317 if currentState == stateDisconnected {
318 // Reset the reconnection timer
319 attempt = 0
320 // Fall through to connection attempt
321 } else {
322 // Already connecting or connected
323 continue
324 }
325 default:
326 // Calculate backoff delay with jitter
327 if attempt > 0 {
328 // Check if stdin is closed and we're disconnected - if so, exit
329 if !stdinActive {
330 fmt.Fprintf(os.Stderr, "%s: Stdin closed and disconnected, exiting\n", programName)
331 return
332 }
333
334 delaySeconds := math.Min(float64(maxDelay.Seconds()),
335 float64(baseDelay.Seconds())*math.Pow(2, float64(attempt-1))*(0.5+jitter()))
336 delay := time.Duration(delaySeconds * float64(time.Second))
337
338 fmt.Fprintf(os.Stderr, "%s: Reconnecting in %.1f seconds (attempt %d)...\n",
339 programName, delaySeconds, attempt)
340
341 // Create timer and wait for it to expire, or for signals
342 timer = time.NewTimer(delay)
343
344 select {
345 case <-timer.C:
346 // Timer expired, continue to connection attempt
347 case <-reconnectCh:
348 // Immediate reconnection requested
349 timer.Stop()
350 case <-doneCh:
351 // Program termination requested
352 timer.Stop()
353 return
354 case <-stdinClosedCh:
355 // Stdin closed
356 timer.Stop()
357 stdinClosedCh = nil
358 continue
359 }
360 }
361 }
362
363 // Update state to connecting
364 stateMu.Lock()
365 state = stateConnecting
366 stateMu.Unlock()
367
368 // Set up connection context with cancellation
369 ctx, cancel := context.WithCancel(context.Background())
370
371 // Set up connection timeout
372 connectionCtx, connectionCancel := context.WithTimeout(ctx, 15*time.Second)
373 defer connectionCancel()
374
375 fmt.Fprintf(os.Stderr, "%s: Connecting to %s...\n", programName, targetURL)
376
377 // Create a custom header with the WebSocket key
378 header := http.Header{}
379 header.Set("Sec-WebSocket-Key", generateWebSocketKey())
380 header.Set("Sec-WebSocket-Version", "13")
381 if bearerToken != "" {
382 header.Set("Authorization", "Bearer "+bearerToken)
383 }
384
385 // Connect to WebSocket
386 conn, _, err := websocket.Dial(connectionCtx, targetURL, &websocket.DialOptions{
387 CompressionMode: websocket.CompressionContextTakeover,
388 HTTPHeader: header,
389 })
390
391 // Connection failed
392 if err != nil {
393 fmt.Fprintf(os.Stderr, "%s: ERROR: websocket connection failed: %v\n", programName, err)
394 cancel()
395
396 // Increment attempt counter and try again
397 attempt++
398
399 // Update state to disconnected
400 stateMu.Lock()
401 state = stateDisconnected
402 stateMu.Unlock()
403 continue
404 }
405
406 // Increase the read limit to match Netdata's WS_MAX_INCOMING_FRAME_SIZE (16MB)
407 conn.SetReadLimit(16 * 1024 * 1024)
408
409 // Update state to connected
410 stateMu.Lock()
411 state = stateConnected
412 stateMu.Unlock()
413
414 fmt.Fprintf(os.Stderr, "%s: Connected\n", programName)
415 attempt = 0 // Reset attempt counter on successful connection
416
417 // Process any queued messages, including pending requests
418 messageQueueMu.Lock()
419 if len(messageQueue) > 0 {
420 fmt.Fprintf(os.Stderr, "%s: Sending %d queued message(s)\n", programName, len(messageQueue))
421 for _, msg := range messageQueue {
422 if err := conn.Write(ctx, websocket.MessageText, []byte(msg)); err != nil {
423 fmt.Fprintf(os.Stderr, "%s: ERROR: failed to send queued message: %v\n", programName, err)
424 } else {
425 // Check if this was a pending request with an ID
426 msgId, _ := parseJsonRpcMessage(msg)
427 if msgId != nil {
428 pendingMu.Lock()
429 if req, exists := pendingRequests[msgId]; exists {
430 // Stop the timer
431 if req.Timer != nil {
432 req.Timer.Stop()
433 }
434 delete(pendingRequests, msgId)
435 }
436 pendingMu.Unlock()
437 }
438 }
439 }
440 messageQueue = nil // Clear the queue
441 }
442 messageQueueMu.Unlock()
443
444 // Memory management: Limit size of pendingRequests to prevent memory leaks
445 pendingMu.Lock()
446 if len(pendingRequests) > 1000 {
447 fmt.Fprintf(os.Stderr, "%s: Too many pending requests (%d), cleaning up older ones\n", programName, len(pendingRequests))
448 // Remove older entries (this is a bit tricky in Go without order guarantee)
449 // For simplicity, we'll just clear it all in this extreme case
450 pendingRequests = make(map[interface{}]*PendingRequest)
451 }
452 pendingMu.Unlock()
453
454 // Signal for connection closure
455 connClosed := make(chan struct{})
456
457 // Create wait group for goroutines
458 var wg sync.WaitGroup
459
460 // Goroutine for handling termination signals during connection
461 wg.Add(1)
462 go func() {
463 defer wg.Done()
464 select {
465 case <-ctx.Done():
466 return
467 case <-doneCh:
468 // Clean up the connection
469 conn.Close(websocket.StatusNormalClosure, "Shutdown requested")
470 cancel()
471 return
472 case <-stdinClosedCh:
473 // Stdin closed, but keep connection active
474 stdinClosedCh = nil
475 return
476 }
477 }()
478
479 // Goroutine for sending messages to websocket
480 wg.Add(1)
481 go func() {
482 defer wg.Done()
483 for {
484 select {
485 case <-ctx.Done():
486 return
487 case <-connClosed:
488 return
489 case msg, ok := <-stdinCh:
490 if !ok {
491 // Stdin channel was closed
492 return
493 }
494
495 err := conn.Write(ctx, websocket.MessageText, []byte(msg))
496 if err != nil {
497 // Check if this was a message with ID
498 msgId, _ := parseJsonRpcMessage(msg)
499
500 select {
501 case <-connClosed:
502 // Connection already known to be closed, queue the message
503 messageQueueMu.Lock()
504 messageQueue = append(messageQueue, msg)
505 messageQueueMu.Unlock()
506
507 // If the message had an ID, keep the pending status
508 if msgId != nil {
509 pendingMu.Lock()
510 if _, exists := pendingRequests[msgId]; !exists {
511 // Create a new pending request with timeout if it doesn't exist
512 req := &PendingRequest{
513 ID: msgId,
514 Message: msg,
515 Timer: time.AfterFunc(ConnectionTimeout, func() { handleRequestTimeout(msgId) }),
516 }
517 pendingRequests[msgId] = req
518 }
519 pendingMu.Unlock()
520 }
521 default:
522 fmt.Fprintf(os.Stderr, "%s: ERROR: write to websocket: %v\n", programName, err)
523 messageQueueMu.Lock()
524 messageQueue = append(messageQueue, msg)
525 messageQueueMu.Unlock()
526
527 // Same ID handling as above
528 if msgId != nil {
529 pendingMu.Lock()
530 if _, exists := pendingRequests[msgId]; !exists {
531 req := &PendingRequest{
532 ID: msgId,
533 Message: msg,
534 Timer: time.AfterFunc(ConnectionTimeout, func() { handleRequestTimeout(msgId) }),
535 }
536 pendingRequests[msgId] = req
537 }
538 pendingMu.Unlock()
539 }
540 }
541 } else {
542 // Message was sent successfully, remove from pending if it had an ID
543 msgId, _ := parseJsonRpcMessage(msg)
544 if msgId != nil {
545 pendingMu.Lock()
546 if req, exists := pendingRequests[msgId]; exists {
547 // Stop the connection establishment timer (if any)
548 if req.Timer != nil {
549 req.Timer.Stop()
550 req.Timer = nil // Clear timer to prevent memory leaks
551 }
552 // Keep in pendingRequests until we get a response
553 }
554 pendingMu.Unlock()
555 }
556 }
557 }
558 }
559 }()
560
561 // Goroutine for receiving messages from websocket
562 wg.Add(1)
563 go func() {
564 defer wg.Done()
565 defer close(connClosed)
566
567 for {
568 messageType, message, err := conn.Read(ctx)
569 if err != nil {
570 // Don't report error if context was cancelled
571 if ctx.Err() == nil {
572 fmt.Fprintf(os.Stderr, "%s: ERROR: read from websocket: %v\n", programName, err)
573 }
574 return
575 }
576
577 if messageType == websocket.MessageText {
578 fmt.Println(string(message))
579
580 // Check if this is a response to a request with an ID
581 var response JsonRpcMessage
582 if err := json.Unmarshal(message, &response); err == nil && response.JsonRpc == "2.0" && response.Id != nil {
583 pendingMu.Lock()
584 if req, exists := pendingRequests[response.Id]; exists {
585 // Stop the timer
586 if req.Timer != nil {
587 req.Timer.Stop()
588 }
589 delete(pendingRequests, response.Id)
590 }
591 pendingMu.Unlock()
592 }
593 }
594 }
595 }()
596
597 // Set up periodic pinger to keep connection alive
598 pingInterval := 30 * time.Second
599 wg.Add(1)
600 go func() {
601 defer wg.Done()
602 ticker := time.NewTicker(pingInterval)
603 defer ticker.Stop()
604
605 for {
606 select {
607 case <-ticker.C:
608 // Send ping using a text message with a special ping format
609 err := conn.Write(ctx, websocket.MessageText, []byte("PING"))
610 if err != nil {
611 fmt.Fprintf(os.Stderr, "%s: ERROR: ping failed: %v\n", programName, err)
612 // Connection will be closed by the read loop
613 }
614 case <-ctx.Done():
615 return
616 case <-connClosed:
617 return
618 }
619 }
620 }()
621
622 // Wait for connection closed
623 <-connClosed
624
625 // Update state to disconnected
626 stateMu.Lock()
627 state = stateDisconnected
628 stateMu.Unlock()
629
630 // Clean up
631 cancel()
632 wg.Wait()
633
634 // Don't automatically reconnect if stdin closed and no messages in queue
635 messageQueueMu.Lock()
636 hasMessages := len(messageQueue) > 0
637 messageQueueMu.Unlock()
638
639 if !stdinActive && !hasMessages {
640 fmt.Fprintf(os.Stderr, "%s: Stdin closed and no pending messages, exiting\n", programName)
641 return
642 }
643
644 // Increment attempt counter for next reconnection
645 attempt++
646 }
647 }
648
649 // jitter returns a random value between 0.0 and 1.0
650 func jitter() float64 {
651 n, err := rand.Int(rand.Reader, big.NewInt(1000))
652 if err != nil {
653 return 0.5 // Fall back to 0.5 if there's an error
654 }
655 return float64(n.Int64()) / 1000.0
656 }