inou/mcp-client/main.go.bak

382 lines
9.6 KiB
Go
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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")
}