| name | connector-module |
| description | Use for ANY work on a connector in the wick core repo — creating a new connector under internal/connectors/, refactoring/improving an existing one, fixing bugs, adding operations, editing the Configs or per-op Input structs, or touching anything that affects the MCP surface (internal/mcp/) or the registry (internal/connectors/registry.go). Covers the full module contract — pkg/connector.{Meta, Module, Operation, Op, OpDestructive, ExecuteFunc, Ctx} — plus the wick:"..." tag grammar shared with Tools and Jobs, the destructive-opt-in model, the typed-response convention, the http.NewRequestWithContext rule that prevents goroutine leaks, and the Bootstrap auto-seed contract. Also mandates the "ask before adding ops" question whenever a new connector is requested. |
| allowed-tools | Read, Grep, Glob, Edit, Write, Bash |
| paths | ["internal/connectors/**","internal/mcp/**","internal/jobs/connector-runs-purge/**","pkg/connector/**","internal/planning/archive/connectors-design.md","AGENTS.md"] |
Connector Module — wick core (upstream)
Scope: this skill is for connectors that ship inside wick itself — modules under internal/connectors/ registered from internal/connectors/registry.go::RegisterBuiltins(). If you are building a connector in a downstream project (a scaffold generated from template/), use the downstream skill at template/.claude/skills/connector-module/SKILL.md — paths, registration site, and verification differ.
Activate this skill whenever the user touches a connector in the wick lab binary or the connector framework itself — creating, improving, fixing, refactoring, or adding operations. When editing an existing connector, audit it against the rules below and bring it up to spec as part of the change.
Mental model
A connector wraps one external API for LLM consumption.
- One
Module carries one shared Configs struct (URL, token, ...) and N Operations (query, list_repos, create_issue, ...), grouped into categories (titled sections).
Module.Operations is a []connector.Category — each Category{Title, Description, Ops} owns its operations. The admin detail page renders one card per category. Code that needs the flat op list calls Module.AllOps(); an op's owning section is Module.CategoryOf(opKey). There is no Operation.Category field — the section owns the grouping.
- Each
Operation has its own typed Input struct (turned into the MCP JSON Schema) and its own ExecuteFunc.
- An admin can create many rows per definition at runtime; each row carries its own credential values, label, and tags. Same Go code, different rows = different (env, team, account).
- LLMs do not see N×M static tools. The MCP server exposes a fixed meta surface (
wick_list, wick_search, wick_get, wick_execute); each (row × operation) pair is addressed by an opaque tool_id of the form conn:{connector_id}/{op_key}.
The full design lives in internal/planning/archive/connectors-design.md. This skill is the operational summary; that document is the source of truth for any architectural question.
Applies to (non-exhaustive triggers)
- "Add a new built-in connector for X"
- "Add operation Y to internal connector X"
- "Refactor internal/connectors/X"
- "Fix bug in MCP dispatch / connector ctx / connector runs"
- Any edit under
internal/connectors/{name}/, pkg/connector/, internal/mcp/
- Changes to
internal/connectors/registry.go::RegisterBuiltins
Before building: ALWAYS gather the contract
Whenever the user asks for a new connector — or for a new operation on an existing one — STOP and ask before writing code. Connectors are LLM-facing; the wrong input/output shape costs more to fix than to gather upfront.
The very first reply back is a short numbered checklist asking the user (or the upstream docs) for:
"Before I write anything, please share these so the shape is right:
- Endpoint + auth — base URL, auth scheme (bearer / basic / header key / OAuth), required scopes. These become
Configs fields.
- Operations — which actions should the LLM call? For each: name, what it does, and whether it mutates state (delete, post, send →
OpDestructive, defaults off on every new row).
- One concrete sample request per operation — method, URL, headers, query params, body. A real cURL or HTTP example, not prose.*
- One concrete sample response per operation — both happy path and a typical error envelope. The error shape decides how
repo.go parses non-2xx.*
- Output shape — typed struct (recommended) or passthrough JSON? Typed gives the LLM a stable shape independent of upstream cosmetics.
- Pagination / rate-limit / retry quirks — anything an op needs to surface as input or error.
- Default tags — group-only (default, every approved user sees it) or restricted by filter tag?"
Then explain back what you'll build: list the ops you're going to write, the Configs fields you'll declare, and the typed return shapes — and wait for confirmation before generating code. This catches "no, the API actually needs both app_id and tenant_id" or "delete is reversible for 30 days, treat it as Op not OpDestructive" while it's still cheap.
Don't silently invent operations, skip the destructive flag, or guess request/response shapes from a description.
Module layout
The default layout is a three-file split — same shape as tools (handler.go / service.go / repo.go):
internal/connectors/myconn/
connector.go # Meta + Configs + per-op Input structs + Operations() + thin op handlers
service.go # pure Go — input validation, URL/body construction, response shaping
repo.go # outbound I/O — HTTP calls, DB, S3 (everything that touches the network)
doc.go # optional — package-level godoc when connector.go gets crowded
The shipped reference at internal/connectors/crudcrud/ follows exactly this split. Mirror it.
connector.go stays scannable — a reader sees Meta, Configs, every Input struct, every Operation description, and the dispatch outline of every handler without scrolling past validation logic or HTTP wiring.
service.go is unit-testable in isolation. Validators and URL builders take a *connector.Ctx (or plain values) and return data — no network, no fixtures, fast tests.
repo.go is where the goroutine-leak rule lives. Concentrating every http.NewRequestWithContext in one file makes the rule trivial to enforce and audit.
Handlers in connector.go should read like five lines: validate via service.go, dispatch via repo.go, return. Anything past "parse, validate, dispatch" goes in service.go. Trivial single-op connectors (no validation, one HTTP call) MAY collapse into a single connector.go, but default to splitting.
File contract
Meta()
func Meta() connector.Meta {
return connector.Meta{
Key: "github",
Name: "GitHub",
Description: "Read repos, issues, and pull requests on GitHub.",
Icon: "🐙",
}
}
Key is a unique slug across every connector (kebab-case allowed; snake_case discouraged for op keys but allowed for connector keys).
Description is shown to admins. The LLM reads per-Operation Description, not this.
Configs struct
Per-instance credentials and endpoints, shared across every operation. Admins fill these in at /manager/connectors/{Key}/{id} after wick auto-seeds the first row at boot.
type Configs struct {
BaseURL string `wick:"url;required;desc=GitHub API base URL. Default: https://api.github.com"`
Token string `wick:"secret;required;desc=Personal access token with the scopes you intend to use."`
}
Read SKILL.md from the config-tags folder (sibling of this skill's folder) for the full tag reference.
Fields tagged secret opt into the encrypted-fields layer — wick auto-decrypts incoming wick_enc_ tokens before ExecuteFunc runs and auto-masks the plaintext in the response back to the LLM. Use it for anything the LLM should pass forward without ever learning the value: tokens, API keys, session credentials. See SKILL.md from the encrypted-fields folder for the full contract (manual c.Mask / c.MaskIgnoreCase for dynamic API responses, wick_enc_ token format, per-user keys, MCP redirect tools).
Read at runtime via c.Cfg("base_url"), c.CfgInt("port"), c.CfgBool("use_tls") — keys are the snake_cased field name unless overridden with key=. Reads always return plaintext; the encrypted-fields layer happens around ExecuteFunc, not inside it.
Session connectors (Module.AllowSessionConfig)
Set AllowSessionConfig: true on the Module to declare that this connector can be cloned into a per-session instance — a throwaway connector a user spins up for one agent session (point base_url at staging, use a different key) without touching any saved connector row.
connector.Module{
Meta: httprest.Meta(),
Configs: entity.StructToConfigs(httprest.Configs{}),
Operations: httprest.Operations(),
AllowSessionConfig: true,
}
This is the capability flag (default false). Leave it false for OAuth/SSO connectors (their config is a user token, not a replaceable per-session value) and anything where a session-scoped clone makes no sense.
It is a two-layer opt-in — both must hold for a connector to be addable as a session instance:
Module.AllowSessionConfig (this flag, code) — the module can be cloned.
entity.Connector.AllowSessionConfig (per-instance, admin) — an admin flips "Allow per-session config override" in Manager → Connectors → {instance} → Per-session config. Default false.
How it works at runtime:
- A session instance is a clone of the base module with its own id (
sw_<uuid>), label, and config map. It lives in sessions/<id>/workspace.json, appears in wick_list/wick_get/wick_execute only when that session_id is passed, and is swept by the session-config-purge job (file age TTL) — it dies with the session.
- Users add/fill/test/remove instances in the session Config tab (the Workspace panel); the agent does the same via the
wick_session_workspace MCP tool (list/add/duplicate/configure/test/remove). The agent NEVER sees config values — it creates blank instances and the user fills them; secret values are stored as wick_cenc_ MASTER tokens (system-decryptable only) and decrypted in Service.Execute via the virtual SessionInstance path (no DB row).
- Custom connectors carry the same flag in their definition (
allow_session_config in the def draft / builder).
Background: internal/planning/in-progress/agent-bridge.md.
Per-operation Input structs
Each operation has its own input schema. Same wick:"..." grammar; the framework reflects it into the MCP JSON Schema clients see in wick_get.
type ListReposInput struct {
Org string `wick:"required;desc=Organization login. Example: anthropics"`
Visibility string `wick:"dropdown=all|public|private;desc=Filter by repo visibility."`
PerPage int `wick:"desc=Page size (1-100). Default 30."`
}
Read at runtime via c.Input("org"), c.InputInt("per_page"), c.InputBool("include_archived").
Operations()
Returns []connector.Category — operations grouped into titled sections via connector.Cat(title, description, ...ops). The category owns its ops; the admin detail page renders one card per section, and the per-connector "Sections" jump sidebar lists them. A connector with a single conceptual group uses one Cat (the title becomes the card header); use an empty title (Cat("", "", ...)) for an ungrouped/flat list.
func Operations() []connector.Category {
return []connector.Category{
connector.Cat("Repositories", "Browse and inspect repositories.",
connector.Op(
"list_repos",
"List Repositories",
"List repositories under {org}. Returns repo name, full_name, default_branch, visibility, and updated_at. Pagination is single-page only.",
ListReposInput{},
listRepos,
wickdocs.Docs{},
),
),
connector.Cat("Issues", "Read and mutate issues.",
connector.OpDestructive(
"close_issue",
"Close Issue",
"Close the given issue on {owner}/{repo}. Reversible only by reopening; comments and history are preserved.",
CloseIssueInput{},
closeIssue,
wickdocs.Docs{},
),
),
}
}
Signatures: Op(key, name, description string, input I, exec ExecuteFunc, docs wickdocs.Docs) and OpDestructive(...) — same args. The trailing docs wickdocs.Docs carries optional self-documentation (examples, quirks, output shape) surfaced by the workflow workflow_node_detail MCP op; pass wickdocs.Docs{} when there is nothing extra. The flat helper Module.AllOps() concatenates every section's ops in order for callers that don't care about grouping.
Op vs OpDestructive:
Op(...) for read-only or idempotent writes that can be safely retried.
OpDestructive(...) for actions the user does not want the LLM to fire by mistake — DELETE, send-message, post-comment, force-push, anything that touches money or user-visible state.
A destructive op is disabled by default on every new row. Admins opt in per (row, operation).
Description discipline
Operation.Description is the load-bearing signal the LLM uses to decide whether to call this op. It shows up verbatim in wick_list / wick_search payloads.
Write action verbs and be specific about input/output shape:
- ✅ "Search Loki using LogQL. Returns log lines with timestamp + labels. Empty result = empty array."
- ✅ "Send a Slack message to {channel}. Returns the posted message timestamp on success. Idempotent if {client_msg_id} is supplied."
- ❌ "query loki"
- ❌ "send slack"
ExecuteFunc
See internal/connectors/crudcrud/ for the canonical reference — five operations split across connector.go (handlers), service.go (validation, URL building, JSON body checks), repo.go (HTTP). One op is destructive; error wrapping and happy-path + sad-path coverage are in repo.go::doRequest.
Golden rules for ExecuteFunc
- MUST build requests with
http.NewRequestWithContext(c.Context(), ...) — never plain http.NewRequest. Without this, MCP cancellations (client disconnect, deadline, SSE per-call timeout in internal/mcp) cannot abort the upstream request and the goroutine leaks for the duration of whatever the upstream eventually returns. This is documented on pkg/connector/Ctx.Context and is the single most common bug in custom connectors.
- MUST validate
Input early before doing the HTTP call. Errors returned here become connector_runs.error_msg.
- MUST read configs via
c.Cfg(...) / inputs via c.Input(...). Never via process-level singletons or env vars at call time — that breaks the multi-row model.
- MUST use
c.HTTP as the starting client (carries a 30s default timeout from pkg/connector.DefaultHTTPTimeout). Replace it locally if you need a different transport, but document why.
- SHOULD wrap upstream errors with
fmt.Errorf("...: %w", err) so the chain reads cleanly in the history detail panel.
- SHOULD transform the upstream response into a typed struct or map shape that's stable across upstream changes. Returning the raw upstream body works but means LLMs see noise (envelopes, pagination cursors, debug fields) and break when upstream tweaks the shape. See
connectors-design.md § 10.1.
- SHOULD mark destructive operations with
OpDestructive(...) — the framework defaults the toggle off so it's an explicit admin opt-in.
- MAY emit progress with
c.ReportProgress(progress, total, message) for long-running calls. Safe to call from any goroutine; no-op on the JSON transport.
Anti-patterns
- ❌
http.NewRequest (no context) — goroutine leak.
- ❌ Returning
*http.Response.Body reader directly — the framework cannot marshal it. Always io.ReadAll + decode.
- ❌ Storing config values in package-level vars set at init — connectors are multi-row, every call must read fresh from
c.Cfg(...).
- ❌ Op keys with hyphens or spaces — slug only (
a-z0-9_).
- ❌ Sharing mutable state across
Execute invocations — connector calls are concurrent.
- ❌ Polling tight loops inside
Execute without honoring c.Context().Done().
Health check (optional Module.HealthCheck)
Connectors whose upstream uses granular permissions (Slack scopes, GitHub token scopes, Google OAuth scopes) SHOULD expose a HealthCheck hook. When non-nil, /manager/connectors/{key}/{id} renders a Check Permissions button that calls POST /manager/connectors/{key}/{id}/health-check → Service.RunHealthCheck → reconciles per-op system_disabled flags against the report.
func HealthCheck(c *connector.Ctx) ([]connector.OpHealth, error) {
granted, err := whoami(c)
if err != nil {
return nil, err
}
out := make([]connector.OpHealth, 0, len(opScopes))
for op, required := range opScopes {
ok, missing := evalScopes(required, granted)
h := connector.OpHealth{Key: op, OK: ok}
if !ok {
h.Reason = "needs scope: " + strings.Join(missing, ", ")
}
out = append(out, h)
}
return out, nil
}
Wire on the Module in registry.go:
connector.Module{
Meta: myconn.Meta(),
Configs: entity.StructToConfigs(myconn.Configs{}),
Operations: myconn.Operations(),
HealthCheck: myconn.HealthCheck,
}
Reconciliation rules (see internal/connectors/service.go::RunHealthCheck):
OpHealth.OK=true → clears system_disabled if previously set; admin's manual Enable/Disable preserved.
OpHealth.OK=false → sets system_disabled=true with Reason displayed inline; admin cannot enable until a later check passes.
- Ops omitted from the report are left untouched — neither locked nor cleared.
- Returning a non-nil error aborts the whole reconcile; existing flags survive.
Effective availability: Enabled AND NOT SystemDisabled (see OpState in service.go).
When to add: any connector where upstream permission is granular and Op.Description claims an action the credential might not be authorized for. Skip for connectors with a single all-or-nothing token (e.g. simple API key).
Row-level disable (separate from per-op and from healthcheck)
entity.Connector.Disabled is the row-level off-switch — flipped by the admin's "Disable Connector" button via POST /manager/connectors/{key}/{id}/disable → Service.SetDisabled (internal/connectors/service.go).
When Disabled=true:
- All
wick_execute against the row error out before ExecuteFunc runs.
- Row hides from
wick_list for non-admin callers.
- Per-op
Enabled / SystemDisabled flags untouched — re-enable restores prior op state.
Reversible. For permanent removal use the Delete action — different code path, different row state (hard delete via Service.Delete).
Three independent off-switches to keep separate when reasoning about availability:
Connector.Disabled (row-level, admin-controlled)
ConnectorOperation.Enabled=false (per-op, admin-controlled — destructive ops default off)
ConnectorOperation.SystemDisabled=true (per-op, healthcheck-controlled)
Effective op call passes only when all three resolve to "available".
Registration
Built-in wick connectors are registered in internal/connectors/registry.go::RegisterBuiltins() via registerOnce:
func RegisterBuiltins() {
registerOnce(connector.Module{
Meta: myconn.Meta(),
Configs: entity.StructToConfigs(myconn.Configs{}),
Operations: myconn.Operations(),
OAuth: myconn.OAuthMeta(),
})
}
Module.Operations is the []connector.Category returned by Operations() directly — no flattening at registration; the category tree is the canonical store.
Downstream projects use app.RegisterConnector(meta, configs, categories) from main.go — categories is the []connector.Category from the connector's Operations(). That's the path the downstream skill covers.
OAuth (user token acquisition)
Connectors that need per-user OAuth tokens (e.g., Slack user token xoxp-, Google OAuth, GitHub app) can opt into wick's generic OAuth framework by setting Module.OAuth.
When to use
Use Module.OAuth when:
- Your connector needs a user identity token (not a shared service account token).
- The user must explicitly grant permission via an OAuth consent page.
- Examples: Slack DM-as-user, Google Drive per-user, GitHub app installations.
Do not use for bot/service tokens pasted by admins — those go in Configs as a secret-tagged field.
How to add OAuth to a new connector
Step 1 — Create internal/connectors/myconn/oauth.go:
package myconn
import (
"context"
"github.com/yogasw/wick/pkg/connector"
)
func OAuthMeta() *connector.OAuthMeta {
return &connector.OAuthMeta{
AuthorizeURL: "https://myservice.com/oauth/authorize",
TokenURL: "https://myservice.com/oauth/token",
ExtraParams: map[string]string{},
Scopes: "read:user write:messages",
DisplayName: "MyService",
Icon: "🔗",
GetUserIdentity: func(ctx context.Context, accessToken string) (userID, displayName string, err error) {
},
}
}
Step 2 — Register with OAuth field in registry.go:
connector.Module{
Meta: myconn.Meta(),
Configs: entity.StructToConfigs(myconn.Configs{}),
Operations: myconn.Operations(),
OAuth: myconn.OAuthMeta(),
},
That's it. The manager UI and OAuth handler pick up everything automatically:
| What appears automatically | Where |
|---|
| "OAuth App" section (Client ID + Client Secret fields) | /manager/connectors/myconn list page (admin-only) |
| "Connect with MyService" button | /manager/connectors/myconn/{id} detail page (any user with access) |
| OAuth start + callback routes | GET /manager/connectors/myconn/oauth/start and .../callback |
| Token saved to connector row on success | Automatic via oauthSaveToken in internal/manager/oauth.go |
| In-memory token map refresh | Automatic via RefreshTokenMap on the Slack channel |
Step 3 — Add Redirect URI to provider app settings:
http://localhost:9425/manager/connectors/myconn/oauth/callback
Replace myconn with Module.Meta.Key and localhost:9425 with the production app_url from wick settings.
How the framework works (internals)
Admin sets Client ID + Secret
→ stored in configs table with owner "connector_oauth:{key}"
User clicks "Connect with MyService"
→ GET /manager/connectors/{key}/oauth/start?connector_id={rowID}
→ reads OAuthMeta.AuthorizeURL + Scopes
→ generates HMAC-signed state token (stored with 10-min TTL)
→ redirects to provider consent page
Provider redirects back
→ GET /manager/connectors/{key}/oauth/callback?code=...&state=...
→ validates state token
→ exchanges code via standard POST to provider token endpoint
→ calls OAuthMeta.GetUserIdentity(token) → (userID, displayName)
→ saves token to connector row (updates existing row or creates new one)
→ refreshes in-memory token map immediately
→ redirects back to /manager/connectors/{key}/{rowID}?oauth=success
Reference files
| File | Role |
|---|
pkg/connector/oauth.go | OAuthMeta struct + OAuthProvider interface definition |
internal/manager/oauth.go | Generic handler (start, callback, state management, token save) |
internal/connectors/slack/oauth.go | Slack reference implementation of OAuthMeta |
internal/manager/connectors.go | How mod.OAuth != nil drives the UI sections |
Caveats
- When
OAuthMeta.TokenURL is set, oauthCallback uses a standard HTTP POST exchange (generic — works for any provider). When TokenURL is empty, the Slack-specific slackgo path is used (backward-compatible; leave empty for Slack). If the response includes a refresh_token, the callback saves it to the refresh_token config key automatically.
GetUserIdentity is called with the access token, not the refresh token. Store the access token in the connector row; add a RefreshToken field to Configs if your provider issues long-lived refresh tokens.
- The stored token is written to the connector row's
user_token config key. Your Configs struct must have a UserToken string field tagged wick:"secret;..." for the admin UI to show it (and for c.Cfg("user_token") to work in operations).
- For providers that issue short-lived access tokens (e.g., Google OAuth — 1 hour TTL), implement lazy token refresh in
repo.go: try the API call with the stored token; on 401, POST to the token endpoint with grant_type=refresh_token + stored refresh_token, get a new access token, retry. Keep the new token in-memory for the current call — no persistence needed since refresh_token is long-lived. Return a clear error if refresh_token is missing: "Access token expired. Reconnect via Manager → Connectors → [Name] → Connect Account."
Bootstrap
Documented in connectors-design.md § 5.4. Highlights:
- First boot per
Meta.Key with CountByKey(key) == 0 → auto-create one empty row (Label = Meta.Name, Configs = "{}").
- Existing rows are never overwritten on subsequent boots.
- Two registrations with the same
Key are a fatal boot error.
- A registered
Key whose module has been removed is tolerated: row stays, marked "Module not registered" in the admin UI; calls return an error.
Custom connectors (admin-built) — category parity
Admin-built connectors (internal/connectors/custom/, stored as entity.CustomConnector rows) mirror the same category model. The Ops JSON column is a nested array of custom.DefCategory{Title, Description, Ops []DefOp} — NOT a flat []DefOp. There is no backward-compat parse: ParseOps decodes the nested shape only.
custom.FlattenOps(cats) / Draft.AllOps() give the flat op list; DefOp has no Category field (the section owns grouping, same as built-ins).
executor.go maps each DefCategory → connector.Category so a custom module is indistinguishable from a built-in at the registry/MCP layer.
- curl / manual defs: sections are stored and editable in the builder (named sections + drag-drop ops between them).
- MCP defs: ops come live from the server's
tools/list, so they land in a single untitled section (toolsToCats) — not re-groupable in the builder today.
ValidateDraft requires globally-unique op keys across all sections (keys map to MCP tool ids) and unique non-empty section titles.
The builder FE (fe/manager/src/lib/components/custom/) renders section blocks with add/remove section, drag-drop ops, and a nested Jump panel.
MCP surface — what to know when editing
The MCP layer (internal/mcp/) exposes a fixed meta-tool set: wick_list, wick_search, wick_get, wick_execute. Connector instances are not advertised as N×M static tools. That choice is deliberate (see connectors-design.md § 7.3) — adding or removing a connector row never invalidates the LLM client's cached tool list.
When editing the MCP layer:
wick_execute resolves tool_id of the form conn:{connector_id}/{op_key} back to a single Service.Execute call.
- Auth check (
IsVisibleTo(connectorID, tagIDs, isAdmin)) and operation enable state are re-validated on every wick_execute and wick_get — never trust list-time caching.
- Streamable HTTP (Accept: text/event-stream) is supported for progress events. JSON transport is the default.
- Auth modes (PAT, OAuth access, OAuth refresh) are dispatched in
internal/mcp/auth.go by token prefix (wick_pat_, wick_oat_, wick_ort_).
Connector runs (audit trail)
Every Execute call (MCP, panel-test, retry) writes a row to connector_runs. See connectors-design.md § 5.3 for the full schema.
The retention job internal/jobs/connector-runs-purge is the in-tree example of a System-tagged auto-enabled job. When touching that job:
- It is registered from BOTH
internal/pkg/worker/server.go (for the scheduler tick) and internal/pkg/api/server.go (so /admin/jobs and /manager/jobs see the row). Skipping either breaks the surface.
- It carries
tags.System with IsSystem=true; IsFilter=true; IsGroup=true — protected by access-control mechanics in connectors-design.md § 9.8.
Meta.AutoEnable=true causes manager.Service.Bootstrap to call repo.ForceEnable on every restart so the job is always live.
Verifying your work
- Compile:
go build ./...
- Templ + Tailwind regen (only if you touched
internal/connectors's manager UI under internal/manager/...):
templ generate
./bin/tailwindcss* -i web/src/input.css -o web/public/css/app.css --minify
- Boot the lab binary, smoke-test, kill the port (per the AGENTS.md one-shot flow):
go run main.go server &
- MCP smoke (optional):
- Generate a Personal Access Token at
/profile/tokens.
- Use cURL to hit
POST /mcp with Authorization: Bearer wick_pat_xxx and {"jsonrpc":"2.0","method":"tools/list","id":1}. Expect the four wick_* tools.
- Call
wick_list to see the new connector's tool_id entries.
When to ask before acting
- New connector that needs per-user OAuth tokens — ask: (a) does it use standard OAuth 2.0 authorization code flow? (b) what scopes are needed? (c) does the provider return a
user_id from a "who am I" endpoint after token exchange? (d) are refresh tokens issued and how long do they live? If all yes → use Module.OAuth + OAuthMeta. If non-standard (e.g., device flow, PKCE-only, custom token endpoint) — confirm before building.
- Removing an existing connector or operation — confirm: removing an op orphans
connector_operations rows and breaks any active MCP client that listed the old tool_id. Migration plan needs to land in the same change.
- Adding an operation that needs
multipart/form-data upload — wick's connector path is JSON-first; this is doable but uncommon, flag it.
- Adding
IsFilter tags — never on your own initiative.
- Touching
internal/mcp/ JSON-RPC dispatch — this is shared by every connector; pause and confirm scope before editing.
- Changing the
pkg/connector public API — downstream projects depend on it; treat as a breaking change.
Reference