Files
free-ide-proxy/cmd/mcp-vision/main.go
T
renato97 0a4289cafc Initial commit: OpenCode Zen Proxy
Proxy translating OpenAI/Anthropic Messages API to OpenCode Zen's free models.
Pure stdlib Go 1.22, no external dependencies.

- OpenAI Chat Completions (/v1/chat/completions) with streaming
- Anthropic Messages API (/v1/messages) with streaming
- Model aliases for 5 free models
- Auth middleware support
- Session rotation (30min per user key)
2026-06-25 11:49:11 -03:00

270 lines
6.2 KiB
Go

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