serverpool

package
v2.7.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 27, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package serverpool manages a pool of MCP servers keyed by GitLab token and URL.

Each unique GitLab Personal Access Token and GitLab URL pair gets its own *mcp.Server, GitLab client, and server-scoped configuration snapshot with independently registered tools, resources, prompts, detected token scopes, and detected CE/EE edition. This provides zero cross-contamination between HTTP clients by construction: each client operates on its own server entry.

The pool has a configurable maximum size (WithMaxSize) and uses LRU eviction when the limit is reached. Token plus URL hashes (SHA-256) are used as pool keys so that raw tokens are never stored in memory.

The package also extracts GitLab tokens and per-request GitLab URLs from HTTP headers and includes an authentication-failure rate limiter for the HTTP MCP endpoint.

Isolation Model

HTTP requests are routed to per-identity server entries:

HTTP request
    |
    v
ExtractToken and ExtractGitLabURL
    |
    v
ServerPool.GetOrCreate
    |
    v
per-token, per-URL MCP server

This design keeps token scopes, edition detection, read-only mode, safe mode, tools, resources, and prompts isolated between concurrent HTTP clients.

Usage

Create a pool with New, retrieve or create servers with ServerPool.GetOrCreate, and extract tokens from HTTP requests with ExtractToken:

pool := serverpool.New(cfg, factory, serverpool.WithMaxSize(100))
defer pool.Close()

handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
    token := serverpool.ExtractToken(r)
    gitlabURL, err := serverpool.ExtractGitLabURL(r, cfg.GitLabURL)
    if err != nil {
        return nil
    }
    srv, err := pool.GetOrCreate(token, gitlabURL)
    if err != nil {
        return nil
    }
    return srv
}, opts)

Index

Constants

View Source
const DefaultIdleTimeout = 1 * time.Hour

DefaultIdleTimeout is how long an entry may go unused before the pool reclaims it. Without it an abandoned entry survives until enough distinct token+URL pairs push it out of the LRU, holding a fully registered server and drawing a revalidation ping against GitLab every interval, forever.

View Source
const DefaultRevalidateInterval = 15 * time.Minute

DefaultRevalidateInterval is the default period between token re-validation checks via a lightweight GitLab API call.

View Source
const RequestOptionGitLabURL = "GITLAB-URL"

RequestOptionGitLabURL identifies the per-request GitLab URL header option.

Variables

View Source
var ErrInvalidCredential = errors.New("gitlab rejected the credential")

ErrInvalidCredential reports that GitLab itself rejected the credential.

It is distinct from every other pool error: those mean the instance could not be reached or the server could not be built, whereas this one is a verdict from GitLab about the token. Callers map it to 401 rather than 503.

Functions

func ExtractBearerToken added in v2.7.4

func ExtractBearerToken(r *http.Request) string

ExtractBearerToken returns only the Authorization: Bearer credential, ignoring PRIVATE-TOKEN. OAuth mode uses it so the gate authenticates as the identity the SDK middleware verified, never as a PRIVATE-TOKEN the same request might also carry.

func ExtractGitLabURL

func ExtractGitLabURL(r *http.Request, defaultURL string) (string, error)

ExtractGitLabURL resolves the GitLab instance URL for an HTTP request. It is a compatibility wrapper around ResolveRequestOptions.

func ExtractToken

func ExtractToken(r *http.Request) string

ExtractToken retrieves the GitLab Personal Access Token from the HTTP request. It checks the following sources in order:

  1. PRIVATE-TOKEN header (GitLab standard)
  2. Authorization header with Bearer scheme

Returns the token string, or empty string if no token is found.

Types

type AuthRateLimiter

type AuthRateLimiter struct {
	// contains filtered or unexported fields
}

AuthRateLimiter tracks authentication failures per client IP and blocks clients that exceed the maximum failure count within the configured window.

func NewAuthRateLimiter

func NewAuthRateLimiter(maxFails int, window time.Duration) *AuthRateLimiter

NewAuthRateLimiter creates a rate limiter that blocks a client IP after maxFails authentication failures within the given time window.

func (*AuthRateLimiter) Cleanup

func (l *AuthRateLimiter) Cleanup()

Cleanup removes expired entries. Call periodically to prevent memory growth.

func (*AuthRateLimiter) IsBlocked

func (l *AuthRateLimiter) IsBlocked(ip string) bool

IsBlocked returns true if the IP has exceeded the failure limit within the window.

func (*AuthRateLimiter) RecordFailure

func (l *AuthRateLimiter) RecordFailure(ip string)

RecordFailure records an authentication failure for the given IP.

type InvalidGitLabURLError

type InvalidGitLabURLError struct {
	// URL is the offending URL value. It is retained for programmatic
	// inspection by callers but is deliberately omitted from [Error] output.
	URL    string
	Reason string
}

InvalidGitLabURLError is returned when the GITLAB-URL header contains an invalid URL. The raw URL value is intentionally not included in the error message to avoid leaking embedded credentials or sensitive query parameters into server logs.

func (*InvalidGitLabURLError) Error

func (e *InvalidGitLabURLError) Error() string

Error implements the [error] interface. The returned message contains only the validation InvalidGitLabURLError.Reason, never the raw URL, to avoid leaking credentials in logs.

type Metrics

type Metrics struct {
	Hits                   atomic.Int64
	Misses                 atomic.Int64
	Evictions              atomic.Int64
	IdleEvictions          atomic.Int64
	RevalidationsFailed    atomic.Int64
	RevalidationsSucceeded atomic.Int64
}

Metrics holds operational counters for the ServerPool. All counters are monotonically increasing and use lock-free atomic increments.

type Option

type Option func(*ServerPool)

Option configures pool behavior.

func WithBaseContext added in v2.7.5

func WithBaseContext(fn func() context.Context) Option

WithBaseContext ties entry construction to a lifetime the caller controls, normally the server's root context.

Without it the GitLab lookups that build an entry run under context.Background() and survive shutdown until their own timeout expires. They are deliberately not derived from the request that triggered them — see [ServerPool.baseContext] — but "not this request" is not the same as "no lifetime at all".

The signature mirrors net/http.Server.BaseContext: a function, so the pool never holds a context of its own. A nil function is ignored, and one that returns nil falls back to context.Background.

func WithIdleTimeout added in v2.7.0

func WithIdleTimeout(d time.Duration) Option

WithIdleTimeout sets how long an entry may go unused before the pool reclaims it. Values <= 0 disable idle eviction, leaving the LRU bound as the only reclamation path.

func WithMaxSize

func WithMaxSize(n int) Option

WithMaxSize sets the maximum number of unique token entries in the pool. Values ≤ 0 are ignored; the default is 100.

func WithRevalidateInterval

func WithRevalidateInterval(d time.Duration) Option

WithRevalidateInterval sets the interval between periodic token re-validation checks. Values ≤ 0 disable revalidation.

type RequestOptions

type RequestOptions struct {
	GitLabURL         string
	IgnoredOptions    []string
	DeprecatedOptions []string
}

RequestOptions contains the effective per-request options after applying server-wide MCP configuration precedence.

func ResolveRequestOptions

func ResolveRequestOptions(r *http.Request, defaultURL string) (RequestOptions, error)

ResolveRequestOptions applies server-wide MCP configuration precedence to the request-provided options. When defaultURL is set, it is authoritative and any GITLAB-URL header is ignored. When defaultURL is empty, a GITLAB-URL header selects the instance per request; if that header is also absent, the public GitLab instance (config.DefaultGitLabURL) is used, mirroring the stdio default so HTTP clients may omit the URL entirely. Effective URLs are normalized so equivalent values hash to the same server-pool session key.

func (RequestOptions) DeprecatedOptionsCopy

func (o RequestOptions) DeprecatedOptionsCopy() []string

DeprecatedOptionsCopy returns a defensive copy of deprecated ignored option names.

func (RequestOptions) HasDeprecatedOptions

func (o RequestOptions) HasDeprecatedOptions() bool

HasDeprecatedOptions reports whether any ignored request options are also deprecated compatibility options.

func (RequestOptions) HasIgnoredOptions

func (o RequestOptions) HasIgnoredOptions() bool

HasIgnoredOptions reports whether any request-provided options were ignored because server-wide MCP configuration is authoritative.

func (RequestOptions) IgnoredOptionsCopy

func (o RequestOptions) IgnoredOptionsCopy() []string

IgnoredOptionsCopy returns a defensive copy of the ignored option names.

type ServerFactory

type ServerFactory func(client *gitlabclient.Client, cfg *config.ServerConfig) (*mcp.Server, error)

ServerFactory creates a fully configured *mcp.Server with all tools, resources, and prompts registered for the given GitLab client and per-entry configuration. This is provided by the caller to decouple pool management from registration logic.

type ServerPool

type ServerPool struct {
	// contains filtered or unexported fields
}

ServerPool maintains a bounded set of *mcp.Server instances keyed by token plus GitLab URL hash (SHA-256). When the pool reaches maxSize, the least recently used entry is evicted. Entries are periodically re-validated against the GitLab API; entries with revoked tokens are evicted automatically.

func New

func New(cfg *config.Config, factory ServerFactory, opts ...Option) *ServerPool

New creates a ServerPool. The cfg provides shared server-wide settings (GitLabURL, SkipTLSVerify, etc.). The factory function creates a fully registered *mcp.Server for each new GitLab client.

func (*ServerPool) Close

func (p *ServerPool) Close()

Close removes all entries from the pool. Active MCP sessions for evicted servers are not forcefully terminated — they will expire naturally via [StreamableHTTPOptions.SessionTimeout].

func (*ServerPool) GetOrCreate

func (p *ServerPool) GetOrCreate(token, gitlabURL string) (*mcp.Server, error)

GetOrCreate returns the *mcp.Server for the given token and GitLab URL, creating one if it doesn't exist. The pool key is derived from both the token and gitlabURL, so the same token against different GitLab instances gets separate server entries. It is safe for concurrent use. Returns an error if the GitLab client or MCP server cannot be created.

func (*ServerPool) IdentityFor added in v2.7.5

func (p *ServerPool) IdentityFor(token, gitlabURL string) (UserIdentity, bool)

IdentityFor returns the GitLab user behind a pooled credential, and whether the pool holds an entry for it at all.

Reading rather than resolving is the point: the answer was determined when the entry was built, so a request costs a map lookup. A caller that gets ok=false has asked before ServerPool.GetOrCreate ran for this credential.

func (*ServerPool) Size

func (p *ServerPool) Size() int

Size returns the current number of entries in the pool.

func (*ServerPool) StartIdleEviction added in v2.7.0

func (p *ServerPool) StartIdleEviction(ctx context.Context)

StartIdleEviction launches a background goroutine that reclaims entries unused for longer than the configured idle timeout. Cancel the context to stop it. It is a no-op when idle eviction is disabled.

Idle eviction runs independently of revalidation so that disabling one does not silently disable the other. It also needs no network I/O: an idle entry is dropped on its timestamp alone, which is the point — the entries it reclaims are exactly the ones revalidation would otherwise keep pinging GitLab about on behalf of a client that is gone.

func (*ServerPool) StartRevalidation

func (p *ServerPool) StartRevalidation(ctx context.Context)

StartRevalidation launches a background goroutine that periodically checks all pool entries for token validity using a lightweight GitLab API call. Entries that fail validation are evicted. Cancel the context to stop.

func (*ServerPool) Stats

func (p *ServerPool) Stats() Snapshot

Stats returns a point-in-time Snapshot of pool metrics and state.

type Snapshot

type Snapshot struct {
	Hits                   int64     `json:"hits"`
	Misses                 int64     `json:"misses"`
	Evictions              int64     `json:"evictions"`
	IdleEvictions          int64     `json:"idle_evictions"`
	RevalidationsFailed    int64     `json:"revalidations_failed"`
	RevalidationsSucceeded int64     `json:"revalidations_succeeded"`
	CurrentSize            int       `json:"current_size"`
	MaxSize                int       `json:"max_size"`
	CreatedAt              time.Time `json:"created_at"`
}

Snapshot is a point-in-time copy of pool Metrics plus current state. Safe for JSON serialization and cross-goroutine use.

type UserIdentity added in v2.7.5

type UserIdentity struct {
	UserID   string
	Username string
}

UserIdentity is the GitLab user a pooled credential belongs to.

It is resolved once when the entry is built, alongside tier and scope discovery, and then answers for every request that reuses the entry. The zero value means the lookup did not succeed — an instance that refuses /user to this token, say — which callers must treat as "unknown", never as "anonymous".

func (UserIdentity) Resolved added in v2.7.5

func (u UserIdentity) Resolved() bool

Resolved reports whether the identity was actually determined.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL