Documentation
¶
Overview ¶
Package connect provides functionality to register MCPProxy as an MCP server in various client configuration files (Claude Code, Cursor, VS Code, Windsurf, Codex, Gemini).
Index ¶
- func ConfigPath(clientID, homeDir string) string
- func DerivePreconditionToken(key []byte, state PreconditionState) string
- type AccessError
- type AccessOutcome
- type ClientDef
- type ClientStatus
- type ConnectPreview
- type ConnectResult
- type EntrySummary
- type PreconditionState
- type Service
- func (s *Service) Connect(clientID, serverName string, force bool) (*ConnectResult, error)
- func (s *Service) ConnectWithPrecondition(clientID, serverName string, force bool, preconditionToken string) (*ConnectResult, error)
- func (s *Service) DetectAppDataDenial() (denied bool, remediation string)
- func (s *Service) Disconnect(clientID, serverName string) (*ConnectResult, error)
- func (s *Service) GetAllStatus() []ClientStatus
- func (s *Service) GetConnectedCount() int
- func (s *Service) GetConnectedIDs() []string
- func (s *Service) GetStatus(clientID string) (ClientStatus, error)
- func (s *Service) Preview(clientID, serverName string) (*ConnectPreview, error)
- func (s *Service) Undo(clientID, serverName, backupName string) (*ConnectResult, error)
- func (s *Service) WithConfigProvider(fn func() (listenAddr, apiKey string, requireMCPAuth bool)) *Service
- func (s *Service) WithRequireMCPAuth(v bool) *Service
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ConfigPath ¶
ConfigPath returns the expected configuration file path for the given client on the current operating system. homeDir overrides os.UserHomeDir when non-empty (useful for testing).
func DerivePreconditionToken ¶
func DerivePreconditionToken(key []byte, state PreconditionState) string
DerivePreconditionToken computes the opaque precondition token that binds a rendered connect preview to the exact operation and pre-write state it described (Spec 091 FR-005).
It is an HMAC-SHA256, hex-encoded, over a canonical LENGTH-PREFIXED encoding of every field, so:
- no two distinct states share a preimage — a byte moved from one field into the next changes the encoding, which a delimiter- or concatenation-based scheme would not catch;
- the token is KEYED with a per-core-instance random in-memory key, so it is not an offline confirmation oracle: an attacker who knows everything about the state except a masked or weak credential still cannot test guesses.
The token is never persisted and never leaves this process except as an opaque string in the preview response.
Types ¶
type AccessError ¶ added in v0.41.0
type AccessError struct {
Client string // client id/name
Path string // config path attempted
Outcome AccessOutcome // accessDenied (could also wrap malformed)
Remediation string // actionable fix text
Err error // underlying OS cause
}
AccessError is returned by connect/disconnect (and surfaced by GetStatus via the Remediation field) when a client-config access is permission-denied. It is errors.As-discoverable so the REST layer can map it to the right field, and unwraps to the underlying OS error so errors.Is(err, fs.ErrPermission) still holds (data-model.md AccessError).
func (*AccessError) Error ¶ added in v0.41.0
func (e *AccessError) Error() string
func (*AccessError) Unwrap ¶ added in v0.41.0
func (e *AccessError) Unwrap() error
Unwrap exposes the underlying OS error for errors.Is/errors.As.
type AccessOutcome ¶ added in v0.41.0
type AccessOutcome = string
AccessOutcome classifies an attempt to read or write a client config file (Spec 075 FR-003). It is an alias of string so it interoperates with the existing untyped access* constants and the ClientStatus.AccessState wire field, while giving classifier signatures a documented, enum-like type.
Valid values are the access* constants in connect.go: accessAccessible, accessAbsent, accessDenied, accessMalformed. (accessUnknown is the overall, not-content-checked status default, not a classification outcome.)
type ClientDef ¶
type ClientDef struct {
ID string // Unique identifier, e.g. "claude-code"
Name string // Human-readable name, e.g. "Claude Code"
Format string // File format: "json" or "toml"
ServerKey string // Top-level key for server entries: "mcpServers" or "servers"
Supported bool // Whether this client can be connected (directly or via a bridge)
Reason string // Explanation when Supported is false
Note string // Optional caveat shown for supported clients (e.g. bridge requirement)
Bridge bool // Connects via a stdio bridge; Connect can create the config when absent
Icon string // Icon identifier for frontend use
}
ClientDef describes a known MCP client and its configuration file format.
func FindClient ¶
FindClient looks up a client definition by ID. Returns nil if not found.
func GetAllClients ¶
func GetAllClients() []ClientDef
GetAllClients returns the definitions of all known clients.
type ClientStatus ¶
type ClientStatus struct {
ID string `json:"id"`
Name string `json:"name"`
ConfigPath string `json:"config_path"`
Exists bool `json:"exists"` // config file exists on disk
Connected bool `json:"connected"` // mcpproxy entry present in config
Supported bool `json:"supported"` // client can be connected (directly or via a bridge)
Reason string `json:"reason,omitempty"` // why not supported
Note string `json:"note,omitempty"` // caveat for supported clients (e.g. bridge requirement)
Bridge bool `json:"bridge,omitempty"` // connects via a stdio bridge; connectable even without an existing config
Icon string `json:"icon"`
ServerName string `json:"server_name,omitempty"` // name under which mcpproxy is registered
// AccessState classifies the per-client content access (Spec 075, additive).
// Empty/"unknown" in the content-read-free overall status; resolved to
// "accessible"/"absent"/"malformed" (and "denied" in US2) by on-demand reads.
AccessState string `json:"access_state"`
// CheckedPaths lists every config location the existence check consults,
// highest precedence first. For most clients this is just ConfigPath; for
// OpenCode it names both opencode.jsonc and opencode.json (#922), so a
// "no config found" UI can say exactly which files were looked for.
CheckedPaths []string `json:"checked_paths,omitempty"`
// Remediation carries actionable fix text, populated only when access is denied.
Remediation string `json:"remediation,omitempty"`
}
ClientStatus describes the current state of a client's configuration with respect to an MCPProxy entry.
type ConnectPreview ¶ added in v0.47.0
type ConnectPreview struct {
Client string `json:"client"`
ConfigPath string `json:"config_path"`
Format string `json:"format"` // "json" | "toml"
ServerKey string `json:"server_key"` // mcpServers / servers / mcp_servers / mcp
ServerName string `json:"server_name"` // key written into the config ("mcpproxy")
Entry map[string]interface{} `json:"entry"` // exact entry (masked) that will be written
EntryText string `json:"entry_text"` // entry rendered in the client's format (masked)
EntryExists bool `json:"entry_exists"` // an entry with this name already exists (overwrite/force case)
ContainsAPIKey bool `json:"contains_api_key"` // the written URL embeds an apikey credential
Bridge bool `json:"bridge,omitempty"` // connects via a stdio bridge (config created if absent)
// AccessState classifies the on-demand config read used to determine
// EntryExists (Spec 075): accessible|absent|malformed. A denied read never
// reaches here — it is returned as a typed *AccessError (403 + remediation).
AccessState string `json:"access_state"`
// ExistingEntrySummary describes the entry this connect would REPLACE, and
// is populated only when EntryExists (Spec 091 FR-003). It is built by
// construction from non-secret projections — see EntrySummary — so no
// arbitrary value from the user's config can ride out in a preview. It is
// display-only: drift detection uses PreconditionToken, never this.
ExistingEntrySummary *EntrySummary `json:"existing_entry_summary,omitempty"`
// PreconditionToken is the opaque, keyed digest of the raw pre-write state
// this preview describes plus the exact entry that would be written (Spec
// 091 FR-005). A caller echoes it on the subsequent write; the core
// recomputes and refuses with a discriminated conflict when anything has
// drifted — externally (the file or the target entry changed) or
// proxy-side (credential rotation, auth toggle, address change).
PreconditionToken string `json:"precondition_token"`
// ConnectRefusal carries, verbatim, the reason a connect would refuse for
// this client regardless of user intent — today only a non-create-capable
// client (OpenCode) whose config is absent. It is produced by the SAME guard
// the write runs, so the form can hide the Connect control and show the
// reason instead of letting the user discover it by clicking (Spec 091
// FR-003). Empty means connectable.
ConnectRefusal string `json:"connect_refusal,omitempty"`
}
ConnectPreview describes the exact change a subsequent Connect would make to a client config, WITHOUT modifying the file or creating a backup (Spec 078 US1). The entry is derived from the same buildServerEntry used by the real write, so what is previewed equals what is written for the same client and configuration (FR-002); the embedded API key is masked for display (FR-004).
type ConnectResult ¶
type ConnectResult struct {
Success bool `json:"success"`
Client string `json:"client"`
ConfigPath string `json:"config_path"`
BackupPath string `json:"backup_path,omitempty"`
ServerName string `json:"server_name"`
Action string `json:"action"` // "created", "updated", "already_exists", "removed", "not_found"
Message string `json:"message"`
}
ConnectResult describes the outcome of a connect or disconnect operation.
type EntrySummary ¶
type EntrySummary struct {
// EntryName is the key the entry actually lives under, which may differ
// from the requested server_name when the write adopts an equivalent entry.
EntryName string `json:"entry_name"`
Type string `json:"type,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
Command string `json:"command,omitempty"`
HeaderNames []string `json:"header_names"`
EnvNames []string `json:"env_names"`
}
EntrySummary is the sanitized, display-only description of the client-config entry a connect would replace (Spec 091 FR-003).
It exists because the preview must tell the user WHAT is being overwritten without ever echoing config content: the pending entry is masked at construction (entryParams substitutes the mask constant), which cannot sanitize arbitrary user-authored content in an EXISTING entry. So the preview renders no existing content at all — only this fixed set of parsed projections, built by construction from a whitelist:
entry name | transport type | endpoint (query, userinfo and fragment stripped) | command | header NAMES | env NAMES
Nothing else from the entry can reach the response: unknown fields are not copied, values of headers/env are never read, and a URL-shaped field is emitted only after being reparsed into scheme://host/path. Secrecy therefore holds structurally rather than by masking heuristics (research D2).
The summary is display-only: it is NEVER used for drift comparison — that is the precondition token's job over the raw value (FR-005).
type PreconditionState ¶
type PreconditionState struct {
// ClientID scopes the token to one client's connect flow.
ClientID string
// ConfigPath is which file is being written.
ConfigPath string
// Requested is the entry name the caller asked for — the key the write
// creates when nothing is adopted. It is NOT implied by ResolvedEntryName:
// that one is empty for every absent target, so without this a preview for
// one name validated a write under any other.
Requested string
// FileExists distinguishes create from update (a file appearing or
// vanishing between preview and write).
FileExists bool
// ResolvedEntryName is the key the write would replace, AFTER the same
// equivalent-entry adoption the write performs. Empty when none resolved.
ResolvedEntryName string
// RawResolvedEntry is that entry's value; nil when there is none. It
// includes values the sanitized summary deliberately hides, so a credential
// rotation inside the existing entry drifts.
RawResolvedEntry json.RawMessage
// PendingEntry is the entry the proxy would write right now, so proxy-side
// drift (API-key rotation, require_mcp_auth toggle, listen-address change)
// invalidates the preview too — otherwise a credential could be embedded
// without the FR-004 notice ever having been shown.
PendingEntry json.RawMessage
}
PreconditionState is everything a precondition token binds a rendered preview to: the operation it described AND the pre-write state it observed. Two operations that would touch the config differently must differ in at least one field here, or a token minted for one authorizes the other.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service provides connect/disconnect operations for MCP client configurations.
func NewService ¶
NewService creates a Service that will inject the given listen address and optional API key into client configurations.
func NewServiceWithHome ¶
NewServiceWithHome creates a Service with a custom home directory (for testing).
func NewServiceWithReader ¶ added in v0.41.0
func NewServiceWithReader(listenAddr, apiKey, homeDir string, readFile func(string) ([]byte, error)) *Service
NewServiceWithReader creates a Service with a custom content reader (for testing the access-classification seam without a real OS denial).
func (*Service) Connect ¶
func (s *Service) Connect(clientID, serverName string, force bool) (*ConnectResult, error)
Connect registers MCPProxy in the specified client's configuration file. serverName defaults to "mcpproxy" if empty. If force is false and an entry already exists, an error is returned.
This is the tokenless entry point kept for the Web UI, the CLI and every existing caller; ConnectWithPrecondition adds the Spec 091 drift guard.
func (*Service) ConnectWithPrecondition ¶
func (s *Service) ConnectWithPrecondition(clientID, serverName string, force bool, preconditionToken string) (*ConnectResult, error)
ConnectWithPrecondition is Connect guarded by the opaque token a preview returned (Spec 091 FR-005). When preconditionToken is non-empty, the core re-resolves the raw pre-write state and the entry it would write, recomputes the token, and refuses with the discriminated "precondition_failed" action — writing nothing and taking no backup — if anything drifted since the preview: the file appearing or vanishing, the resolved (possibly adopted) entry changing in any way, or the proxy's own configuration changing what would be written. force=true rides WITH the token for a replace-classified flow and never rescues a stale one.
An empty token means exactly today's behavior, so existing consumers are unaffected (contracts §2).
func (*Service) DetectAppDataDenial ¶ added in v0.43.0
DetectAppDataDenial probes installed client configs for a persisted macOS App-Data (TCC) permission denial, for the doctor diagnostic (Spec 075 US3, FR-007/008). It walks the supported clients and, for the first whose config file exists (os.Stat metadata only), performs a single content read through the seam; if that read classifies as accessDenied it reports the denial with the canonical, one-command remediation. It returns (false, "") when no installed client config is permission-denied — including when none are installed — so the check never raises a false positive on a machine that simply has no clients or has granted access (FR-008, T022).
Unlike GetAllStatus this DOES read content: the doctor command is an explicit user action, the one place a macOS App-Data prompt may legitimately appear.
func (*Service) Disconnect ¶
func (s *Service) Disconnect(clientID, serverName string) (*ConnectResult, error)
Disconnect removes the MCPProxy entry from the specified client's configuration.
func (*Service) GetAllStatus ¶
func (s *Service) GetAllStatus() []ClientStatus
GetAllStatus returns the connection status for every known client.
It determines "installed" via os.Stat metadata only and performs ZERO config content reads (Spec 075 FR-001): no client config file is opened, so simply viewing status raises no macOS App-Data privacy prompt. AccessState is left as "unknown" and Connected stays false for installed clients until an explicit per-client read via GetStatus.
func (*Service) GetConnectedCount ¶ added in v0.29.0
GetConnectedCount returns the number of supported clients in which mcpproxy is currently registered. Used as the "has any client connected?" wizard predicate (Spec 046).
func (*Service) GetConnectedIDs ¶ added in v0.29.0
GetConnectedIDs returns the identifiers of supported clients in which mcpproxy is currently registered. Identifiers come from the fixed per-client adapter table; user-entered values never appear here.
func (*Service) GetStatus ¶ added in v0.41.0
func (s *Service) GetStatus(clientID string) (ClientStatus, error)
GetStatus returns the status for a single client, reading its config contents on demand (Spec 075 FR-002). This is the scoped, explicit-action path where a macOS App-Data prompt may legitimately appear. It resolves Connected and AccessState (accessible/absent/malformed; "denied" is added in US2).
func (*Service) Preview ¶ added in v0.47.0
func (s *Service) Preview(clientID, serverName string) (*ConnectPreview, error)
Preview computes the exact entry a Connect would write for the given client, without modifying the config or creating a backup (Spec 078 FR-001). It reads the config on demand only to classify create-vs-overwrite (FR-003) and to resolve the Spec 075 access state; a permission denial surfaces as the same typed *AccessError that connect/disconnect return (FR-012).
func (*Service) Undo ¶ added in v0.47.0
func (s *Service) Undo(clientID, serverName, backupName string) (*ConnectResult, error)
Undo reverts the connect that produced the named backup, restoring the client config to its exact pre-connect state (Spec 078 US3 / FR-008). backupName is the bare filename (filepath.Base) of the backup the connect returned, NOT a path — undo resolves the full path itself against the client's own config directory (see the resolution/validation below):
- backupName != "": the config is restored byte-for-byte from that backup — this is the only revert that can bring back a pre-existing same-named entry a force-connect overwrote (surgical disconnect cannot).
- backupName == "": the preceding connect created the file (no prior file existed, ConnectResult.backup_path was empty); undo deletes the file so the pre-connect "no file" state is restored.
Safety first: Undo refuses (Action "conflict") unless the CURRENT file is byte-identical to what that connect produced — i.e. the backup content with exactly the mcpproxy entry applied, reconstructed via the same buildServerEntry/marshal path the write used. Any other content means the user (or another tool) changed the file since the connect, and a restore would clobber those edits. Callers should fall back to Disconnect (surgical entry removal) in that case. A missing backup refuses with Action "not_found". Every mutation takes its own safety backup before touching the file, and a permission denial anywhere surfaces as the same typed *AccessError as connect/disconnect (403 + remediation at the REST boundary).
Note the drift check also intentionally refuses when the effective listen address / API key / require_mcp_auth changed since the connect: the entry the service would write today no longer matches the one on disk, so mcpproxy can no longer prove the file is untouched.
func (*Service) WithConfigProvider ¶ added in v0.47.0
func (s *Service) WithConfigProvider(fn func() (listenAddr, apiKey string, requireMCPAuth bool)) *Service
WithConfigProvider installs a live-config accessor so the service reflects runtime changes to listen/api_key/require_mcp_auth (hot-reloaded via the file watcher or the wizard's require_mcp_auth toggle) rather than a startup snapshot. Wired only for the long-lived HTTP server; CLI one-shots leave it nil. Returns the receiver for chaining.
func (*Service) WithRequireMCPAuth ¶ added in v0.47.0
WithRequireMCPAuth sets whether the /mcp endpoint requires authentication, which decides whether connect embeds a credential in client configs at all. Threaded from config.RequireMCPAuth at the wiring sites, alongside listenAddr and apiKey. Returns the receiver for chaining.