Files
sherlock/cmd/gitea-mcp/client.go
T
amacocian f5109abfad
CI / build (push) Successful in 19s
Gitea mcp pre-release
2026-05-25 15:22:30 +02:00

94 lines
2.6 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
// giteaAPI is a thin wrapper around the few Gitea REST endpoints we
// use. We don't pull in code.gitea.io/sdk/gitea because its client
// only knows how to send `Authorization: token <PAT>`, not the
// `Authorization: Bearer <OAuth>` that Gitea's OAuth2 server issues.
type giteaAPI struct {
baseURL string
token string
client *http.Client
}
// get does a GET against path (with optional query) and JSON-decodes
// into into.
func (g *giteaAPI) get(ctx context.Context, path string, query url.Values, into any) error {
full := g.baseURL + path
if len(query) > 0 {
full += "?" + query.Encode()
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, full, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+g.token)
req.Header.Set("Accept", "application/json")
resp, err := g.client.Do(req)
if err != nil {
return err
}
defer func() { _, _ = io.Copy(io.Discard, resp.Body); _ = resp.Body.Close() }()
if err := checkStatus(resp, req); err != nil {
return err
}
if into == nil {
return nil
}
return json.NewDecoder(resp.Body).Decode(into)
}
// getRaw returns the raw body (up to maxBytes; anything beyond is
// silently dropped). Useful for endpoints that return plain text or
// binary (e.g. job logs).
func (g *giteaAPI) getRaw(ctx context.Context, path string, query url.Values, maxBytes int64) ([]byte, bool, error) {
full := g.baseURL + path
if len(query) > 0 {
full += "?" + query.Encode()
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, full, nil)
if err != nil {
return nil, false, err
}
req.Header.Set("Authorization", "Bearer "+g.token)
req.Header.Set("Accept", "*/*")
resp, err := g.client.Do(req)
if err != nil {
return nil, false, err
}
defer func() { _ = resp.Body.Close() }()
if err := checkStatus(resp, req); err != nil {
return nil, false, err
}
body, err := io.ReadAll(io.LimitReader(resp.Body, maxBytes+1))
if err != nil {
return nil, false, err
}
truncated := false
if int64(len(body)) > maxBytes {
body = body[:maxBytes]
truncated = true
}
return body, truncated, nil
}
func checkStatus(resp *http.Response, req *http.Request) error {
if resp.StatusCode == http.StatusUnauthorized {
return fmt.Errorf("gitea: 401 (token rejected by %s; try `sherlock logout gitea` and retry)", req.URL.Host)
}
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return fmt.Errorf("gitea: %s %s: HTTP %d: %s", req.Method, req.URL.Path, resp.StatusCode, strings.TrimSpace(string(body)))
}
return nil
}