gateway

package
v1.61.4 Latest Latest
Warning

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

Go to latest
Published: May 14, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package gateway provides an MCP gateway toolkit that proxies tools from an upstream MCP server through the platform's auth, persona, and audit pipeline.

Index

Constants

View Source
const (
	// Kind is the toolkit kind identifier. Each connection of this kind is
	// a remote MCP server that the platform's gateway feature proxies. The
	// kind value is what operators see in the connection picker; the
	// gateway terminology is reserved for the platform-side feature
	// (admin endpoints, internal package, DB tables) that does the proxying.
	Kind = "mcp"

	// AuthModeNone disables outbound authentication.
	AuthModeNone = "none"
	// AuthModeBearer sends "Authorization: Bearer <credential>" on upstream requests.
	AuthModeBearer = "bearer"
	// AuthModeAPIKey sends "X-API-Key: <credential>" on upstream requests.
	AuthModeAPIKey = "api_key"
	// AuthModeOAuth acquires a bearer token via an OAuth 2.1 grant
	// (client_credentials in v1) and refreshes it before expiry.
	AuthModeOAuth = "oauth"

	// OAuthGrantClientCredentials is the machine-to-machine grant type.
	// Requires a token URL, client id, and client secret. No user
	// interaction — the platform exchanges the credentials for a token on
	// behalf of all platform users.
	OAuthGrantClientCredentials = "client_credentials"

	// OAuthGrantAuthorizationCode is the user-driven grant. The operator
	// completes a one-time browser flow at setup; the resulting refresh
	// token is persisted (encrypted) so subsequent platform restarts
	// and background workloads keep working without further interaction.
	OAuthGrantAuthorizationCode = "authorization_code"

	// TrustLevelUntrusted is the default. Upstream responses are treated as
	// untrusted content (reserved for future enforcement).
	TrustLevelUntrusted = "untrusted"
	// TrustLevelTrusted bypasses future content sanitization. Use only for
	// first-party upstreams under the operator's control.
	TrustLevelTrusted = "trusted"

	// DefaultConnectTimeout is the default timeout for initial upstream connection + tool discovery.
	DefaultConnectTimeout = 10 * time.Second
	// DefaultCallTimeout is the default per-tool-call timeout for upstream forwarding.
	DefaultCallTimeout = 60 * time.Second

	// NamespaceSeparator joins the connection name and remote tool name (e.g. "crm__get_contact").
	NamespaceSeparator = "__"
)
View Source
const (
	OAuthAuthStyleHeader = "header"
	OAuthAuthStyleParams = "params"
)

OAuthAuthStyle values for OAuthConfig.EndpointAuthStyle.

View Source
const (

	// LogKeyTokenURLHost is the structured-log field name used when
	// emitting an IdP host. Exported so external packages don't
	// duplicate the literal and risk drift.
	LogKeyTokenURLHost = "token_url_host" // #nosec G101 -- structured-log key name, not a credential

	// LogKeyGrantType is the structured-log field name used when
	// emitting an OAuth grant_type. Exported so the admin handler
	// (which performs the authorization_code exchange) and the
	// gateway token source (which performs refresh / acquire) emit
	// the same field name across the full OAuth lifecycle.
	LogKeyGrantType = "grant_type"
)

Log key constants keep structured-slog field names consistent across the package — and across packages that compose with this one. LogKeyTokenURLHost is exported so the admin handler that performs the OAuth code-exchange (which lives outside this package) can use the same field name as the token-source's refresh / acquire logs, allowing operators to grep one connection's full auth lifecycle by `token_url_host=<host>`.

Variables

View Source
var ErrConnectionExists = errors.New("gateway: connection already exists")

ErrConnectionExists is returned when AddConnection is called with a name already live in the toolkit.

View Source
var ErrConnectionNotFound = errors.New("gateway: connection not found")

ErrConnectionNotFound is returned when RemoveConnection is called for a name that is not present.

View Source
var ErrConnectionNotLive = errors.New("gateway: connection has no live client")

ErrConnectionNotLive is returned by TestLiveConnection when the named connection is registered but does not have a live client (typically an authorization_code OAuth connection awaiting the operator's first Connect, or a connection whose initial dial failed and is sitting as a placeholder). Callers convert this to an actionable user-facing message rather than a generic upstream error.

Functions

func URLHost added in v1.57.4

func URLHost(u string) string

URLHost returns the host portion of a URL, falling back to the raw input when net/url cannot parse it. Exported because the legacy admin gateway_oauth_handler emits structured log lines with the same host-only audit field; sharing this helper keeps both call sites' log shape identical.

Types

type Config

type Config struct {
	// Endpoint is the streamable HTTP URL of the upstream MCP server. Required.
	Endpoint string
	// AuthMode is "none", "bearer", "api_key", or "oauth".
	AuthMode string
	// Credential is the bearer token or API key. Ignored when AuthMode is "none" or "oauth".
	Credential string
	// OAuth carries the OAuth-specific configuration used when AuthMode is "oauth".
	OAuth OAuthConfig
	// ConnectionName is the audit-visible connection identifier and also the
	// tool-name prefix. Defaults to the toolkit instance name when unset.
	ConnectionName string
	// ConnectTimeout caps the initial connection + ListTools call.
	ConnectTimeout time.Duration
	// CallTimeout caps each forwarded tool invocation.
	CallTimeout time.Duration
	// TrustLevel is "untrusted" (default) or "trusted".
	TrustLevel string
}

Config holds gateway toolkit configuration for a single upstream MCP connection.

func ParseConfig

func ParseConfig(cfg map[string]any) (Config, error)

ParseConfig parses a gateway configuration from a map.

func (Config) Validate

func (c Config) Validate() error

Validate returns an error if the configuration is missing required fields or contains invalid values.

type ConnectionStatus

type ConnectionStatus struct {
	Name     string                 `json:"name"`
	Healthy  bool                   `json:"healthy"`
	AuthMode string                 `json:"auth_mode"`
	Tools    []string               `json:"tools,omitempty"`
	OAuth    *connoauth.OAuthStatus `json:"oauth,omitempty"`
}

ConnectionStatus is a per-connection health snapshot exposed by the admin status endpoint.

type IngestOAuthTokenInput

type IngestOAuthTokenInput struct {
	Name             string
	AccessToken      string
	RefreshToken     string
	ExpiresIn        int
	RefreshExpiresIn int
	Scope            string
	AuthenticatedBy  string
}

IngestOAuthTokenInput is the parameter set for IngestOAuthToken. Defined as a struct to keep the public method's argument list under the project's revive limit.

RefreshExpiresIn is optional: only some IdPs (Keycloak) emit it on their token-endpoint response. Zero leaves the refresh deadline unknown — the admin UI renders an em dash.

type MultiConfig

type MultiConfig struct {
	DefaultName string
	Instances   map[string]Config
}

MultiConfig holds one or more parsed per-connection gateway configs along with the aggregate toolkit's default connection name.

func ParseMultiConfig

func ParseMultiConfig(defaultName string, raw map[string]map[string]any) (MultiConfig, error)

ParseMultiConfig validates and returns the parsed config for every instance. Per-instance parse errors are surfaced as fatal (operator must fix the config); dial/connectivity errors are handled at load time in NewMulti, not here.

type OAuthConfig

type OAuthConfig struct {
	// Grant selects the OAuth flow. One of "client_credentials" or
	// "authorization_code".
	Grant string
	// TokenURL is the upstream's OAuth token endpoint.
	TokenURL string
	// AuthorizationURL is the upstream's authorization endpoint. Only
	// used by the authorization_code grant; the platform redirects the
	// admin's browser here to start the flow.
	AuthorizationURL string
	// ClientID is the platform's registered client id with the upstream.
	ClientID string
	// ClientSecret is the platform's registered client secret. Encrypted
	// at rest (same field-level encryption as Credential).
	ClientSecret string
	// Scope is the operator-supplied space-delimited scope string,
	// sent to the IdP verbatim. Empty scope is permitted — most IdPs
	// apply a client-default scope when none is requested. Operators
	// who need long-lived refresh tokens (refresh that survives
	// platform restarts beyond the IdP's SSO Session Idle window)
	// must add the IdP-specific offline-token scope themselves:
	// `offline_access` for Keycloak/Auth0/Okta, `refresh_token` for
	// Salesforce. The platform does NOT inject any scope automatically
	// — operators' input is the source of truth.
	Scope string
	// Prompt is an optional OIDC prompt parameter (RFC OIDC §3.1.2.1).
	// When non-empty, the platform appends ?prompt=<value> to the
	// authorize URL on every browser-side flow start. Common values:
	//
	//   "login"          — force fresh credential prompt every time
	//                      (defeats stale-Keycloak-form bugs; correct
	//                      posture for admin Reconnect flows).
	//   "consent"        — force the consent screen.
	//   "select_account" — force account picker.
	//   "none"           — silent auth; fail if interaction needed.
	//   ""  (default)    — emit no prompt parameter; let the IdP
	//                      decide. Required for non-OIDC OAuth providers
	//                      that reject unknown parameters with
	//                      invalid_request.
	//
	// Operators of strict OIDC realms (Keycloak, Auth0, Okta) typically
	// set this to "login" for connections an admin holds. Operators
	// running pure-OAuth providers should leave it empty.
	Prompt string
	// EndpointAuthStyle controls how the client credentials are
	// transmitted to the token endpoint. One of:
	//
	//   "header" (default) — HTTP Basic on the token request; the
	//                        OAuth 2.1 recommended style.
	//   "params"           — form-body parameters; required by some
	//                        legacy / strict-spec IdPs (older
	//                        Keycloak, some self-hosted authorization
	//                        servers).
	//
	// Empty defaults to "header". Mirrors the apigateway toolkit's
	// `oauth2_endpoint_auth_style`.
	EndpointAuthStyle string
}

OAuthConfig describes the OAuth 2.1 parameters used when AuthMode is "oauth". The grant determines whether the token is acquired from machine-to-machine credentials or via a one-time browser flow that hands the platform a refresh token for long-running background use.

type OAuthKindHandler added in v1.60.1

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

OAuthKindHandler adapts the MCP gateway toolkit to the unified connoauth flow. The admin layer's unified OAuth handler dispatches on the connection_kind path parameter ("mcp" for gateway connections) and invokes this implementation for config extraction and post-auth side effects.

The MCP gateway's post-auth side effect is to re-add the connection to the running toolkit so its discovered tool surface is registered with the platform's MCP server. Without that step, the operator completes Connect, the token persists, but the platform's tool list still treats the connection as "needs auth" until the next process restart.

func NewOAuthKindHandler added in v1.60.1

func NewOAuthKindHandler(toolkit *Toolkit) *OAuthKindHandler

NewOAuthKindHandler wires the MCP gateway toolkit into the unified OAuth dispatch. Returns nil when toolkit is nil so callers can register conditionally without a nil-check at every callsite.

func (*OAuthKindHandler) AfterConnect added in v1.60.1

func (h *OAuthKindHandler) AfterConnect(_ context.Context, name string, connConfig map[string]any) error

AfterConnect rebuilds the connection on the live toolkit so tools register against the freshly-authorized upstream. Idempotent: if the connection already exists, RemoveConnection + AddConnection is safe (the in-memory state is replaced atomically). Errors are logged and returned so the admin layer can surface them — but the admin handler treats the error as non-fatal (token is already persisted; the toolkit's next reconciliation will retry).

func (*OAuthKindHandler) ParseOAuthConfig added in v1.60.1

func (*OAuthKindHandler) ParseOAuthConfig(connConfig map[string]any) (connoauth.Config, error)

ParseOAuthConfig validates the connection's stored config and maps the MCP gateway's per-kind OAuth shape into a connoauth.Config. Returns an error when the connection is not configured for the authorization_code grant — the unified handler maps that to HTTP 409 Conflict, matching the prior per-kind handler's response code.

type ProbeTool

type ProbeTool struct {
	Name        string `json:"name"`
	LocalName   string `json:"local_name"`
	Description string `json:"description,omitempty"`
}

ProbeTool is a summary of a single discovered upstream tool, used by the admin test endpoint to preview what a connection would expose.

func Probe

func Probe(ctx context.Context, cfg Config) ([]ProbeTool, error)

Probe dials the upstream described by cfg, lists its tools, and closes the session. It does NOT mutate any live toolkit state, so it's safe to call from the admin "test connection" endpoint before persisting a config.

type ToolListChangedNotifier added in v1.58.0

type ToolListChangedNotifier interface {
	NotifyToolsListChanged(ctx context.Context)
}

ToolListChangedNotifier publishes a tools/list_changed signal to downstream MCP clients. The gateway calls this whenever its aggregate tool inventory changes (a connection comes up after re-auth, a connection is removed, etc.). Decoupled as an interface so the gateway package doesn't depend on pkg/session — the platform wires an adapter at startup.

Implementations must be safe for concurrent calls and must NOT block on slow downstream subscribers; the toolkit's lock may be held when this fires (debounced in practice via notifyToolListChanged).

type Toolkit

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

Toolkit is a gateway that proxies tools from one or more upstream MCP servers through the platform's registry, auth, persona, and audit pipeline.

A single Toolkit manages multiple named upstream connections. Each upstream tool is re-exposed under a namespaced local name: "<connection_name>__<remote_tool_name>". Connections can be added or removed at runtime; the MCP server is notified of tool-list changes through AddTool / RemoveTools.

Startup is failure-isolated: an unreachable upstream is logged and skipped — it does not block platform startup. Other connections remain functional.

func New

func New(defaultName string) *Toolkit

New builds a Toolkit with the given default connection name and no initial connections.

func NewMulti

func NewMulti(cfg MultiConfig) *Toolkit

NewMulti builds a Toolkit and pre-loads the given parsed connection configs. Unreachable upstreams are logged and skipped so platform startup is never blocked.

func (*Toolkit) AddConnection

func (t *Toolkit) AddConnection(name string, config map[string]any) error

AddConnection parses the raw config, dials the upstream, discovers its tools, and registers them on the server (if one is already set). The admin layer's hotAddConnection helper logs any returned error as a structured warning.

func (*Toolkit) Close

func (t *Toolkit) Close() error

Close closes every upstream session. Safe to call on a never-registered toolkit.

Snapshots the live client pointers under the lock and closes them outside the lock so a slow upstream cannot block other toolkit operations during shutdown. A concurrent AddConnection that lands after the snapshot will leak its client at process exit — by design, Close is called once at process shutdown and external callers are expected to stop dispatching new AddConnection calls before invoking Close (the platform's lifecycle layer enforces this).

func (*Toolkit) ConnOAuthStore added in v1.61.3

func (t *Toolkit) ConnOAuthStore() connoauth.Store

ConnOAuthStore returns the unified OAuth token store wired into this toolkit, or nil when none has been wired yet. Mirrors apigateway.Toolkit.ConnOAuthStore for cross-toolkit parity.

func (*Toolkit) Connection

func (t *Toolkit) Connection() string

Connection returns the default connection name used in audit logs when a request does not carry one. Empty if no default is configured.

func (*Toolkit) ConnectionForTool

func (t *Toolkit) ConnectionForTool(toolName string) string

ConnectionForTool maps a namespaced local tool name (e.g. "vendor__list_contacts") back to its source connection name. Used by the platform's audit middleware to attribute proxied tool calls to the specific upstream connection they were routed to.

func (*Toolkit) HasConnection

func (t *Toolkit) HasConnection(name string) bool

HasConnection reports whether a connection with the given name is live.

func (*Toolkit) IngestOAuthToken

func (t *Toolkit) IngestOAuthToken(ctx context.Context, in IngestOAuthTokenInput) error

IngestOAuthToken stores tokens obtained from an authorization_code callback into the unified connection_oauth_tokens row AND rebuilds the upstream so the previously "needs reauth" connection becomes live with its discovered tools registered on the MCP server.

In production this is reached via the legacy admin gateway_oauth_handler — the unified connection OAuth handler in pkg/admin writes the same row directly through connoauth.Store.Set and then calls OAuthKindHandler.AfterConnect for the rebuild.

func (*Toolkit) Kind

func (*Toolkit) Kind() string

Kind returns the toolkit kind.

func (*Toolkit) ListConnections

func (t *Toolkit) ListConnections() []toolkit.ConnectionDetail

ListConnections returns metadata for every live connection, sorted by name.

func (*Toolkit) Name

func (t *Toolkit) Name() string

Name returns the toolkit instance name.

func (*Toolkit) ReacquireOAuthToken

func (t *Toolkit) ReacquireOAuthToken(ctx context.Context, name string) error

ReacquireOAuthToken forces a fresh token mint for the named connection. For authorization_code, that's a refresh-token exchange against the IdP via connoauth.Source.Reacquire (the cached row in connection_oauth_tokens is rolled forward). For client_credentials, the in-memory token is dropped and the grant is re-run against the IdP. Returns an error if the connection is missing or not configured for OAuth.

func (*Toolkit) RegisterTools

func (t *Toolkit) RegisterTools(s *mcp.Server)

RegisterTools captures the server reference and registers every tool from every already-loaded connection. Must be called exactly once, after the toolkit is registered in the platform registry.

func (*Toolkit) RemoveConnection

func (t *Toolkit) RemoveConnection(name string) error

RemoveConnection unregisters a connection's tools from the MCP server, closes its upstream session, and removes it from the toolkit.

The toolkit's mutex is held only for the brief map mutation + server.RemoveTools call. The actual client.close() — which performs network I/O (DELETE the MCP session against the upstream) — runs WITHOUT the lock so a slow upstream cannot block other toolkit operations.

func (*Toolkit) SetAuthEvents added in v1.61.0

func (t *Toolkit) SetAuthEvents(w *authevents.Writer)

SetAuthEvents wires the audit-event writer into the toolkit so every outbound refresh / revocation / rotation event from the tool-call hot path lands in the connection_auth_events history. Connections that were dialed before this call pick up the writer on their next outbound request (the round-tripper builds a fresh connoauth.Source per call from the toolkit's current store + events pair).

func (*Toolkit) SetConnOAuthStore added in v1.61.3

func (t *Toolkit) SetConnOAuthStore(s connoauth.Store)

SetConnOAuthStore wires the unified OAuth token store into the gateway. Required for authorization_code grants to survive process restarts and to refresh tokens silently between operator interactions.

When called after AddConnection, any authorization_code placeholder connections (those that were registered as "awaiting reauth" because the store wasn't yet wired) are automatically retried so persisted tokens are picked up without requiring a manual refresh. This makes startup wiring order-independent.

Concurrency: the toolkit lock is HELD ONLY for the snapshot of placeholders to retry and again for the final pointer-swap on each success. The discover() network I/O happens with the lock RELEASED so concurrent Status / ListConnections / Tools / AddConnection callers don't block on potentially-slow upstream dials. The placeholder remains in the connections map throughout, so the admin UI keeps showing "Connect" while a retry is in flight.

func (*Toolkit) SetEnrichmentEngine

func (t *Toolkit) SetEnrichmentEngine(e *enrichment.Engine)

SetEnrichmentEngine wires a cross-enrichment engine into this gateway. When set, every successful forwarded tool result is run through the engine before being returned to the client. Safe to call before or after RegisterTools — handlers fetch the current engine on each call.

func (*Toolkit) SetQueryProvider

func (t *Toolkit) SetQueryProvider(provider query.Provider)

SetQueryProvider stores the query provider (not consumed directly in v1).

func (*Toolkit) SetSemanticProvider

func (t *Toolkit) SetSemanticProvider(provider semantic.Provider)

SetSemanticProvider stores the semantic provider (not consumed directly in v1).

func (*Toolkit) SetToolListChangedNotifier added in v1.58.0

func (t *Toolkit) SetToolListChangedNotifier(n ToolListChangedNotifier)

SetToolListChangedNotifier wires the tools/list_changed publisher. Pass nil to disable (default). Both bare-nil and typed-nil interface values are detected via isNilNotifier and treated as "disable"; a `var p *somePtrAdapter; SetToolListChangedNotifier(p)` is safe and behaves identically to the bare-nil call. The notifier is fired (debounced) on every aggregate tool-inventory change that registers or removes at least one tool — RegisterTools, addParsed successes, SetTokenStore retry promotions, and RemoveConnection. Connections that resolve to zero tools (placeholder upstreams that never finished discovery, RemoveConnection on a connection with no live tools) are silently skipped to keep the downstream notification budget proportional to operator-visible inventory state.

Safe to call before or after the toolkit has connections. The production wiring path: main.go calls WireGatewayBroadcaster AFTER p.Start() runs RegisterTools, so the notifier is set after the connections in the initial config are already loaded — those initial RegisterTools calls fire BEFORE the notifier is wired, so they do not produce a notification. A caller that wires the notifier earlier (e.g., a custom integration test, or a hot-reload path that reinstalls connections) WILL see notifications for every connection coming up.

func (*Toolkit) Status

func (t *Toolkit) Status(ctx context.Context, name string) *ConnectionStatus

Status returns a status snapshot for the named connection. Returns nil when the connection is not registered.

For OAuth connections the OAuth field is populated by reading the persisted token row via connoauth.Source.Status — identical to what the general /api/v1/admin/connections/{kind}/{name}/oauth-status endpoint returns, so the gateway-specific status panel and the generic OAuthStatusCard never disagree about token state.

ctx is propagated to the persistence read so a client disconnect during admin polling cancels the DB query. The toolkit's read lock is released BEFORE the DB roundtrip so the storage call cannot stall concurrent writers (AddConnection, RemoveConnection, etc.).

func (*Toolkit) TestLiveConnection added in v1.58.2

func (t *Toolkit) TestLiveConnection(ctx context.Context, name string) ([]ProbeTool, error)

TestLiveConnection exercises the live upstreamClient for the named connection by issuing a `tools/list` request — the same call any tool invocation would trigger to satisfy auth and reach the upstream. It is the correct implementation of an admin "Test connection" button on a connection that is already wired into the toolkit: it uses the live auth round-tripper (loading and refreshing OAuth tokens via the toolkit's TokenStore), so it sees the SAME state that real tool calls see. Probe (the config-only one-shot dial) cannot do this because it builds an isolated client with no token-store access and would always fail for `authorization_code` connections that depend on a persisted refresh token.

Returns ErrConnectionNotFound when no row exists for name, and ErrConnectionNotLive when the row is a placeholder awaiting reauth.

func (*Toolkit) Tools

func (t *Toolkit) Tools() []string

Tools returns the aggregate, sorted set of namespaced local tool names across all live connections.

Directories

Path Synopsis
Package enrichment implements the cross-enrichment rule engine for the gateway toolkit.
Package enrichment implements the cross-enrichment rule engine for the gateway toolkit.
Package sources provides concrete enrichment Source adapters for the platform's built-in toolkits (Trino, DataHub).
Package sources provides concrete enrichment Source adapters for the platform's built-in toolkits (Trino, DataHub).

Jump to

Keyboard shortcuts

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