package main import ( "bufio" "bytes" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "strings" ) const ( defaultAPIKey = "opx-57ffcb013651f7acae558cd9ab094dbd719fdec980a6ad53" proxyBaseURL = "http://127.0.0.1:6446/v1" ) func main() { debug := os.Getenv("MCP_DEBUG") != "" logf := func(f string, a ...any) { if debug { fmt.Fprintf(os.Stderr, f+"\n", a...) } } reader := bufio.NewReader(os.Stdin) buf := make([]byte, 0, 64*1024) for { var msg map[string]any first, err := reader.ReadString('\n') if err != nil { logf("stdin EOF: %v", err) return } logf("RAW stdin first line: %q", first) if strings.HasPrefix(first, "Content-Length: ") { var contentLen int fmt.Sscanf(first, "Content-Length: %d", &contentLen) logf("mode=Content-Length len=%d", contentLen) emptyLine, err := reader.ReadString('\n') if err != nil { logf("err reading empty line: %v", err) return } logf("empty line: %q", emptyLine) buf = buf[:contentLen] if _, err := io.ReadFull(reader, buf); err != nil { logf("err reading body: %v", err) return } logf("body: %s", string(buf)) json.Unmarshal(buf, &msg) } else if len(first) > 0 && first[0] == '{' { // Raw JSON line mode (like engram) logf("mode=rawJSON") json.Unmarshal([]byte(strings.TrimRight(first, "\r\n")), &msg) } else { logf("unrecognized input, skipping: %q", first) continue } if msg == nil { logf("msg is nil, skipping") continue } method, _ := msg["method"].(string) id, _ := msg["id"] switch method { case "initialize": respond(id, map[string]any{ "protocolVersion": "2025-03-26", "capabilities": map[string]any{ "tools": map[string]any{}, }, "serverInfo": map[string]any{ "name": "mcp-vision", "version": "1.0.0", }, }) case "tools/list": respond(id, map[string]any{ "tools": []map[string]any{ { "name": "read_image", "description": "Describe an image file using AI vision. Sends only the image + prompt, no conversation context.", "inputSchema": map[string]any{ "type": "object", "properties": map[string]any{ "path": map[string]any{ "type": "string", "description": "Absolute path to the image file (jpg, png, webp)", }, "prompt": map[string]any{ "type": "string", "description": "Optional custom question about the image", }, }, "required": []string{"path"}, }, }, }, }) case "tools/call": params, _ := msg["params"].(map[string]any) name, _ := params["name"].(string) args, _ := params["arguments"].(map[string]any) if name != "read_image" { respondError(id, -32601, fmt.Sprintf("Unknown tool: %s", name)) continue } path, _ := args["path"].(string) prompt, _ := args["prompt"].(string) if prompt == "" { prompt = "Describe esta imagen en detalle." } result, err := readImage(path, prompt) if err != nil { respondError(id, -32000, err.Error()) continue } respond(id, map[string]any{ "content": []map[string]any{ {"type": "text", "text": result}, }, }) case "notifications/initialized": case "notifications/cancelled": } } } func readImage(path, prompt string) (string, error) { ext := strings.ToLower(filepath.Ext(path)) var mediaType string switch ext { case ".jpg", ".jpeg": mediaType = "image/jpeg" case ".png": mediaType = "image/png" case ".webp": mediaType = "image/webp" default: return "", fmt.Errorf("unsupported format: %s (use jpg, png, or webp)", ext) } data, err := os.ReadFile(path) if err != nil { return "", fmt.Errorf("cannot read file: %w", err) } b64 := base64.StdEncoding.EncodeToString(data) reqBody := map[string]any{ "model": "mimo-v2.5-free", "messages": []map[string]any{ { "role": "user", "content": []map[string]any{ {"type": "text", "text": prompt}, {"type": "image_url", "image_url": map[string]string{ "url": fmt.Sprintf("data:%s;base64,%s", mediaType, b64), }}, }, }, }, "stream": false, } body, _ := json.Marshal(reqBody) httpReq, err := http.NewRequest("POST", proxyBaseURL+"/chat/completions", bytes.NewReader(body)) if err != nil { return "", fmt.Errorf("request error: %w", err) } apiKey := os.Getenv("VISION_API_KEY") if apiKey == "" { apiKey = defaultAPIKey } // using proxy at proxyBaseURL httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+apiKey) httpReq.Header.Set("User-Agent", "mcp-vision/1.0") httpReq.Header.Set("x-opencode-client", "mcp-vision") httpReq.Header.Set("x-opencode-project", "vision") httpReq.Header.Set("x-opencode-request", fmt.Sprintf("req_%d", len(data))) httpReq.Header.Set("x-opencode-session", fmt.Sprintf("ses_%d", len(data)%1000000)) client := &http.Client{Timeout: 2 * 60} resp, err := client.Do(httpReq) if err != nil { return "", fmt.Errorf("api error: %w", err) } defer resp.Body.Close() respData, _ := io.ReadAll(resp.Body) if resp.StatusCode != 200 { return "", fmt.Errorf("api returned %d: %s", resp.StatusCode, string(respData)) } var result struct { Choices []struct { Message struct { Content any `json:"content"` } `json:"message"` } `json:"choices"` } if err := json.Unmarshal(respData, &result); err != nil { return "", fmt.Errorf("parse error: %w", err) } if len(result.Choices) == 0 { return "", fmt.Errorf("empty response") } switch c := result.Choices[0].Message.Content.(type) { case string: return c, nil default: b, _ := json.MarshalIndent(c, "", " ") return string(b), nil } } func respond(id any, result any) { resp := map[string]any{ "jsonrpc": "2.0", "id": id, "result": result, } writeJSON(resp) } func respondError(id any, code int, message string) { resp := map[string]any{ "jsonrpc": "2.0", "id": id, "error": map[string]any{ "code": code, "message": message, }, } writeJSON(resp) } func writeJSON(v any) { data, _ := json.Marshal(v) // Claude Code accepts newline-delimited JSON (same format as engram) os.Stdout.Write(data) os.Stdout.Write([]byte{'\n'}) }