package main import ( "bufio" "bytes" "encoding/json" "fmt" "io" "net" "net/http" "os" "runtime" "strconv" "strings" "sync" "time" ) var ( Version = "dev" BuildTime = "unknown" // Fetched on connect latestBridgeVersion string downloadURL string ) func log(format string, args ...interface{}) { msg := fmt.Sprintf(format, args...) fmt.Fprintf(os.Stderr, "[%s] %s\n", time.Now().Format("15:04:05.000"), msg) } // semverLessThan returns true if a < b (e.g., "1.0.2" < "1.0.10") func semverLessThan(a, b string) bool { partsA := strings.Split(a, ".") partsB := strings.Split(b, ".") for i := 0; i < len(partsA) && i < len(partsB); i++ { numA, _ := strconv.Atoi(partsA[i]) numB, _ := strconv.Atoi(partsB[i]) if numA < numB { return true } if numA > numB { return false } } return len(partsA) < len(partsB) } func fetchLatestVersion(server string) { url := fmt.Sprintf("%s/version?os=%s&arch=%s", server, runtime.GOOS, runtime.GOARCH) resp, err := http.Get(url) if err != nil { log("Version fetch failed: %v", err) return } defer resp.Body.Close() var info struct { LatestBridgeVersion string `json:"latest_bridge_version"` DownloadURL string `json:"download_url"` } if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { log("Version parse failed: %v", err) return } latestBridgeVersion = info.LatestBridgeVersion downloadURL = info.DownloadURL log("Latest bridge version: %s (current: %s), download: %s", latestBridgeVersion, Version, downloadURL) } func needsUpdate() bool { if Version == "dev" || latestBridgeVersion == "" { return false } return semverLessThan(Version, latestBridgeVersion) } func prependUpdateWarning(data string, lastMethod string) string { if !needsUpdate() { return data } // Only block actual tool calls, not initialization if lastMethod != "tools/call" { return data } log("Update needed! Version=%s, Latest=%s - returning update instructions only", Version, latestBridgeVersion) // Get our own executable path exePath, err := os.Executable() if err != nil { exePath = "inou_bridge" } // Parse response to get the ID var resp map[string]interface{} if err := json.Unmarshal([]byte(data), &resp); err != nil { return data } id, ok := resp["id"] if !ok { return data } // Build platform-appropriate command var cmd string if runtime.GOOS == "windows" { cmd = fmt.Sprintf("curl -o \"%s\" %s", exePath, downloadURL) } else { cmd = fmt.Sprintf("curl -o %s %s && chmod +x %s", exePath, downloadURL, exePath) } // Return success but with only the warning - no actual data warning := fmt.Sprintf("⚠️ BRIDGE UPDATE REQUIRED\n\n") warning += fmt.Sprintf("Your Inou bridge (%s) is outdated. Latest version: %s\n\n", Version, latestBridgeVersion) warning += "I cannot access medical imaging until the bridge is updated.\n\n" warning += "Run this command in Desktop Commander:\n\n" warning += fmt.Sprintf(" %s\n\n", cmd) warning += "Then restart Claude Desktop." successResp := map[string]interface{}{ "jsonrpc": "2.0", "id": id, "result": map[string]interface{}{ "content": []map[string]interface{}{ {"type": "text", "text": warning}, }, }, } modified, _ := json.Marshal(successResp) return string(modified) } func handleLocalCall(line, server string) string { var req struct { JSONRPC string `json:"jsonrpc"` ID int `json:"id"` Method string `json:"method"` Params struct { Name string `json:"name"` } `json:"params"` } if err := json.Unmarshal([]byte(line), &req); err != nil { return "" } if req.Method != "tools/call" || req.Params.Name != "get_version" { return "" } // Fetch latest version from server resp, err := http.Get(server + "/version") if err != nil { log("Version fetch failed: %v", err) return "" } defer resp.Body.Close() var info struct { ServerVersion string `json:"server_version"` LatestBridgeVersion string `json:"latest_bridge_version"` } if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { log("Version parse failed: %v", err) return "" } // Build response text := fmt.Sprintf("Bridge version: %s\nServer version: %s\nLatest bridge: %s\n", Version, info.ServerVersion, info.LatestBridgeVersion) if Version != "dev" && Version != info.LatestBridgeVersion { text += "\n⚠️ UPDATE REQUIRED: Bridge is outdated.\n\n" text += "Please update the bridge:\n" text += "1. Download http://inou.com/download/windows/amd64\n" text += "2. Save to C:\\Users\\%USERNAME%\\inou\\inou_bridge.exe (overwrite existing)\n" text += "3. Restart Claude Desktop\n" } else { text += "\n✓ Bridge is up to date." } response := map[string]interface{}{ "jsonrpc": "2.0", "id": req.ID, "result": map[string]interface{}{ "content": []map[string]interface{}{ {"type": "text", "text": text}, }, }, } data, _ := json.Marshal(response) return string(data) } func main() { log("mcp-client starting, version=%s, build=%s", Version, BuildTime) log("PID=%d, UID=%d", os.Getpid(), os.Getuid()) log("Args: %v", os.Args) // Log environment log("HOME=%s", os.Getenv("HOME")) log("USER=%s", os.Getenv("USER")) log("PATH=%s", os.Getenv("PATH")) var server, account string for _, arg := range os.Args[1:] { if strings.HasPrefix(arg, "--server=") { server = strings.TrimPrefix(arg, "--server=") } else if strings.HasPrefix(arg, "--account=") { account = strings.TrimPrefix(arg, "--account=") } else if arg == "--version" || arg == "-v" { fmt.Printf("mcp-client %s (built %s)\n", Version, BuildTime) os.Exit(0) } } if server == "" || account == "" { fmt.Fprintf(os.Stderr, "mcp-client %s - MCP bridge for Inou medical imaging\n\n", Version) fmt.Fprintf(os.Stderr, "Usage: mcp-client --server=http://host:port --account=GUID\n") fmt.Fprintf(os.Stderr, " mcp-client --version\n") os.Exit(1) } log("Server: %s", server) log("Account: %s...", account[:8]) // Test DNS resolution host := strings.TrimPrefix(server, "http://") host = strings.TrimPrefix(host, "https://") if idx := strings.Index(host, ":"); idx > 0 { host = host[:idx] } log("Resolving host: %s", host) ips, err := net.LookupIP(host) if err != nil { log("DNS lookup failed: %v", err) } else { log("DNS resolved: %v", ips) } // Test TCP connection addr := strings.TrimPrefix(server, "http://") addr = strings.TrimPrefix(addr, "https://") log("Testing TCP connection to %s", addr) conn, err := net.DialTimeout("tcp", addr, 5*time.Second) if err != nil { log("TCP connect failed: %v", err) } else { log("TCP connect OK") conn.Close() } // Fetch latest version info fetchLatestVersion(server) // Connect to SSE endpoint (no timeout - SSE stays open) sseURL := server + "/sse?account=" + account log("HTTP GET %s", sseURL) sseClient := &http.Client{Timeout: 0} resp, err := sseClient.Get(sseURL) if err != nil { log("HTTP request failed: %v", err) os.Exit(1) } log("HTTP response: %d %s", resp.StatusCode, resp.Status) defer resp.Body.Close() if resp.StatusCode != 200 { body, _ := io.ReadAll(resp.Body) log("SSE error body: %s", string(body)) os.Exit(1) } // Read first event to get session ID reader := bufio.NewReader(resp.Body) var sessionID string for { line, err := reader.ReadString('\n') if err != nil { log("Failed to read session: %v", err) os.Exit(1) } log("SSE line: %s", strings.TrimSpace(line)) if strings.HasPrefix(line, "data: /message?session=") { sessionID = strings.TrimPrefix(line, "data: /message?session=") sessionID = strings.TrimSpace(sessionID) break } } log("Session established: %s", sessionID) messageURL := server + "/message?session=" + sessionID log("Message URL: %s", messageURL) // Track last method for version check var lastMethod string var lastMethodMu sync.Mutex // Goroutine to read SSE responses and write to stdout go func() { for { line, err := reader.ReadString('\n') if err != nil { log("SSE reader error: %v", err) return } if strings.HasPrefix(line, "data: ") { data := strings.TrimPrefix(line, "data: ") data = strings.TrimSpace(data) if data != "" { // Check if we need to block with update warning lastMethodMu.Lock() method := lastMethod lastMethodMu.Unlock() data = prependUpdateWarning(data, method) log("SSE -> stdout: %d bytes", len(data)) fmt.Println(data) } } } }() log("Ready, reading stdin...") // Read stdin, send to server scanner := bufio.NewScanner(os.Stdin) buf := make([]byte, 64*1024) scanner.Buffer(buf, 50*1024*1024) postClient := &http.Client{Timeout: 30 * time.Second} for scanner.Scan() { line := scanner.Text() log("stdin -> server: %d bytes", len(line)) // Validate JSON and extract method var req struct { Method string `json:"method"` } if err := json.Unmarshal([]byte(line), &req); err != nil { log("Invalid JSON, skipping: %v", err) continue } // Track method for version check lastMethodMu.Lock() lastMethod = req.Method lastMethodMu.Unlock() // Check if this is a get_version call we handle locally if response := handleLocalCall(line, server); response != "" { log("Handled locally (get_version)") fmt.Println(response) continue } // POST to server httpReq, _ := http.NewRequest("POST", messageURL, bytes.NewBufferString(line)) httpReq.Header.Set("Content-Type", "application/json") resp, err := postClient.Do(httpReq) if err != nil { log("POST failed: %v", err) continue } log("POST response: %d", resp.StatusCode) resp.Body.Close() } if err := scanner.Err(); err != nil { log("stdin error: %v", err) } log("stdin closed, exiting") }