Documentation
¶
Overview ¶
Package mcp implements the Model Context Protocol JSON-RPC 2.0 transport that LLM clients (Claude Desktop, Cursor, Claude.ai web) call to discover and invoke wick connectors as tools.
Surface:
POST /mcp — JSON-RPC requests GET /.well-known/oauth-protected-resource — auth metadata (RFC 9728)
Methods served (server-side, JSON-RPC 2.0):
initialize — protocol handshake, capability negotiation tools/list — enumerate the caller's accessible connector ops tools/call — invoke one op, dispatching to connectors.Service.Execute notifications/* — accepted as no-ops (we don't push from the server yet)
Auth model:
Static bearer (wick_pat_...) — PAT path, decoded via accesstoken.Service
OAuth opaque token — OAuth path, decoded via oauth.Service
Anything else — 401 with WWW-Authenticate pointing at
/.well-known/oauth-protected-resource
The auth middleware resolves a user_id and the user's filter-tag IDs onto the request context; everything below it (tools/list, tools/call) sees only the connectors that user can access.
Index ¶
- Constants
- type AuthMiddleware
- type Handler
- func (h *Handler) AgentToolDescriptors(ctx context.Context) []handlers.ToolDescriptor
- func (h *Handler) CallAgentTool(ctx context.Context, name string, args map[string]any, sessionID string) (string, bool)
- func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (h *Handler) ServeStdio(ctx context.Context, r io.Reader, w io.Writer)
- func (h *Handler) ServeStdioOS(ctx context.Context)
- func (h *Handler) WithAppURL(get func() string) *Handler
- func (h *Handler) WithAskUser(m askuser.Asker) *Handler
- func (h *Handler) WithAskUserPolicy(fn func(sessionID string) (bool, string)) *Handler
- func (h *Handler) WithBuildInfo(version, commit, buildTime string) *Handler
- func (h *Handler) WithDB(db *gorm.DB) *Handler
- func (h *Handler) WithLayout(l agentconfig.Layout) *Handler
- func (h *Handler) WithPool(p *agentpool.Pool, layout agentconfig.Layout) *Handler
- func (h *Handler) WithRefreshSession(fn func(id string) error) *Handler
- func (h *Handler) WithSchedule(s *schedule.Store) *Handler
- func (h *Handler) WithWickRoot(root string) *Handler
- type ScopedTokens
Constants ¶
const InternalAgentUserID = "wick-agent-internal"
InternalAgentUserID is the id of the synthetic admin principal the per-boot internal token maps to. Agent spawns authenticate with that token, so this id shows up as the caller's user id for agent MCP calls that don't resolve a session owner — per-owner gates (data tables) treat it as an unrestricted system principal rather than a real user.
const ScopedTokenPrefix = "wick_sub_"
ScopedTokenPrefix marks tokens minted here. Distinct from the PAT prefix so resolveToken never routes one to the wrong validator.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type AuthMiddleware ¶
type AuthMiddleware struct {
// contains filtered or unexported fields
}
AuthMiddleware extracts the Authorization: Bearer header, routes the token to the right validator (PAT vs OAuth), loads the user record + filter-tag IDs, and stamps both onto the request context using login.WithUser so downstream code reads identity exactly the way it does for cookie-authed requests.
Unauth requests get a 401 with WWW-Authenticate pointing at the resource-metadata document (RFC 9728), so spec-compliant MCP clients can discover the OAuth server and run the auth dance.
func NewAuthMiddleware ¶
func NewAuthMiddleware(tokens *accesstoken.Service, users userResolver, oauth oauthValidator, resourceMetaURL string) *AuthMiddleware
NewAuthMiddleware wires the bearer middleware. Pass nil oauth to run PAT-only (Phase B); attach later by replacing the field.
func (*AuthMiddleware) WithInternalToken ¶ added in v0.15.1
func (m *AuthMiddleware) WithInternalToken(token string) *AuthMiddleware
WithInternalToken sets the per-boot internal MCP secret agent spawns send as their Bearer token to reach the live MCP server over loopback.
func (*AuthMiddleware) WithScopedTokens ¶ added in v0.36.0
func (m *AuthMiddleware) WithScopedTokens(s *ScopedTokens) *AuthMiddleware
WithScopedTokens attaches the sub-agent token issuer.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler wires the MCP JSON-RPC surface — tools/list, tools/call, initialize. Bearer auth is applied by AuthMiddleware before this handler runs.
Tool surface uses a meta-tool pattern: three stable tools (wick_list, wick_search, wick_execute) let the LLM discover connectors at runtime. Adding/removing connectors never changes the cached tool list.
func NewHandler ¶
func NewHandler(c *connectors.Service) *Handler
func (*Handler) AgentToolDescriptors ¶ added in v0.34.0
func (h *Handler) AgentToolDescriptors(ctx context.Context) []handlers.ToolDescriptor
AgentToolDescriptors returns the tool catalog the in-process agent may call, as the synthetic admin principal (full visibility, like the CLI agents' loopback MCP calls). Includes the dynamic wickmanager tools.
func (*Handler) CallAgentTool ¶ added in v0.34.0
func (h *Handler) CallAgentTool(ctx context.Context, name string, args map[string]any, sessionID string) (string, bool)
CallAgentTool dispatches one tool call in-process and returns the tool result text + isError, reusing dispatchTool (identical routing to the HTTP transport). sessionID is threaded via the X-Wick-Session-Id header so session-aware tools (ask_user, wick_session_*) resolve correctly.
func (*Handler) ServeStdio ¶ added in v0.5.1
ServeStdio runs the MCP JSON-RPC server over r/w for local clients (Claude Desktop, Cursor, etc.). Each line from r is one JSON-RPC message; the response is written as one JSON line to w.
No auth middleware — the caller pre-populates ctx with login.WithUser (typically a synthetic local admin) before calling. Logs go to stderr so the protocol stream stays clean.
func (*Handler) ServeStdioOS ¶ added in v0.5.1
ServeStdioOS is the process-level entrypoint: reads os.Stdin, writes os.Stdout. Thin wrapper around ServeStdio for production use.
func (*Handler) WithAppURL ¶ added in v0.6.0
func (*Handler) WithAskUser ¶ added in v0.9.0
func (*Handler) WithAskUserPolicy ¶ added in v0.13.0
func (*Handler) WithBuildInfo ¶ added in v0.5.1
func (*Handler) WithLayout ¶ added in v0.17.0
func (h *Handler) WithLayout(l agentconfig.Layout) *Handler
WithLayout wires the agents storage layout without a pool — stdio mode needs it for the session-scoped tools (wick_session_info, wick_set_title, wick_session_workspace) even though no agent pool runs in that process.
func (*Handler) WithRefreshSession ¶ added in v0.16.13
WithRefreshSession wires the registry-refresh callback used by wick_set_title to keep the dashboard's in-memory session cache in sync after the title is written to disk.
func (*Handler) WithSchedule ¶ added in v0.29.0
WithSchedule wires the scheduled-message store that backs wick_schedule_message. nil (stdio/tests) disables the tool.
func (*Handler) WithWickRoot ¶ added in v0.6.0
type ScopedTokens ¶ added in v0.36.0
type ScopedTokens struct {
// contains filtered or unexported fields
}
ScopedTokens issues short-lived, in-memory bearer tokens that authenticate a spawned SUB-AGENT to the loopback MCP server as the human who triggered it, with an explicitly narrowed tag set.
Why this exists ¶
Normal agent spawns authenticate with the per-boot internal token, which maps to a synthetic ADMIN principal. Admin bypasses tag filtering entirely. If a sub-agent were handed that same token, a profile's "allowed tags" would be decorative: the child would see every tool in the system regardless of what the profile said, and regardless of what its triggering user was allowed to reach.
So a sub-agent gets one of these instead. The token carries:
- the REAL triggering user (so per-owner gates behave normally and the audit trail names a human, not a synthetic admin), and
- a precomputed tag slice that is already the intersection of the user's tags with the profile's optional narrowing list.
Tokens are process-local by design. They are only ever sent over loopback to this same process, they must die with the run, and persisting them would create a durable credential where a transient one suffices.
func NewScopedTokens ¶ added in v0.36.0
func NewScopedTokens() *ScopedTokens
NewScopedTokens builds an empty issuer.
func (*ScopedTokens) Issue ¶ added in v0.36.0
func (s *ScopedTokens) Issue(userID string, tagIDs []string) (string, error)
Issue mints a token bound to userID with exactly tagIDs.
The caller is responsible for having already intersected tagIDs down to what the user may reach — this type stores what it is given and never widens it, but it also cannot verify the intersection for you.
func (*ScopedTokens) Lookup ¶ added in v0.36.0
func (s *ScopedTokens) Lookup(token string) (userID string, tagIDs []string, ok bool)
Lookup resolves a token to its principal. ok is false for unknown or expired tokens.
func (*ScopedTokens) Revoke ¶ added in v0.36.0
func (s *ScopedTokens) Revoke(token string)
Revoke drops a token. Called when a delegation reaches a terminal state so a finished child cannot keep calling tools.
func (*ScopedTokens) Sweep ¶ added in v0.36.0
func (s *ScopedTokens) Sweep()
Sweep drops expired grants. Cheap; safe to call periodically.