Documentation
¶
Overview ¶
Package connector defines the public contract every connector module must satisfy. Connectors are the third class of wick module — sibling to Tools (human, web UI) and Jobs (scheduler, background) — built specifically to be consumed by LLM clients via MCP.
One connector definition is a Go module that wraps a single external API. A definition exposes one shared credential set (URL, token, ...) and N Operations — small, named actions an LLM can invoke. Each operation has its own input schema and Execute function.
At runtime an admin or user creates N instances of a definition through the web UI; each instance carries its own credential values and tag-based access control. Per instance, every operation can be toggled on or off, so admins can disable destructive or unverified operations without giving up the rest of the connector.
Connectors are not surfaced to MCP clients as N×M static tools. The MCP layer exposes a fixed three-tool meta surface (wick_list / wick_search / wick_execute), and the LLM discovers individual (instance, operation) pairs at runtime via wick_list. Each pair is addressed by an opaque tool_id of the form "conn:{connector_id}/ {op_key}", which wick_execute resolves back to a single ExecuteFunc call. Adding or removing instances therefore never changes the client's cached tool list — no manual "Refresh tool list" needed.
A typical downstream registration looks like:
package main
import (
"github.com/yogasw/wick/app"
"myproject/connectors/loki"
)
func main() {
app.RegisterConnector(loki.Meta(), loki.Creds{}, loki.Operations())
app.Run()
}
Wick reflects the typed Creds struct and each operation's typed Input struct into entity.Configs rows (via `wick:"..."` tags), so both the admin form for a new instance and the per-operation MCP JSON Schema can be auto-generated.
Index ¶
- Constants
- func NewHTTPClient() *http.Client
- type Ctx
- func (c *Ctx) Cfg(key string) string
- func (c *Ctx) CfgBool(key string) bool
- func (c *Ctx) CfgInt(key string) int
- func (c *Ctx) Context() context.Context
- func (c *Ctx) Input(key string) string
- func (c *Ctx) InputBool(key string) bool
- func (c *Ctx) InputInt(key string) int
- func (c *Ctx) InstanceID() string
- func (c *Ctx) Mask(data string, values []string) string
- func (c *Ctx) MaskIgnoreCase(data string, values []string) string
- func (c *Ctx) ReportProgress(progress, total int, message string)
- type ExecuteFunc
- type HealthCheckFunc
- type Masker
- type Meta
- type Module
- type OAuthMeta
- type OpHealth
- type Operation
- type ProgressReporter
Constants ¶
const DefaultHTTPTimeout = 30 * time.Second
DefaultHTTPTimeout is the per-request timeout wick uses for the http.Client it injects into Ctx. Connectors that need a different timeout can build their own *http.Client and call it via Ctx.HTTP only as a starting point — Ctx.HTTP is a field, not a method, so it can be replaced inside Execute when needed.
Variables ¶
This section is empty.
Functions ¶
func NewHTTPClient ¶
NewHTTPClient returns the *http.Client wick injects into Ctx by default. Exposed so tests and the panel-test handler can build a matching client without recreating the timeout policy.
Types ¶
type Ctx ¶
type Ctx struct {
// HTTP is the shared client every connector should use for outbound
// calls. Connectors MUST build requests with
//
// http.NewRequestWithContext(c.Context(), method, url, body)
//
// (NOT plain http.NewRequest) so the request inherits the call's
// deadline. Without that, the MCP transport's per-call timeout
// (sseExecuteTimeout in internal/mcp) and client-disconnect
// cancellations cannot abort an in-flight upstream request — the
// goroutine would leak until the upstream eventually responds on
// its own, accumulating one such goroutine per stuck call.
HTTP *http.Client
// contains filtered or unexported fields
}
Ctx is the per-call handle wick passes to a connector's ExecuteFunc. It bundles the resolved per-instance credential map, the per-call input arguments from the LLM, an HTTP client, and a context.Context so the call participates in cancellation and deadlines.
Unlike tool.Ctx and job.Ctx, the values in Ctx are NOT looked up via a global config service keyed on Owner+Key. A connector instance has its own row of credential values, materialized into c.configs at dispatch time, so reads stay O(1) and don't touch the configs table.
Construction is internal to wick — modules receive a ready-made *Ctx from the MCP dispatch layer (and, later, from the panel-test handler).
func NewCtx ¶
func NewCtx(ctx context.Context, instanceID string, configs, input map[string]string, httpClient *http.Client, progress ProgressReporter, masker Masker) *Ctx
NewCtx is used by wick when dispatching an MCP tools/call or a panel test. Downstream code does not call this directly. Pass a nil ProgressReporter for non-streaming calls; ReportProgress will be a no-op when the reporter is absent. Pass a nil Masker when running without the encrypted-fields layer; c.Mask / c.MaskIgnoreCase become passthroughs.
func (*Ctx) Cfg ¶
Cfg returns the value of a credential field declared by this connector's Creds struct. Returns "" when the key is absent.
func (*Ctx) CfgBool ¶
CfgBool returns c.Cfg(key) parsed as bool. "true"/"1"/"yes"/"on" (case-insensitive) count as true; anything else is false.
func (*Ctx) Context ¶
Context returns the context.Context bound to this call. The MCP transport derives this ctx from the inbound HTTP request and may further wrap it with a deadline (e.g. the SSE path's per-call timeout). Connectors MUST plumb this ctx into every outbound HTTP request and downstream service call so:
- the call aborts promptly when the deadline fires, instead of hanging until the upstream API returns on its own
- the client-disconnect signal propagates and the goroutine servicing the call winds down rather than leaking
Skipping this is the single most common way to introduce a goroutine leak in a connector.
func (*Ctx) Input ¶
Input returns the value of an argument the LLM passed in this tools/call request. The set of valid keys is declared by the connector's Input struct. Returns "" when the key is absent.
func (*Ctx) InputBool ¶
InputBool returns c.Input(key) parsed as bool. "true"/"1"/"yes"/"on" (case-insensitive) count as true; anything else is false.
func (*Ctx) InputInt ¶
InputInt returns c.Input(key) parsed as int. Unparseable or empty values return 0.
func (*Ctx) InstanceID ¶
InstanceID returns the connector_instances.id this call is bound to. Useful for structured logging.
func (*Ctx) Mask ¶ added in v0.6.0
Mask replaces every occurrence of each value in `values` inside `data` with a wick_enc_ token, scoped to the calling user's per-user key. Use it for sensitive plaintext that arrives from an upstream API and is NOT declared as a `secret` Configs/Input field — those are masked automatically by the framework.
Identical values within one call receive identical tokens (per-call dedup cache), so the LLM does not mistake duplicates for distinct credentials. Returns `data` unchanged when the framework was booted without the encrypted-fields layer or with WICK_ENC_DISABLE set.
Match is case-sensitive. For case-folded matching ("Admin" == "admin" share one token) use MaskIgnoreCase.
func (*Ctx) MaskIgnoreCase ¶ added in v0.6.0
MaskIgnoreCase is the case-folded variant of Mask. Every case variant of a keyword in `data` is replaced with a single token derived from the keyword's configured form, so the round-tripped plaintext matches what was configured regardless of which case variant appeared in the upstream response.
func (*Ctx) ReportProgress ¶
ReportProgress emits a progress event to the active MCP session, if one is listening. Safe to call from any goroutine. When the call was initiated over the JSON transport (no streaming) this is a no-op, so connectors can call it freely without checking for a reporter.
Pass total = 0 when the total is unknown — the MCP client renders the message and a spinner instead of a percentage. progress is the monotonically increasing units-completed count; values that go backwards confuse some clients.
type ExecuteFunc ¶
ExecuteFunc is the per-operation handler signature. It receives a *Ctx carrying the resolved per-instance credential map, the per-call input arguments from the LLM, and a configured *http.Client. The returned value is JSON-marshaled into the MCP tools/call result — return a typed struct or slice for a stable, ramping shape rather than the raw upstream payload.
type HealthCheckFunc ¶ added in v0.10.0
HealthCheckFunc verifies that the configured credentials carry the upstream permissions every operation needs. The framework calls it from the admin UI's "Check Permissions" button. Implementations should make one cheap probe call (e.g. Slack's auth.test) and project the granted permissions against each operation's requirements.
type Masker ¶ added in v0.6.0
Masker is the narrow slice of the encrypted-fields service connectors use to mask dynamic sensitive values they pull from upstream APIs. The framework provides an implementation pre-bound to the calling user's per-user key; connectors never see the user UUID directly.
caseInsensitive selects between exact-match (false, default) and case-folded matching (true) — useful when a connector's keyword list should match "Admin" and "admin" alike. The token returned is derived from the keyword's configured form, so decrypt yields the configured spelling regardless of which case variant was masked.
type Meta ¶
type Meta struct {
Key string
Name string
Description string
Icon string
// Fixed marks this connector as single-instance. Wick auto-seeds one
// row on first boot, the admin UI hides the "Add new instance"
// button, and connectors.Repo.Create rejects a second insert with
// ErrFixedInstanceViolation. Useful when the connector wraps a
// single in-process resource (e.g. wickmanager) or an external
// service that can only have one configuration.
//
// Default false = many instances allowed (existing behaviour).
Fixed bool
// DefaultTags is the list of tags wick auto-attaches to each newly
// seeded row for this connector at boot. Tags are reused across
// connectors via the central tags package; admins can add or remove
// links from the admin UI without redeploy. Existing rows are
// untouched on subsequent boots — admin unlinks survive restarts.
//
// Convention: every connector should set at least `tags.Connector`
// so the home page groups it under "Connector". Add module-specific
// tags (e.g. `tags.System` for built-in maintenance connectors) on
// top of that.
DefaultTags []tool.DefaultTag
}
Meta is the static metadata for a connector definition. Key must be a unique slug across every connector; entity.Connector.Key references it (one Meta.Key, many entity rows for multi-instance setups).
Description is shown to the admin in the manager UI. The LLM never sees it directly — it only reads per-operation Description fields surfaced through wick_list / wick_search.
type Module ¶
type Module struct {
Meta Meta
Configs []entity.Config
Operations []Operation
HealthCheck HealthCheckFunc
// OAuth is non-nil when this connector supports user OAuth.
OAuth *OAuthMeta
}
Module is the internal, fully-resolved registration record wick keeps for every connector definition. It is produced by app.RegisterConnector — the Meta, the configs reflected from the typed Creds struct, and the list of operations the connector exposes. Downstream code does not construct Module directly.
HealthCheck is optional. When non-nil, the connector detail page renders a "Check Permissions" button that invokes it and toggles per-operation system_disabled flags based on the report.
OAuth is non-nil when this connector supports user OAuth. The manager UI shows an "OAuth App" section on the list page and a "Connect" button on detail pages automatically — no per-connector handler wiring needed.
type OAuthMeta ¶ added in v0.13.0
type OAuthMeta struct {
// AuthorizeURL is the OAuth consent redirect URL
// (e.g. https://slack.com/oauth/v2/authorize).
AuthorizeURL string
// Scopes is the space- or comma-separated list of requested scopes
// (sent as the user_scope param for Slack, scope for standard OAuth2).
Scopes string
// DisplayName is shown on the Connect button (e.g. "Slack", "Google").
DisplayName string
// Icon is an SVG string or emoji rendered next to the Connect button.
Icon string
// GetUserIdentity exchanges a fresh access token for a unique user ID
// and human-readable display name. Called after the code→token exchange
// to route the token to the correct connector row.
GetUserIdentity func(ctx context.Context, accessToken string) (userID, displayName string, err error)
}
OAuthMeta describes how a connector participates in the OAuth 2.0 flow. The generic manager handler uses AuthorizeURL and Scopes to build the consent redirect; GetUserIdentity is called after the token exchange to resolve who the token belongs to.
Set Module.OAuth to a non-nil pointer to opt in. The manager UI will render an "OAuth App" section on the connector list page and a "Connect" button on user-token rows automatically.
type OpHealth ¶ added in v0.10.0
OpHealth is one entry in the report returned by Module.HealthCheck. OK=true means the configured credential has every upstream permission the operation needs; OK=false means the op should be system-disabled and Reason explains what is missing (e.g. "needs scope: chat:write").
HealthCheck implementations should return one OpHealth per operation the module exposes that can be permission-checked; ops omitted from the report are left untouched (neither system-disabled nor cleared).
type Operation ¶
type Operation struct {
Key string
Name string
Description string
Input []entity.Config
Execute ExecuteFunc
Destructive bool
// Docs carries the opt-in self-documentation fields the workflow
// MCP `workflow_node_detail` op surfaces to AI clients (examples,
// quirks, templateable fields, pair-with, common pitfalls).
// Zero-value Docs = current behaviour; populate per op when worth
// it. See pkg/wickdocs + internal/docs/workflow/24-describe-contract.md.
wickdocs.Docs
}
Operation is one named action exposed by a connector definition. A single connector can carry many operations: a "github" connector might have list_repos, create_issue, list_issues, add_comment.
Description is the load-bearing field for the LLM — it is shown verbatim in the wick_list / wick_search payload and is the primary signal the model uses to decide whether to call this op. Use action verbs and be specific ("List repositories visible to the authenticated user", not "list").
Destructive marks operations that mutate state in a way that is hard or impossible to undo (delete, force-push, send message, post comment). Wick uses this hint to default the per-instance toggle to off so admins must explicitly opt in, and to surface a warning chip in the admin UI.
func Op ¶
func Op[I any](key, name, description string, input I, exec ExecuteFunc, docs wickdocs.Docs) Operation
Op is a small constructor that reflects a typed input struct into the Operation's Input rows. Equivalent to building Operation{} by hand and calling entity.StructToConfigs(input) yourself, but reads nicer when listing many operations inline.
docs carries the optional self-documentation bundle exposed by the workflow MCP `workflow_node_detail` op — quirks, examples, sample payloads, pair-with, common pitfalls. Pass `wickdocs.Docs{}` when the op has no extra guidance; the description + reflected schema alone are already enough for callers in that case.
connector.Op("query", "Query Logs",
"Search Loki using LogQL.",
QueryInput{}, queryExec, wickdocs.Docs{})
Pass struct{}{} as input when the operation takes no input arguments.
func OpDestructive ¶
func OpDestructive[I any](key, name, description string, input I, exec ExecuteFunc, docs wickdocs.Docs) Operation
OpDestructive is the destructive-marked variant of Op. The resulting Operation defaults to disabled when a new instance is created, and the admin UI flags it so admins know to verify before enabling.
type ProgressReporter ¶
ProgressReporter receives incremental progress events emitted by a connector during a long-running call. The MCP layer wires an implementation that pushes JSON-RPC notifications/progress frames over the active SSE response; the JSON transport supplies no reporter and ReportProgress becomes a no-op.
Report MUST NOT block the caller — implementations drop events when the client is slow or has disconnected rather than back-pressuring the connector that emits them.