mcp

package
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: Apache-2.0, MIT Imports: 21 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnsupportedTransport is returned when transport type is not supported
	ErrUnsupportedTransport = errors.New("unsupported transport type")

	// ErrNotConnected is returned when operation requires connection but client is not connected
	ErrNotConnected = errors.New("client not connected")

	// ErrAlreadyConnected is returned when trying to connect an already connected client
	ErrAlreadyConnected = errors.New("client already connected")

	// ErrInitializeFailed is returned when MCP initialize handshake fails
	ErrInitializeFailed = errors.New("MCP initialize handshake failed")

	// ErrToolNotFound is returned when requested tool is not found
	ErrToolNotFound = errors.New("tool not found")

	// ErrResourceNotFound is returned when requested resource is not found
	ErrResourceNotFound = errors.New("resource not found")

	// ErrInvalidResponse is returned when server response is invalid
	ErrInvalidResponse = errors.New("invalid response from server")

	// ErrTimeout is returned when operation times out
	ErrTimeout = errors.New("operation timed out")

	// ErrConnectionClosed is returned when connection is closed unexpectedly
	ErrConnectionClosed = errors.New("connection closed")

	// ErrToolCallOutcomeUnknown means a call was attempted but its response
	// does not establish whether the server executed the tool.
	ErrToolCallOutcomeUnknown = errors.New("MCP tool execution outcome is unknown")
)
View Source
var ErrOAuthClientNotRegistered = errors.New("no OAuth client is registered for this MCP service")

ErrOAuthClientNotRegistered is returned when the service has no OAuth client to send the assertion grant with: neither a pre-registered one nor a stored registration. The grant does not register one: dynamic client registration describes a redirect-based client, which is not what a server-to-server grant needs.

Functions

func IsToolCallRetrySafe added in v0.9.0

func IsToolCallRetrySafe(err error) bool

IsToolCallRetrySafe identifies rejections before tool execution. A missing local connection sends no request; a terminated HTTP session is rejected with 404 and requires initialization. Typed authorization failures also reject the request and may require in-conversation authorization recovery. Other transport/RPC failures may occur after a side effect and must not authorize an automatic replay.

func ValidateServiceOutboundURLs

func ValidateServiceOutboundURLs(service *types.MCPService) error

ValidateServiceOutboundURLs validates every URL that the MCP transport or OAuth discovery flow may contact. It is intentionally called both at persistence boundaries and immediately before client construction so stale or imported rows cannot bypass the current SSRF policy.

Types

type CallToolResult

type CallToolResult struct {
	Content           []ContentItem `json:"content"`
	StructuredContent any           `json:"structuredContent,omitempty"`
	IsError           bool          `json:"isError,omitempty"`
}

CallToolResult represents the result of tools/call request

type ClientConfig

type ClientConfig struct {
	Service *types.MCPService

	// OAuth wiring (only used when Service.AuthConfig.AuthType == oauth).
	// The token store is scoped to (TenantID, Principal, Service.ID) so each
	// identity connects with its own access/refresh token.
	TenantID  uint64
	Principal types.Principal
	// UserID is kept for compatibility with older call sites/tests. New code
	// should pass Principal.
	UserID    string
	OAuthRepo interfaces.MCPOAuthRepository
}

ClientConfig represents configuration for creating an MCP client

type ContentItem

type ContentItem struct {
	Type     string `json:"type"` // "text", "image", "audio", "resource", "resource_link"
	Text     string `json:"text,omitempty"`
	Data     string `json:"data,omitempty"`
	MimeType string `json:"mimeType,omitempty"`
	// URI identifies the resource of a "resource" or "resource_link" item.
	URI string `json:"uri,omitempty"`
}

ContentItem represents a content item in tool result

type InitializeResult

type InitializeResult struct {
	ProtocolVersion string             `json:"protocolVersion"`
	Capabilities    ServerCapabilities `json:"capabilities"`
	ServerInfo      ServerInfo         `json:"serverInfo"`
	Instructions    string             `json:"instructions,omitempty"`
}

InitializeResult represents the result of initialize request

type MCPClient

type MCPClient interface {
	// Connect establishes connection to the MCP service
	Connect(ctx context.Context) error

	// Disconnect closes the connection to the MCP service
	Disconnect() error

	// Initialize performs the MCP initialize handshake
	Initialize(ctx context.Context) (*InitializeResult, error)

	// ListTools retrieves the list of available tools from the MCP service
	ListTools(ctx context.Context) ([]*types.MCPTool, error)

	// ListResources retrieves the list of available resources from the MCP service
	ListResources(ctx context.Context) ([]*types.MCPResource, error)

	// CallTool calls a tool on the MCP service
	CallTool(ctx context.Context, name string, args map[string]interface{}) (*CallToolResult, error)

	// ReadResource reads a resource from the MCP service
	ReadResource(ctx context.Context, uri string) (*ReadResourceResult, error)

	// IsConnected returns true if the client is connected
	IsConnected() bool

	// GetServiceID returns the service ID this client is connected to
	GetServiceID() string
}

MCPClient defines the interface for MCP client implementations

func NewMCPClient

func NewMCPClient(config *ClientConfig) (MCPClient, error)

NewMCPClient creates a new MCP client based on the transport type

type MCPManager

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

MCPManager manages MCP client connections

func NewMCPManager

func NewMCPManager(oauthRepo interfaces.MCPOAuthRepository) *MCPManager

NewMCPManager creates a new MCP manager. oauthRepo is used to wire per-user OAuth token stores for OAuth-enabled MCP services.

func (*MCPManager) CloseAll

func (m *MCPManager) CloseAll()

CloseAll closes all clients. The clients are detached under the lock and disconnected concurrently outside it, so one slow server neither serializes the rest nor blocks callers waiting on the lock; whatever has not finished within closeAllTimeout is abandoned.

func (*MCPManager) CloseClient

func (m *MCPManager) CloseClient(serviceID string) error

CloseClient closes and removes all cached connections for a service. For OAuth services this spans every per-principal connection (keys are prefixed with the service ID).

func (*MCPManager) CloseClientForPrincipal

func (m *MCPManager) CloseClientForPrincipal(serviceID string, principal types.Principal) error

CloseClientForPrincipal closes the connection one principal holds to an OAuth service and retires that principal's connection attempt if one is still in flight. Connections of other principals stay open, so the requests they have in flight are not canceled. A service without OAuth shares one connection that carries no per-user credential; it is left alone.

func (*MCPManager) CloseClientsAfterAuthorization

func (m *MCPManager) CloseClientsAfterAuthorization(
	ctx context.Context, tenantID uint64, serviceID string, principal types.Principal,
) error

CloseClientsAfterAuthorization recycles the cached connections that a completed OAuth authorization of principal leaves outdated:

  • The principal's own connection. The server may have tied its MCP session, and the tools it advertised, to the account that authorized it, and the new authorization may be for another account. An SSE event stream also keeps the credentials it was opened with.
  • Connections of the service in the same tenant that were built with an OAuth client ID other than the registration stored now. The registration is shared by every principal of the service and may have been replaced, in this process or in another replica. A connection still holding the old client ID can fail its next token refresh with invalid_client, and handling that failure deletes the stored registration, which by then is the replacement.

Other connections stay open: each one reads its principal's token from the store on every request, and closing a connection cancels the requests it has in flight. Only connections cached by this process are examined, and connection attempts of other principals that are still in flight are not. If the stored registration cannot be read, every connection of the service is closed, unless ctx has ended, in which case only the principal's own connection is: a caller that went away must not cancel other principals' requests.

func (*MCPManager) GetActiveClients

func (m *MCPManager) GetActiveClients() int

GetActiveClients returns the number of active clients

func (*MCPManager) GetClient

func (m *MCPManager) GetClient(serviceID string) (MCPClient, bool)

GetClient gets an existing client

func (*MCPManager) GetOrCreateClient

func (m *MCPManager) GetOrCreateClient(ctx context.Context, service *types.MCPService) (MCPClient, error)

GetOrCreateClient gets an existing client or creates a new one Caches and reuses existing connections for SSE/HTTP Streamable Note: Stdio transport is disabled for security reasons

For OAuth-enabled services the connection is keyed per principal (derived from ctx) so each identity connects with its own token.

func (*MCPManager) InvalidateToolSchemas

func (m *MCPManager) InvalidateToolSchemas(serviceID string, listedBy MCPClient)

InvalidateToolSchemas drops the header-schema caches of every cached connection for a service after listedBy completed a newer directory read. Connections stay open; their next modern CallTool reloads the directory. listedBy's own cache is current and is kept.

func (*MCPManager) ListActiveServices

func (m *MCPManager) ListActiveServices() []string

ListActiveServices returns IDs of services with active connections

func (*MCPManager) Shutdown

func (m *MCPManager) Shutdown()

Shutdown gracefully shuts down the manager

type OAuthAttempt

type OAuthAttempt struct {
	TenantID  uint64          `json:"tenant_id"`
	Principal types.Principal `json:"principal"`
	ServiceID string          `json:"service_id"`
	Completed bool            `json:"completed"`
}

OAuthAttempt is the non-secret, authenticated status of one authorization flow. It is kept separately from OAuthState because Take consumes the PKCE state before the token exchange finishes, while the opener still needs to distinguish this callback from an older token stored for the same service.

type OAuthAuthorizationResult

type OAuthAuthorizationResult struct {
	// FrontendRedirect is where the browser is sent next. It is set even when
	// the callback fails, as soon as the attempt's state has been read.
	FrontendRedirect string
	// TenantID and ServiceID identify the service the attempt authorized.
	TenantID  uint64
	ServiceID string
	// Principal is the identity the token was issued for.
	Principal types.Principal
}

OAuthAuthorizationResult describes the authorization attempt a provider callback belonged to.

type OAuthAuthorizationStatus

type OAuthAuthorizationStatus struct {
	Authorized       bool       `json:"authorized"`
	State            string     `json:"state"`
	RefreshAvailable bool       `json:"refresh_available"`
	ExpiresAt        *time.Time `json:"expires_at,omitempty"`
}

OAuthAuthorizationStatus distinguishes a currently usable access token from an expired token that can still be refreshed. This prevents a stale database row from being presented as an already successful authorization.

type OAuthGrantRejectedError

type OAuthGrantRejectedError struct {
	StatusCode int
	Code       string
}

OAuthGrantRejectedError reports that the authorization server refused the grant: a 4xx response other than 408 and 429, or a 2xx response carrying an OAuth error. Sending the same request again does not help; the assertion or the client has to change.

func (*OAuthGrantRejectedError) Error

func (e *OAuthGrantRejectedError) Error() string

type OAuthManager

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

OAuthManager orchestrates the MCP OAuth2 authorization-code flow: discovery, dynamic client registration, the authorize redirect, and the callback code exchange. Tokens are persisted per (tenant, principal, service); the registered client is persisted per (tenant, service) and reused.

func NewOAuthManager

func NewOAuthManager(
	repo interfaces.MCPOAuthRepository,
	serviceRepo interfaces.MCPServiceRepository,
	rdb *redis.Client,
) *OAuthManager

NewOAuthManager constructs the OAuth manager. rdb may be nil (Lite mode), in which case in-flight authorization states are kept in memory.

func (*OAuthManager) AuthorizationStatus

func (m *OAuthManager) AuthorizationStatus(
	ctx context.Context, tenantID uint64, principal types.Principal, serviceID string,
) (OAuthAuthorizationStatus, error)

AuthorizationStatus reports whether the stored access token is usable now, or is expired but still has a refresh token that runtime use can rotate.

func (*OAuthManager) AuthorizeWithAssertion

func (m *OAuthManager) AuthorizeWithAssertion(
	ctx context.Context,
	service *types.MCPService,
	tenantID uint64,
	principal types.Principal,
	assertion string,
) error

AuthorizeWithAssertion obtains principal's token for service with the JWT bearer grant and stores it the way an authorization code callback does.

It serves deployments where an application in front of WeKnora signs the user in and calls WeKnora for that user, so no browser can follow an authorization redirect. That application presents an assertion of the user's identity; WeKnora forwards it unread, because validating it is up to the authorization server. The grant is sent with the service's pre-registered client, or else the client already registered for it.

func (*OAuthManager) CompleteAuthorization

func (m *OAuthManager) CompleteAuthorization(
	ctx context.Context, state, code string,
) (OAuthAuthorizationResult, error)

CompleteAuthorization handles the provider callback: it validates state, exchanges the code for tokens (PKCE), and persists the per-user token. The result identifies the authorized service and principal so the caller can recycle the cached connections the new authorization leaves outdated.

func (*OAuthManager) IsAuthorizationAttemptComplete

func (m *OAuthManager) IsAuthorizationAttemptComplete(
	ctx context.Context,
	tenantID uint64,
	principal types.Principal,
	serviceID, attemptID string,
) (bool, error)

IsAuthorizationAttemptComplete reports whether this exact authorization attempt completed for the requested principal and service. A pre-existing token must never satisfy a newly opened OAuth popup.

func (*OAuthManager) IsAuthorized

func (m *OAuthManager) IsAuthorized(
	ctx context.Context, tenantID uint64, principal types.Principal, serviceID string,
) (bool, error)

IsAuthorized reports whether the given principal has an access token that is usable now. An expired row is not authorization success merely because its encrypted token columns remain non-empty.

func (*OAuthManager) Revoke

func (m *OAuthManager) Revoke(
	ctx context.Context, tenantID uint64, principal types.Principal, serviceID string,
) error

Revoke removes the principal's stored token for the service.

func (*OAuthManager) StartAuthorization

func (m *OAuthManager) StartAuthorization(
	ctx context.Context,
	service *types.MCPService,
	tenantID uint64,
	principal types.Principal,
	redirectURI, frontendRedirect string,
) (authorizationURL, attemptID string, err error)

StartAuthorization performs discovery + (one-time) dynamic client registration, then returns the authorization URL and an opaque attempt ID. redirectURI is the backend callback URL registered with the auth server; frontendRedirect is where the callback bounces the browser when finished.

func (*OAuthManager) StartAuthorizationForService

func (m *OAuthManager) StartAuthorizationForService(
	ctx context.Context,
	tenantID uint64,
	principal types.Principal,
	serviceID, redirectURI, frontendRedirect string,
) (string, error)

StartAuthorizationForService loads the MCP service by ID and starts the authorization-code flow, returning the URL the user must open. It is a convenience for callers (e.g. IM channels) that only hold a service ID and cannot reach the MCP service lookup directly.

type OAuthReauthorizationRequiredError

type OAuthReauthorizationRequiredError struct {
	Reason string
}

OAuthReauthorizationRequiredError means no usable access token can be recovered without interactive user consent.

func (*OAuthReauthorizationRequiredError) Error

type OAuthRefreshTemporaryError

type OAuthRefreshTemporaryError struct {
	Err error
}

OAuthRefreshTemporaryError preserves a token when refresh failed for a transient reason. Callers must surface/retry this as an operational failure, not open a new consent popup.

func (*OAuthRefreshTemporaryError) Error

func (*OAuthRefreshTemporaryError) Unwrap

func (e *OAuthRefreshTemporaryError) Unwrap() error

type OAuthRequiredError

type OAuthRequiredError struct {
	// MetadataURL is the RFC 9728 protected-resource metadata URL advertised by
	// the server via the WWW-Authenticate header. Non-empty by construction
	// (asOAuthRequired only wraps when the server advertised it).
	MetadataURL string
	Err         error
}

OAuthRequiredError signals that the target MCP server requires OAuth authorization — it answered the connect/initialize handshake with a 401 that advertised RFC 9728 protected-resource metadata — even though the service was NOT configured to use OAuth. Callers use this to guide the user to switch the auth strategy to OAuth instead of surfacing a generic "401" failure.

func (*OAuthRequiredError) Error

func (e *OAuthRequiredError) Error() string

func (*OAuthRequiredError) Unwrap

func (e *OAuthRequiredError) Unwrap() error

type OAuthState

type OAuthState struct {
	TenantID     uint64          `json:"tenant_id"`
	UserID       string          `json:"user_id"`
	Principal    types.Principal `json:"principal"`
	ServiceID    string          `json:"service_id"`
	CodeVerifier string          `json:"code_verifier"`
	ClientID     string          `json:"client_id"`
	RedirectURI  string          `json:"redirect_uri"`
	// FrontendRedirect is where the backend callback redirects the browser
	// after completing (or failing) the exchange.
	FrontendRedirect string `json:"frontend_redirect"`
}

OAuthState is the transient data needed to complete an authorization-code exchange. It is keyed by the opaque OAuth `state` parameter and MUST hold the PKCE code_verifier, which is a secret that must never reach the authorization server — hence server-side storage rather than encoding it into the state parameter.

type PromptsCapability

type PromptsCapability struct {
	ListChanged bool `json:"listChanged,omitempty"`
}

PromptsCapability represents prompts capability

type ReadResourceResult

type ReadResourceResult struct {
	Contents []ResourceContent `json:"contents"`
}

ReadResourceResult represents the result of resources/read request

type ResourceContent

type ResourceContent struct {
	URI      string `json:"uri"`
	MimeType string `json:"mimeType,omitempty"`
	Text     string `json:"text,omitempty"`
	Blob     string `json:"blob,omitempty"` // Base64 encoded
}

ResourceContent represents resource content

type ResourcesCapability

type ResourcesCapability struct {
	Subscribe   bool `json:"subscribe,omitempty"`
	ListChanged bool `json:"listChanged,omitempty"`
}

ResourcesCapability represents resources capability

type ServerCapabilities

type ServerCapabilities struct {
	Tools        *ToolsCapability       `json:"tools,omitempty"`
	Resources    *ResourcesCapability   `json:"resources,omitempty"`
	Prompts      *PromptsCapability     `json:"prompts,omitempty"`
	Logging      map[string]interface{} `json:"logging,omitempty"`
	Experimental map[string]interface{} `json:"experimental,omitempty"`
}

ServerCapabilities represents server capabilities

type ServerInfo

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

ServerInfo represents information about the server

type ToolsCapability

type ToolsCapability struct {
	ListChanged bool `json:"listChanged,omitempty"`
}

ToolsCapability represents tools capability

Jump to

Keyboard shortcuts

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