connector

package
v0.12.1 Latest Latest
Warning

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

Go to latest
Published: May 14, 2026 License: MIT Imports: 7 Imported by: 0

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

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

func NewHTTPClient() *http.Client

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

func (c *Ctx) Cfg(key string) string

Cfg returns the value of a credential field declared by this connector's Creds struct. Returns "" when the key is absent.

func (*Ctx) CfgBool

func (c *Ctx) CfgBool(key string) bool

CfgBool returns c.Cfg(key) parsed as bool. "true"/"1"/"yes"/"on" (case-insensitive) count as true; anything else is false.

func (*Ctx) CfgInt

func (c *Ctx) CfgInt(key string) int

CfgInt returns c.Cfg(key) parsed as int. Unparseable or empty values return 0.

func (*Ctx) Context

func (c *Ctx) Context() context.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

func (c *Ctx) Input(key string) string

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

func (c *Ctx) InputBool(key string) bool

InputBool returns c.Input(key) parsed as bool. "true"/"1"/"yes"/"on" (case-insensitive) count as true; anything else is false.

func (*Ctx) InputInt

func (c *Ctx) InputInt(key string) int

InputInt returns c.Input(key) parsed as int. Unparseable or empty values return 0.

func (*Ctx) InstanceID

func (c *Ctx) InstanceID() string

InstanceID returns the connector_instances.id this call is bound to. Useful for structured logging.

func (*Ctx) Mask added in v0.6.0

func (c *Ctx) Mask(data string, values []string) string

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

func (c *Ctx) MaskIgnoreCase(data string, values []string) string

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

func (c *Ctx) ReportProgress(progress, total int, message string)

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

type ExecuteFunc func(c *Ctx) (any, error)

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

type HealthCheckFunc func(c *Ctx) ([]OpHealth, error)

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

type Masker interface {
	Mask(data string, values []string, caseInsensitive bool) string
}

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
}

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.

type OpHealth added in v0.10.0

type OpHealth struct {
	Key    string
	OK     bool
	Reason string
}

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
}

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) 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.

connector.Op("query", "Query Logs",
    "Search Loki using LogQL.",
    QueryInput{}, queryExec)

Pass struct{}{} when the operation takes no input arguments.

func OpDestructive

func OpDestructive[I any](key, name, description string, input I, exec ExecuteFunc) 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

type ProgressReporter interface {
	Report(progress, total int, message string)
}

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.

Jump to

Keyboard shortcuts

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