.agents/skills/add-integration/SKILL.md
Use when adding a new external API integration to Switchboard, scaffolding an integration adapter, or deciding between SDK vs raw HTTP for a new service. Not for modifying existing integrations or fixing bugs in current adapters.
npx skillsauth add daltoniam/switchboard add-integrationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Full lifecycle for adding a new integration adapter to Switchboard.
See AGENTS.md for interface contracts, project structure, and conventions referenced below.
Before writing code, answer these questions:
listAfter, collection parents use setParent)Undocumented APIs: When working with internal or undocumented APIs (no OpenAPI spec, no public docs), intercept the first-party client's network traffic to discover expected payload shapes. For web apps: monkey-patch window.fetch in Chrome DevTools to log request bodies. For mobile apps: use a MITM proxy. This is often the only way to discover commands, required fields, and transaction formats.
| Criteria | Use Typed SDK | Use Raw HTTP | |----------|--------------|--------------| | Go SDK exists and maintained | Yes | - | | SDK covers needed endpoints | Yes | - | | No Go SDK available | - | Yes | | SDK exists but poorly typed or incomplete | - | Yes | | API is GraphQL | - | Yes (hand-rolled queries) |
Existing precedent: GitHub, Datadog, Slack use typed SDKs. Linear, Sentry, Metabase use raw HTTP.
One tool per API operation. Follow naming and dispatch conventions in AGENTS.md > Conventions and Patterns.
| Tool count | Structure |
|-----------|-----------|
| < 30 | 1-2 handler files + tools.go + <name>.go + <name>_test.go |
| 30-60 | 3-5 handler files split by domain (see sentry/) |
| 60+ | 5+ handler files (see github/, datadog/) |
Tool descriptions are scored by a TF-IDF search engine with synonym expansion. They're the ONLY text an LLM sees when deciding which tool to use. Write them for discoverability, not just accuracy.
Three-tier pattern (from the GitHub adapter — the gold standard):
Entry points — tools users search for first:
"List error and exception issues for a project. Start here for error tracking, debugging, and finding unresolved bugs or crashes."
Drill-down tools — used after entry points:
"Get details of a specific error issue, including stacktrace and debugging context. Use after list_issues."
Action tools — mutations with chaining hints:
"Update an error issue (resolve, assign, triage). Use after list_issues or get_issue."
Rules:
server/search.go — common plurals are covered there, but new domain words need explicit plural coverage either in the description or as a synonym group.Entry-point guidance (MUST): Every integration's first/primary tool MUST include "Start here" in its description. This is wayfinding — models use it to orient when browsing an unfamiliar integration. Examples:
"List all schemas in the database. Start here for schema discovery.""List all projects in the PostHog organization. Start here to discover projects."Naming deviation callouts: When the integration uses non-standard verb prefixes (e.g., Notion uses retrieve_* where others use get_*), add the standard verb in parentheses: "Retrieve (get) a page's metadata". This helps both search scoring and models that skip search.
Noun synonym awareness: The search engine has noun synonym groups (table/tables, label/labels/tag/tags, diff/patch/changes, database/databases/db, etc.) in server/search.go. When adding tools with domain-specific nouns, check if your nouns are covered. If not, either add a synonym group or include both forms in the description.
IDF dilution warning: Avoid creating synonym groups where the union covers too many tools (>60). High-union groups dilute IDF scores and can cause regressions. Verb groups are safe because MAX-per-word scoring means rare synonyms carry the score. Noun groups need more care — test with /search-benchmark before and after.
Anti-patterns:
"List issues for a project" — too generic, no domain keywords"Get a specific message by ID" — no "email", "mail", or "read""List events with optional filters" — what kind of events? For what purpose?Verify with benchmark: After adding tools, run /search-benchmark to check
that your tools surface for natural-language queries users would actually type.
| Auth type | Pattern | Example adapter |
|-----------|---------|-----------------|
| API key / token | Header in doRequest | metabase/ (x-api-key), sentry/ (Bearer) |
| OAuth token via SDK | SDK transport/config | github/ (oauth2), datadog/ (context keys) |
| Session token + cookie | Custom http.RoundTripper | slack/ (cookieTransport) |
| OAuth setup flow | Separate oauth.go file | github/, linear/, sentry/, slack/ |
Add an OAuth flow when the API supports it and you want guided credential setup in the Web UI. Get basic token auth working first. Grant type depends on the API: Device Flow for headless, PKCE for browser-redirect. Add a corresponding setup page in web/templates/pages/.
Reference AGENTS.md > Adding a New Integration for the 7-step mechanical checklist.
Focus here on judgment calls:
Configure() is where you reject invalid state. Validate eagerly, fail on missing credentials — never let an unconfigured adapter reach Execute().
func (x *myapi) Configure(creds mcp.Credentials) error {
x.apiKey = creds["api_key"]
if x.apiKey == "" {
return fmt.Errorf("myapi: api_key is required")
}
// For services with a fixed base URL, hardcode a default (see sentry/)
// For services where URL varies, require it (see metabase/)
if v := creds["base_url"]; v != "" {
x.baseURL = strings.TrimRight(v, "/")
}
return nil
}
Implement a lightweight API call that verifies credentials work (e.g., "get current user" or "list with limit=1"). Must handle the case where Configure() hasn't been called yet (nil client) — return false, don't panic.
Follow AGENTS.md > Error Handling. Key judgment: surface errors to the caller — never swallow them, never add fallback defaults.
Add integration-specific helpers when a pattern repeats 3+ times within an adapter:
integrations/sentry/org())integrations/linear/resolveTeamID())integrations/sentry/queryEncode())Note: arg helpers are shared from args.go — use mcp.NewArgs(args) reader for bulk extraction or standalone mcp.ArgStr/mcp.ArgInt/etc. for conditional fields. NEVER define local argStr/argInt in adapters. Use r.OptInt("page", 1) for pagination defaults. See docs/go-anti-patterns.md for extraction pitfalls. Result constructors (mcp.JSONResult, mcp.RawResult, mcp.ErrResult) are shared from the root package. Some adapters wrap mcp.ErrResult in a local errResult to inject retry semantics.
Every adapter must have these test categories (see existing *_test.go files):
New() returns valid integration, Name() matchesName(), no duplicates. Descriptions follow the three-tier pattern (see "Tool Description Quality" above)/search-benchmark — new tools surface for natural-language queries. Check synonym coverage with existing synonymGroups in server/search.goTestDispatchMap_AllToolsCovered — every Tools() entry has a dispatch handlerTestDispatchMap_NoOrphanHandlers — every dispatch key has a ToolDefinitionIsError: true, "unknown tool" in Datahttptest.NewServer for success, API errors (>=400), 204 no-contentmcp.NewArgs(args) reader with r.Err() check — type coercion is tested in root args_test.go. TestNewArgs_ErrCheckParity automatically covers new adaptersFollow AGENTS.md > Adding a New Integration steps 6-7 (register + config defaults), then verify:
go build ./... && go test ./... && go vet ./... && go tool golangci-lint runsearch for new integration tools, execute oneNew adapters should implement FieldCompactionIntegration to keep list/search responses compact.
Contract vs implementation: The interface contract is CompactSpec(toolName string) ([]CompactField, bool) defined in mcp.go. How you build the specs is an implementation detail — integrations/github/compact_specs.go uses a raw string map parsed at init, but adapters can construct CompactField slices however they want.
Optimize specs for fewest total tokens across the entire task workflow, not smallest single response. A field that prevents an N+1 drill-down saves ~5KB per item even if it costs 50 bytes in the compacted list. Distribution of tokens across 1 or N calls doesn't matter as long as N is small enough that network latency doesn't dominate timing. The goal is a finite minimum token budget for any given workflow — get as close to it as possible.
Example: requested_reviewers[].login adds ~80 bytes per PR to the compacted list, but without it "which PRs need review?" requires a separate list_requested_reviewers call per PR (~3KB each). For 20 open PRs: +1.6KB in compacted list vs. +60KB in drill-down calls.
integrations/<name>/compact_specs.go with rawFieldCompactionSpecs map and mustBuildFieldCompactionSpecs init (copy pattern from integrations/github/compact_specs.go)CompactSpec(toolName string) ([]CompactField, bool) method on the adapter structvar _ mcp.FieldCompactionIntegration = (*myapi)(nil)TestFieldCompactionSpecs_NoOrphanSpecs — every spec key must exist in dispatchresp.Items not resp) so field compaction operates on the array directlyFor each tool's spec, verify against these questions before finalizing:
requested_reviewers means "which PRs need review?" requires N extra calls.additions/deletions on GitHub's List PRs API return 0/null).status without conclusion in CI runs is incomplete. additions without deletions in PRs is half the story. Include paired fields together or not at all.id, number, or sha — whatever the get tool requires — is included.Canonical example: integrations/github/compact_specs.go
| Mistake | Correct approach |
|---------|-----------------|
| Defaulting missing credentials | Return error from Configure() |
| Returning Go error for API failures | Use ToolResult{IsError: true}, nil Go error |
| Skipping dispatch parity tests | Non-negotiable — tests catch tool/handler drift |
| Pre-building helpers before duplication | Wait for 3+ uses, then extract |
| Duplicating AGENTS.md content in handlers | Read AGENTS.md for conventions |
| Adding OAuth before basic auth works | Get token-based auth working first, add OAuth flow after |
| Returning full SDK wrapper for lists | Unwrap to inner slice, declare compaction specs |
| No default page size on list tools | Use sensible defaults (10-50 items) |
tools
Cross-model search quality benchmark for Switchboard's tool discovery. Dispatches identical search scenarios to opus, sonnet, and haiku in parallel, compiles a comparison table, and identifies optimization opportunities. Use when: "benchmark search", "test search quality", "run search benchmark", after changing scoring logic, synonyms, stop words, IDF, or tool descriptions, after adding new integrations, or when evaluating Phase 2 tag impact. Also use when the user mentions "search hit rate", "search recall", or "did search get better/worse". Not for full MCP smoke tests (use mcp-benchmark) or unit testing (use make test).
tools
Review a GitHub pull request for the Switchboard Go MCP server project. Enforces idiomatic Go, project conventions (hexagonal architecture, dispatch maps, port interfaces), test coverage, build/lint verification, and production readiness.
tools
Submit a PR review as inline GitHub comments on specific files and lines using the gh CLI.
tools
Improve an existing Switchboard integration adapter's LLM usability — tool description enrichment, field compaction refinement, and response tuning. Use when: "optimize integration", "improve tool descriptions", "extend compaction", "make integration better for LLMs", after user story mapping, or when an LLM is making wrong tool choices or passing wrong IDs. Not for adding new integrations (use add-integration) or fixing bugs.