mirror of
https://gitea.com/gitea/gitea-mcp.git
synced 2026-08-27 02:27:45 +00:00
6744cab627
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)
109 lines
3.0 KiB
Go
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)
|
|
}
|