Documentation
¶
Overview ¶
Package mcp is the shared MCP (Model Context Protocol) client layer: it connects to a configured mcp_servers: entry (config.MCPServer) over either Streamable HTTP (endpoint:) or a local subprocess speaking newline- delimited JSON on stdin/stdout (command:, see stdio.go), lists its tools, and calls one. internal/agent (a tool source for agent steps) and internal/resource (a check/in/out backend) both build on this package rather than importing the vendored SDK directly, so both share one transport/auth implementation.
Bearer and oauth auth are both supported (see config.MCPServerAuth); oauth token refresh (reading and writing the login through the Vault) and the interactive `steps mcp login` bootstrap live in oauth.go/login.go respectively.
Index ¶
- Variables
- func CheckScopes(ctx context.Context, pipeline string, srv config.MCPServer) error
- func ForgetLogins(pipeline string) error
- func ListServerTools(ctx context.Context, pipeline string, srv config.MCPServer) ([]*sdkmcp.Tool, error)
- func Login(ctx context.Context, pipeline string, srv config.MCPServer, ...) error
- func LoginHosted(ctx context.Context, pipeline string, srv config.MCPServer, ...) error
- func LoginsExist(pipeline string) (string, bool)
- func MoveLogins(from, to string) error
- func PrintAndOpen(w io.Writer, open func(string) error, authURL string)
- func StaleLogin(ctx context.Context, pipeline string, srv config.MCPServer) error
- func TokenPath(pipeline, server string) (string, error)
- type Client
- type HostedCallback
- type LoginKey
- type MemoryVault
- func (m *MemoryVault) Get(pipeline, server string) *TokenFile
- func (m *MemoryVault) Load(_ context.Context, key LoginKey) (StoredLogin, error)
- func (m *MemoryVault) Put(pipeline, server string, tf *TokenFile) error
- func (m *MemoryVault) Save(_ context.Context, key LoginKey, _ string, data []byte) error
- type ScopeRefusal
- type StoredLogin
- type TokenFile
- type TokenState
- type Vault
Constants ¶
This section is empty.
Variables ¶
var ErrLoginOrphaned = errors.New("the pipeline this login was read under is gone")
ErrLoginOrphaned is a write-back for a pipeline that is gone: destroyed, or its name taken by another since the login was read.
var ErrLoginsExist = errors.New("mcp logins already exist")
ErrLoginsExist is MoveLogins refusing a destination that already holds logins: merging would hand one pipeline another's accounts unnoticed, and overwriting would destroy them.
var ErrNeedsLogin = errors.New("run `steps mcp login`")
ErrNeedsLogin marks an oauth failure that only a human re-running `steps mcp login` can clear: no token on disk, one authorized for a different endpoint, an expired token with nothing to refresh it with, or a refresh the authorization server actively rejected.
It exists to split one question preflight cannot answer from the error text alone: would WAITING fix this? Every other way a server fails to answer — DNS, a VPN not up yet, a 502 — is a fact about right now, and a `steps web` that quits over one is a watcher found dead on Monday for a blip that healed in a minute. These four are facts about the credential, and no interval grows a refresh token. See config.Problem.Transient, which is what both preflight callers set from this.
Functions ¶
func CheckScopes ¶ added in v0.3.0
CheckScopes reports why srv's saved login cannot do what the pipeline asks of it, adding to staleLogin what only the server can say: which scopes it offers now. Only an oauth server has a login to judge, and a missing or unusable login is checkCredential's to report, so both answer nil here.
The metadata is fetched on every call, so this belongs behind a cache: preflight's, which is where it is called from.
func ForgetLogins ¶
ForgetLogins removes every login pipeline has, for a destroy, so a later pipeline given the same name does not silently inherit them.
func ListServerTools ¶
func ListServerTools(ctx context.Context, pipeline string, srv config.MCPServer) ([]*sdkmcp.Tool, error)
ListServerTools connects to srv, lists its tools, and closes the connection — the discovery path `steps mcp tools <pipeline> <server>` calls, and a smoke test of connectivity/auth for any auth type.
func Login ¶
func Login(ctx context.Context, pipeline string, srv config.MCPServer, open func(url string) error) error
Login runs the interactive OAuth authorization-code + PKCE flow for an auth: {type: oauth} server and saves the resulting login to the Vault (see saveLogin) — the implementation behind `steps mcp login <server>`. open is the browser-launch function (injected so this is testable without a real browser and so this package stays free of an os/exec dependency — see main.go's openBrowser for the production implementation), falling back to printing the URL to stdout whenever it returns an error.
Login does its own protected-resource/authorization-server metadata discovery and dynamic client registration up front (rather than letting auth.AuthorizationCodeHandler do it internally, as it would for an ordinary request-triggered flow) specifically so it can capture the resulting client_id/client_secret/token_endpoint for persistence — the handler never exposes what it discovered/registered internally, and a headless run/watch process needs those fields to silently refresh later without repeating discovery or registering a new client every time (see oauthTokenSource in oauth.go, which is what run/watch actually use — this function is never called from there, only from the CLI's login command).
func LoginHosted ¶
func LoginHosted(ctx context.Context, pipeline string, srv config.MCPServer, hosted *HostedCallback) error
LoginHosted is Login for a redirect that arrives through hosted, blocking until it does, ctx ends, or the flow fails. The token lands where Login puts it: on THIS machine, which is the one that will spend it.
func LoginsExist ¶
LoginsExist reports whether pipeline already has a login directory, and where, so a rename can refuse before anything has moved. A directory that cannot be looked at counts as existing: the rename it guards would otherwise go ahead over whatever is there.
func MoveLogins ¶
MoveLogins carries from's logins to to, for a pipeline rename, which keeps history and so keeps its logins. A pipeline with no logins is a no-op; a destination that exists, even empty, is ErrLoginsExist, checked here because os.Rename silently replaces an empty directory on Linux.
func PrintAndOpen ¶
PrintAndOpen announces an authorization URL the way `steps mcp login` always has.
func StaleLogin ¶ added in v0.3.0
StaleLogin is CheckScopes without the request: what the saved login alone says. Cheap enough to run where a cached verdict would otherwise answer, which is the point — a refusal arrives mid-run, and preflight's cache would otherwise vouch for the login for minutes after it.
Types ¶
type Client ¶
type Client interface {
ListTools(ctx context.Context) ([]*sdkmcp.Tool, error)
CallTool(ctx context.Context, name string, args map[string]any) (*sdkmcp.CallToolResult, error)
Close() error
}
Client is a live connection to one MCP server: list its tools, call one, and close when done. It wraps a *sdkmcp.ClientSession so callers never import the vendored SDK directly.
func Connect ¶
Connect opens a session to srv, over whichever transport it's configured for (see config.MCPServer.IsStdio/newTransport). For an HTTP server, per its configured auth (see config.MCPServerAuth): "bearer" reads a static token from the named env var (mirroring internal/agent/llmclient's LookupAPIKey with required set); "oauth" loads the pipeline's login from the Vault as a self-refreshing token (see oauth.go) — never an interactive flow here, that's `steps mcp login` (login.go) alone. "" / "none" connects unauthenticated. A stdio server is always unauthenticated by construction (config.validateMCPServerTransport rejects any other auth.type at load time), since there is no HTTP request to attach a token to.
type HostedCallback ¶
type HostedCallback struct {
// contains filtered or unexported fields
}
HostedCallback is one pending login's redirect target, mounted by whoever owns the HTTP server.
func NewHostedCallback ¶
func NewHostedCallback(redirectURL, returnURL string, announce func(authURL string)) *HostedCallback
NewHostedCallback builds the redirect target for one login. redirectURL is where the provider sends the browser, which the caller must route to ServeHTTP; returnURL is where the browser is sent afterwards, empty when a terminal rather than a page is waiting; announce is told the authorization URL, once per attempt, so it can travel to whoever has the browser.
func (*HostedCallback) Matches ¶
func (h *HostedCallback) Matches(state string) bool
Matches reports whether state belongs to this login's outstanding request. It is the whole of the route's authentication: a provider's redirect is a bare browser navigation carrying no credential of the daemon's, and the state is a nonce this process minted that only the party holding the authorization URL has seen.
func (*HostedCallback) ReturnsTo ¶
func (h *HostedCallback) ReturnsTo() string
ReturnsTo is where this login sends the browser when it is done, empty for one a terminal started. It is what tells a caller holding a replaced login whether there is a page to send its abandoned browser back to.
func (*HostedCallback) ServeHTTP ¶
func (h *HostedCallback) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP receives the redirect, under the rules loopback.go's handle already keeps: an unrecognized state is refused without consuming the flow, and a second delivery is a conflict rather than an answer.
type LoginKey ¶ added in v0.3.0
type LoginKey struct{ Pipeline, Server string }
LoginKey names one login: a pipeline's oauth login to one of its servers. A struct so the two strings cannot be passed swapped.
type MemoryVault ¶ added in v0.2.0
type MemoryVault struct {
// contains filtered or unexported fields
}
MemoryVault keeps logins in memory, for tests that are not about where they land.
func (*MemoryVault) Get ¶ added in v0.2.0
func (m *MemoryVault) Get(pipeline, server string) *TokenFile
Get is the login saved for pipeline's server, or nil.
func (*MemoryVault) Load ¶ added in v0.2.0
func (m *MemoryVault) Load(_ context.Context, key LoginKey) (StoredLogin, error)
Load is the login saved for pipeline's server; a MemoryVault has one scope.
type ScopeRefusal ¶ added in v0.3.0
type ScopeRefusal struct {
Pipeline string
Server string
Tool string
// Required is the scope the server named, when it named one.
Required string
// Detail is the server's own words, bounded, for a refusal that named no scope.
Detail string
// contains filtered or unexported fields
}
ScopeRefusal is a server refusing a tool call for want of a permission the credential lacks. It is not something a model can act on — no argument it could change makes the call succeed — so a step that meets one ends as errored, and an oauth one is remembered against its login.
func (*ScopeRefusal) Error ¶ added in v0.3.0
func (r *ScopeRefusal) Error() string
func (*ScopeRefusal) Unwrap ¶ added in v0.3.0
func (r *ScopeRefusal) Unwrap() error
Unwrap makes an oauth refusal ErrNeedsLogin, which is what it is: a login fixes it and nothing else does, so a probe re-derives it rather than remembering it past the login that clears it.
type StoredLogin ¶ added in v0.3.0
StoredLogin is what a Vault holds for a LoginKey.
type TokenFile ¶
type TokenFile struct {
Endpoint string `json:"endpoint"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
TokenURL string `json:"token_url"`
Scopes []string `json:"scopes,omitempty"`
// Granted is what the authorization server said the token holds, from the token response's scope at login and at every refresh that names one. Scopes is only what was ASKED for, and a server is free to grant less, or rename what it grants; this is the one record of what the login can actually do. Nil when the server never said: RFC 6749 §5.1 lets it omit scope when it granted exactly what was asked.
Granted []string `json:"granted,omitempty"`
// LoggedIn is when this login was made, carried unchanged through every refresh. A refusal a server gave this login (scopes.go) belongs to it, and a login with a different LoggedIn is a different login that has not been refused yet.
LoggedIn time.Time `json:"logged_in,omitempty"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
TokenType string `json:"token_type,omitempty"`
Expiry time.Time `json:"expiry,omitempty"`
// contains filtered or unexported fields
}
TokenFile is one pipeline's login for one oauth-configured server, as handed to the Vault. It holds everything a later process needs to silently refresh an access token without repeating discovery or dynamic client registration: the resolved client credentials and token endpoint (captured once, during `steps mcp login` — see login.go) alongside the token itself. Endpoint guards against a stale login authorizing the wrong server if a name is ever reused for a different endpoint.
Never merkle-hashed, and never reaches the state database unsealed: the vault internal/cli installs encrypts it first. See docs/mcp.md's "Trust boundary" section.
func LoadTokenFile ¶
LoadTokenFile reads and parses a pre-vault token file at path; only importLegacyLogin still calls it.
type TokenState ¶
type TokenState struct {
// Connected is a token that can be used right now, whether or not it can renew itself.
Connected bool
// Stale is a connected token that still needs a login: it was refused a tool call, or no longer matches the scopes the pipeline asks for (scopes.go). The server still answers it, so it can be Tested; it just cannot do the work.
Stale bool
// Renews is a refresh token in hand, which is what makes a credential survive unattended.
Renews bool
// Detail is the sentence a person acts on: what is wrong, or that nothing is.
Detail string
}
TokenState is where one oauth server's saved credential stands. It carries no part of the credential itself — a page renders this, and the token is the one thing that must never reach one.
func InspectToken ¶
InspectToken reports the state of srv's saved credential from the saved login alone: no request, no refresh, nothing spent. The checks and their order are checkCredential's, which is what a run goes through, so the page and the run cannot disagree — but the WORDING is not, because a run's error names the login command to go and type and a page has a button for that.
type Vault ¶ added in v0.2.0
type Vault interface {
// Load is the login saved under key. StoredLogin.Scope names the pipeline it was read under: stable across a rename, different for a pipeline later set under a destroyed one's name. StoredLogin.Found is false when there is none.
Load(ctx context.Context, key LoginKey) (StoredLogin, error)
// Save replaces it. A non-empty scope is a write-back, refused with ErrLoginOrphaned unless the pipeline is still the one the login was read under; empty is a fresh login for whichever pipeline holds the name. A vault that cannot keep it safe refuses rather than keep it in the clear.
Save(ctx context.Context, key LoginKey, scope string, data []byte) error
}
Vault is where a pipeline's oauth logins are kept. internal/cli installs the one that seals them into the state database; this package hands it plaintext and never decides where it lands.