mcp

package
v0.2.2 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 34 Imported by: 0

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 persistence and the interactive `steps mcp login` bootstrap live in oauth.go/login.go respectively.

Index

Constants

This section is empty.

Variables

View Source
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.

View Source
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.

View Source
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 ForgetLogins

func ForgetLogins(pipeline string) error

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 persists the resulting token to its per-pipeline token path (see TokenPath) — 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

func LoginsExist(pipeline string) (string, bool)

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

func MoveLogins(from, to string) error

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

func PrintAndOpen(w io.Writer, open func(string) error, authURL string)

PrintAndOpen announces an authorization URL the way `steps mcp login` always has.

func TokenPath

func TokenPath(pipeline, server string) (string, error)

TokenPath is where steps before the state database kept pipeline's login for server, in the clear: ${XDG_CONFIG_HOME:-~/.config}/steps/mcp/<pipeline>/<server>.json (via os.UserConfigDir()). A login still there is imported on first use (importLegacyLogin) and the file removed.

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

func Connect(ctx context.Context, pipeline string, srv config.MCPServer) (Client, error)

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's lookupAPIKey exactly); "oauth" loads a persisted, self-refreshing token from the per-pipeline token store (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 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, pipeline, server string) ([]byte, string, bool, error)

Load is the login saved for pipeline's server; a MemoryVault has one scope.

func (*MemoryVault) Put added in v0.2.0

func (m *MemoryVault) Put(pipeline, server string, tf *TokenFile) error

Put saves tf as a login for pipeline's server.

func (*MemoryVault) Save added in v0.2.0

func (m *MemoryVault) Save(_ context.Context, pipeline, server, _ string, data []byte) error

Save replaces it.

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"`
	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

func LoadTokenFile(path string) (*TokenFile, error)

LoadTokenFile reads and parses the token file at path.

type TokenState

type TokenState struct {
	// Connected is a token that can be used right now, whether or not it can renew itself.
	Connected 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

func InspectToken(ctx context.Context, pipeline string, srv config.MCPServer) TokenState

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 for server in pipeline, and scope names the pipeline it was read under: stable across a rename, different for a pipeline later set under a destroyed one's name. found is false when there is none.
	Load(ctx context.Context, pipeline, server string) (data []byte, scope string, found bool, err 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, pipeline, server, 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.

func UseVault added in v0.2.0

func UseVault(v Vault) Vault

UseVault installs v for this process and returns what it replaced, so a test can put it back. The tokens held from the old one are dropped with it.

Jump to

Keyboard shortcuts

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