mcpaggregate

package
v0.2.4 Latest Latest
Warning

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

Go to latest
Published: Sep 16, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Overview

Package mcpaggregate serves the hub's always-on aggregate MCP endpoint.

This is a base-layer capability of the hub: the endpoint is mounted unconditionally at apiurl.PathPrefixMCPServer and always answers, even when no providers are registered (it just serves an empty tool list). It never depends on edges — edges are a first-class provider that federates its tools in exactly like every other provider (kuery, code, infrastructure, …).

Per request the handler parses the tenant cluster + MCPServer name out of the path, verifies the caller's bearer against that tenant (see BearerVerifier), builds a fresh stateless mcp.Server, federates the /mcp endpoint of every Ready provider visible to the verified tenant (see ProviderEnumerator) into it, and serves the MCP protocol over streamable HTTP. Providers' discovered tools and instructions are cached per caller (see discovery.go), so only the first request of a caller waits on provider discovery. Nothing is federated for a bearer that fails verification.

Index

Constants

View Source
const (
	// DefaultDiscoveryCacheTTL is how long a provider's discovered tools and
	// instructions are served without a background refresh.
	DefaultDiscoveryCacheTTL = 30 * time.Second
)
View Source
const DefaultVerifyCacheTTL = 60 * time.Second

DefaultVerifyCacheTTL is how long a successful bearer verification — and the tenant context it resolved — is reused for the same (bearer, cluster, MCPServer) before it is re-checked.

View Source
const MCPIdentityNamespace = "default"

MCPIdentityNamespace is the tenant-workspace namespace the per-MCPServer ServiceAccount and token Secret live in. The mcpserver controller provisions the identity here and the aggregate handler verifies bearers against it, so the two sides share one definition.

Variables

View Source
var (
	// ErrUnauthenticated means the bearer is not a credential the hub or the
	// tenant cluster recognises.
	ErrUnauthenticated = errors.New("bearer is not authenticated")
	// ErrForbidden means the bearer is a valid credential but not for the
	// tenant cluster and MCPServer addressed by the request.
	ErrForbidden = errors.New("bearer is not authorized for this MCPServer")
)

PathPrefix is the router prefix this handler mounts under. Re-exported for the hub server wiring so the prefix and the handler live together.

Functions

func FederatedInstructions

func FederatedInstructions(ctx context.Context, targets []ProviderTarget, bearerToken, cluster string) string

FederatedInstructions fetches each provider's server-level MCP instructions (from `initialize`) in parallel and returns a merged block to append to the aggregate's own instructions — so operator-authored provider guidance (e.g. a Home Assistant Service's spec.instructions describing its entity layout) reaches the model connecting to the aggregate, not just the provider's direct endpoint. Providers with no instructions or an error contribute nothing. Enumeration order is preserved for deterministic output.

func New

func New(opts Options) http.Handler

New returns the http.Handler mounted at apiurl.PathPrefixMCPServer. The handler expects the prefix to have been stripped, so it sees /{cluster}/apis/railgrid.ai/v1alpha1/mcpservers/{name}/mcp.

func ServiceAccountName

func ServiceAccountName(mcpServerName string) string

ServiceAccountName returns the name of the ServiceAccount the mcpserver controller provisions for the named MCPServer.

func ServiceAccountUsername

func ServiceAccountUsername(mcpServerName string) string

ServiceAccountUsername returns the Kubernetes username a TokenReview reports for the named MCPServer's ServiceAccount token.

func TenantFromPath

func TenantFromPath(path string) (orgUUID, wsUUID string, ok bool)

TenantFromPath splits a tenant workspace path into its Organization UUID and optional child Workspace UUID. Paths outside root:railgrid:tenants, or nested deeper than one child workspace, are not tenant workspaces users can hold Memberships in.

Types

type BearerVerifier

type BearerVerifier interface {
	Verify(r *http.Request, token, cluster, mcpServerName string) (Caller, error)
}

BearerVerifier decides whether the bearer presented on an aggregate MCP request may act in the tenant cluster and MCPServer named by the request path. It runs before any federation, so a rejected bearer never reaches a provider.

Implementations return the verified Caller and a nil error to allow, ErrUnauthenticated when the bearer is not a credential the hub recognises, ErrForbidden when it is a valid credential for a different tenant or identity, and any other error when verification itself could not be performed (the handler answers 503).

type BearerVerifierFunc

type BearerVerifierFunc func(r *http.Request, token, cluster, mcpServerName string) (Caller, error)

BearerVerifierFunc adapts a function to BearerVerifier.

func (BearerVerifierFunc) Verify

func (f BearerVerifierFunc) Verify(r *http.Request, token, cluster, mcpServerName string) (Caller, error)

Verify implements BearerVerifier.

type Caller

type Caller struct {
	// OrgUUID and WorkspaceUUID are the tenant the request's cluster belongs
	// to. WorkspaceUUID is empty for an org-scope cluster; both are empty for
	// a ServiceAccount bearer in a cluster outside the tenants tree, which
	// federates the platform catalog only.
	OrgUUID       string
	WorkspaceUUID string
	// User is the hub User name for a human bearer whose membership in the
	// tenant was verified. Empty for a ServiceAccount bearer.
	User string
	// ServiceAccount is the TokenReview username for a ServiceAccount bearer.
	// Empty for a human.
	ServiceAccount string
}

Caller is the verified tenant context of one aggregate request. Only a BearerVerifier produces it — from the cluster named in the request path, the cluster's own kcp.io/path, and an online check of the bearer — never from anything the client asserts in headers. It is what the ProviderEnumerator scopes federation to.

func (Caller) IsServiceAccount

func (c Caller) IsServiceAccount() bool

IsServiceAccount reports whether the bearer was a ServiceAccount token. Such a caller has no human to delegate for, so org-owned providers are never federated for it (see RegistryEnumerator).

type FederatedProvider

type FederatedProvider struct {
	Name        string          `json:"name"`
	DisplayName string          `json:"displayName,omitempty"`
	MCPURL      string          `json:"mcpURL"`
	Reachable   bool            `json:"reachable"`
	Error       string          `json:"error,omitempty"`
	Tools       []FederatedTool `json:"tools"`
	// contains filtered or unexported fields
}

FederatedProvider is the introspection view of one federation target: a Ready provider, whether its MCP endpoint answered, and the tools it advertised. It is what the portal renders so a user can see what the aggregate is federating live, without connecting an MCP client.

func DiscoverFederation

func DiscoverFederation(ctx context.Context, targets []ProviderTarget, bearerToken, cluster string) []FederatedProvider

DiscoverFederation runs the same concurrent tools/list discovery the aggregate performs at request-build time, but returns a structured snapshot instead of registering proxy tools. It never fails as a whole: an unreachable provider is reported with Reachable=false and a populated Error, so the UI can show partial state. Providers are returned in enumeration order (deterministic).

type FederatedTool

type FederatedTool struct {
	Name        string `json:"name"`
	Title       string `json:"title,omitempty"`
	Description string `json:"description,omitempty"`
}

FederatedTool is one tool advertised by a federated provider. Names are the provider-local names; the aggregate prefixes them "<provider>__" when it proxies, but introspection shows the raw names the provider reports.

type Options

type Options struct {
	// Providers enumerates the live Ready providers to federate for a
	// verified caller. Required.
	Providers ProviderEnumerator
	// ExternalURL is the hub's externally reachable base URL, used only to
	// self-describe the endpoint in the railgrid://about resource. Optional.
	ExternalURL string
	// Logger is used for federation diagnostics. Optional.
	Logger logr.Logger
	// Verifier checks the bearer against the tenant cluster and MCPServer
	// named by the request before anything is federated. Required: without
	// one every request is refused with 503 rather than forwarded unverified.
	Verifier BearerVerifier
	// VerifyCacheTTL bounds how long a successful verification is reused for
	// the same bearer, cluster and MCPServer. Defaults to DefaultVerifyCacheTTL.
	VerifyCacheTTL time.Duration
	// RateLimiter, when set, caps uncached verification attempts per client
	// address; cache hits are never counted so verified clients are not
	// throttled. Optional.
	RateLimiter RateLimiter
	// ClientIP derives the address the rate limiter keys on. Defaults to the
	// host part of RemoteAddr; the hub passes its proxy-header-aware helper.
	ClientIP func(*http.Request) string
	// DiscoveryCacheTTL is how long a provider's discovered tools and
	// instructions are served before a background refresh (see discovery.go).
	// Defaults to DefaultDiscoveryCacheTTL; negative disables the cache, so
	// every request discovers every provider afresh.
	DiscoveryCacheTTL time.Duration
}

Options configures the aggregate handler.

type OrgProviderRouter

type OrgProviderRouter interface {
	OrgProviderRoute(ctx context.Context, prov providers.Provider, caller providers.DelegatedCaller) (providers.OrgProviderRoute, error)
}

OrgProviderRouter resolves how a hub-originated request reaches an org-owned provider's backend on behalf of a caller. The hub's backend proxy (*providers.ProviderProxy) implements it, so the aggregate reuses the exact edge hop and delegated-token substitution that /services/providers/{name} applies, rather than a second copy of that boundary.

type ProviderEnumerator

type ProviderEnumerator func(ctx context.Context, caller Caller) []ProviderTarget

ProviderEnumerator returns the live set of Ready providers exposing an MCP endpoint that the verified caller may see. Called once per MCP request from buildServer with the Caller its BearerVerifier returned, never with anything taken from request headers.

func RegistryEnumerator

func RegistryEnumerator(reg *providers.Registry, router OrgProviderRouter, log logr.Logger) ProviderEnumerator

RegistryEnumerator returns the ProviderEnumerator the hub wires into the aggregate endpoint and the MCPServer status controller.

What a caller sees is the catalog of the Org its bearer was verified against, and nothing else:

  • The provider set is reg.ListForOrg(caller.OrgUUID): every platform provider plus that Org's own. Another Org's providers are never listed, so their tools cannot appear and their backends are never contacted. An empty OrgUUID lists the platform catalog only — never every org's.
  • An org-owned provider shadows the platform provider of the same name (ListForOrg drops the platform copy), matching what /services/providers/{name} routes to for that Org. The two never both register "<name>__*" tools, and when the org copy cannot be federated for this caller the platform copy does NOT come back in its place: the Org replaced it, so its tools would act on the wrong backend.
  • A platform provider is federated exactly as before: its backend URL, dialled directly with the caller's bearer.
  • An org-owned provider is federated only through router: over the platform edges tunnel, with a delegated token minted for the verified (org, workspace, user) in place of the caller's bearer. Where no such token can be minted — a ServiceAccount bearer (no human to delegate for), an org-scope cluster (no team workspace to mint in), no issuer, an unusable edge route — the provider is skipped for this request and logged at V(1). It is never reached with the caller's bearer.

Output is sorted by provider name so the aggregate's tool list is stable across requests.

type ProviderTarget

type ProviderTarget struct {
	Name        string
	DisplayName string
	MCPURL      string

	// OrgUUID is the owning Org of an org-owned ("bring your own") provider;
	// empty for a platform provider. An org-owned target is only ever reached
	// through Transport: one without a Transport is not contacted at all.
	OrgUUID string
	// Transport, when set, carries every request to this target and owns its
	// authorization: the federation client never attaches the caller's bearer
	// to a request that goes through it. Org-owned targets get one from
	// providers.ProviderProxy.OrgProviderRoute (edge hop + delegated token).
	// Nil for a platform provider, which is dialled directly with the caller's
	// bearer as before.
	Transport http.RoundTripper
}

ProviderTarget is one federation target: a Ready provider and the URL of its own MCP endpoint.

type RateLimiter

type RateLimiter interface {
	Allow(clientIP string) bool
}

RateLimiter admits or rejects a pre-authentication verification attempt for one client address. The hub's token-login limiter satisfies it.

type Verifier

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

Verifier is the production BearerVerifier. It accepts three kinds of bearer:

  1. The MCPServer's own ServiceAccount token, which the mcpserver controller mints and the portal / REST API hand to the user for long-lived MCP client configuration. It is verified online with a TokenReview in the tenant cluster named by the request, and the reviewed username must be ServiceAccountUsername(name) for the MCPServer named by the request.
  2. A hub user bearer (static token or OIDC id_token) as resolved by the hub's normal identity path; the CLI's `railgrid mcp` and the e2e suites use these. The user must hold a live Membership covering the tenant the cluster belongs to, per the UserMembershipIndex.
  3. Other tenant ServiceAccounts with explicit RBAC permission to use the named MCPServer. TokenReview authenticates them before SubjectAccessReview authorizes access; platform providers still receive the original bearer.

Signatures and claims are never trusted offline; both paths are online checks against kcp. Either way the result carries the tenant (Org and Workspace) the cluster belongs to, which is what federation is scoped to.

func NewVerifier

func NewVerifier(clusterConfig func(cluster string) *rest.Config, opts ...VerifierOption) *Verifier

NewVerifier builds a Verifier. clusterConfig must return a rest.Config whose Host addresses the named tenant cluster (apiurl.KCPClusterURL); returning nil makes every verification fail closed.

func (*Verifier) SetUserIdentity

func (v *Verifier) SetUserIdentity(
	identify func(*http.Request) (string, error),
	membership func(ctx context.Context, user string) (*tenancyv1alpha1.UserMembershipIndex, error),
)

SetUserIdentity enables the hub-user branch. identify resolves a request's bearer to a User name (KCPProxy.IdentifyUser); membership reads that user's UserMembershipIndex. Until both are set only MCPServer ServiceAccount tokens are accepted.

func (*Verifier) Verify

func (v *Verifier) Verify(r *http.Request, token, cluster, mcpServerName string) (Caller, error)

Verify implements BearerVerifier.

type VerifierOption

type VerifierOption func(*Verifier)

VerifierOption customises a Verifier.

func WithClusterPathResolver

func WithClusterPathResolver(f func(ctx context.Context, cluster string) (string, error)) VerifierOption

WithClusterPathResolver overrides how a logical-cluster ID is mapped to its workspace path.

func WithKubeClientFactory

func WithKubeClientFactory(f func(*rest.Config) (kubernetes.Interface, error)) VerifierOption

WithKubeClientFactory overrides how the TokenReview client is built.

Jump to

Keyboard shortcuts

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