Files
MCP/pkg/gitea/gitea.go
T
Lunny Xiao 6744cab627 feat(gitea): support extra outbound HTTP headers via GITEA_EXTRA_HEADERS
Add a GITEA_EXTRA_HEADERS environment variable that accepts a JSON
object of header name/value pairs (e.g. Cloudflare Access
credentials) and applies them to every outbound request to Gitea,
both the raw pkg/gitea.DoJSON/DoBytes path and the SDK-backed
pkg/gitea.NewClient path, without overriding Authorization,
Content-Type, or Accept.

Co-Authored-By: Codet <codet@commitgo.dev> (GPT-5-Codex)
2026-08-23 23:33:36 -07:00

109 lines
3.0 KiB
Go

package gitea
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/http"
"sync"
mcpContext "gitea.com/gitea/gitea-mcp/pkg/context"
"gitea.com/gitea/gitea-mcp/pkg/flag"
"gitea.dev/sdk"
)
var (
clientCache sync.Map // token -> *gitea.Client
sharedTransOnce sync.Once
sharedTrans *http.Transport
)
func sharedTransport() *http.Transport {
sharedTransOnce.Do(func() {
sharedTrans = http.DefaultTransport.(*http.Transport).Clone()
if flag.Insecure {
sharedTrans.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec // user-requested insecure mode
}
})
return sharedTrans
}
// extraHeaderTransport injects flag.ExtraHeaders into every request, without
// overriding headers the caller already set (e.g. Authorization, Content-Type,
// Accept). It reads flag.ExtraHeaders on each round trip rather than caching
// it, so tests can change it between requests.
type extraHeaderTransport struct {
base http.RoundTripper
}
func (t *extraHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
headers := flag.ExtraHeaders
if len(headers) == 0 {
return t.base.RoundTrip(req)
}
cloned := req.Clone(req.Context())
for name, values := range headers {
if cloned.Header.Get(name) == "" {
cloned.Header[name] = values
}
}
return t.base.RoundTrip(cloned)
}
func giteaTransport() http.RoundTripper {
return &extraHeaderTransport{base: sharedTransport()}
}
// NewClient returns a cached *gitea.Client keyed by host+token. The SDK's per-client
// version cache and the shared transport let us reuse keep-alive connections
// and avoid the SDK's /api/v1/version preflight on every tool call.
func NewClient(token string) (*gitea.Client, error) {
key := flag.Host + "\x00" + token
if v, ok := clientCache.Load(key); ok {
return v.(*gitea.Client), nil
}
httpClient := &http.Client{
Transport: giteaTransport(),
CheckRedirect: checkRedirect,
}
opts := []gitea.ClientOption{
gitea.SetToken(token),
gitea.SetHTTPClient(httpClient),
}
if flag.Debug {
opts = append(opts, gitea.SetDebugMode())
}
client, err := gitea.NewClient(flag.Host, opts...)
if err != nil {
return nil, fmt.Errorf("create gitea client err: %w", err)
}
client.SetUserAgent("gitea-mcp-server/" + flag.Version)
actual, _ := clientCache.LoadOrStore(key, client)
return actual.(*gitea.Client), nil
}
// checkRedirect prevents Go from silently changing mutating requests (POST, PATCH, etc.)
// to GET when following 301/302/303 redirects, which would drop the request body and
// make writes appear to succeed when they didn't.
func checkRedirect(_ *http.Request, via []*http.Request) error {
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
if via[0].Method != http.MethodGet && via[0].Method != http.MethodHead {
return http.ErrUseLastResponse
}
return nil
}
func ClientFromContext(ctx context.Context) (*gitea.Client, error) {
token, ok := ctx.Value(mcpContext.TokenContextKey).(string)
if !ok {
token = flag.Token
}
return NewClient(token)
}