如何在缺少連接器時,為 AI Agent 建立 TypeScript 自訂整合
How to Build a Custom Integration for Your AI Agent When the Connector Doesn't Exist (TypeScript)
Corsair 的 TypeScript 教程示範在缺少現成連接器時,如何用插件腳手架為 AI Agent 建立自訂整合。腳手架可透過 `pnpm run generate:plugin MyService` 生成,並設定插件註冊、API key 或 OAuth 2 認證,以及 bundler 和套件設定。
When Your Agent Needs an App Nobody's Built Yet
Sooner or later, an agent needs to talk to an app that nothing in your stack has built a connector for. Maybe it is a niche CRM, an internal tool, or a newer API no integration platform has gotten to yet. At that point there are really only two paths: hand roll a one off TypeScript client, or use the plugin scaffolding an open source integration framework already provides and get the same structure every other plugin uses.
This post walks through the second path end to end: scaffolding the plugin, typing every endpoint so an agent cannot misuse it, wiring up auth without a single hardcoded secret, and contributing the result back so the next person who needs that same API does not have to repeat the work. It does not try to teach SDK design theory from first principles, there are whole books on that. The goal here is the practical shape: what a production integration needs that a quick script usually skips, and why.
The Real Cost of Building It Yourself
Before scaffolding anything, it is worth running a quick test: is this integration a one off call your product needs once, or something you will need typed, authenticated, and maintained across every tenant your product serves. That answer changes how much structure is worth building up front.
The first 80 percent of any custom API integration is usually the easy part: reading the provider's docs, mapping a handful of endpoints, and getting a working call. The remaining 20 percent is where the real engineering hours go: rotating OAuth tokens before they expire, verifying webhook signatures on every incoming event, respecting rate limits without silently dropping requests, and multi tenant credential isolation so one tenant's token can never leak into another's calls. None of that shows up in the first proof of concept, and all of it shows up the week something breaks in production.
This is the actual build vs buy integrations decision, and it is less about whether you can write the HTTP calls, you can, and more about whether you want to own that 20 percent indefinitely. A closed platform takes that maintenance off your plate, but it also means waiting on someone else's roadmap the next time you need an API they have not built yet.
Open source integrations change that calculus without removing the work entirely. You are not stuck waiting on a vendor, since you can browse the existing plugin catalog and, if the app you need is not already covered, scaffold it yourself in the same structure every other integration uses. You are also not starting from a blank file: the scaffolding, typing conventions, and auth patterns below are what that structure looks like in practice.
Scaffolding a Plugin in One Command
A hand built integration usually starts as a single file with a handful of fetch calls. That works until the second endpoint, the first webhook, or the first time a second tenant needs isolated credentials, at which point you are rebuilding the same TypeScript SDK structure every integration eventually converges on:
packages/<plugin-name>/
├── index.ts # entry point, auth setup
├── client.ts # HTTP client with auth headers
├── error-handlers.ts # error classes and status mappers
├── endpoints/ # typed API operations by resource
├── webhooks/ # webhook signature verification and event routing
└── schema/ # Zod schemas for anything cached locally
A generator produces that same layout in one command instead of by hand:
pnpm run generate:plugin MyService
Running it does more than create empty folders. It registers the plugin in the framework's core constants so it is importable immediately, wires up the base authentication pattern for API key or OAuth 2 flows, and sets up the bundler and package config so the workspace recognizes it as a real package rather than a loose folder of files.
The structure itself is doing real work here, independent of whichever API you are wrapping. Every plugin built this way looks the same, so a developer who has worked on one integration already knows where to find the webhook handler or the Zod schema on the next. It also turns out to matter for AI agent tools specifically: a predictable scaffold is something a coding agent can inspect and fill in correctly, since it already knows what client.ts or endpoints/ is supposed to contain, rather than reverse engineering a one off file's conventions from scratch. Corsair's plugin generator follows exactly this layout, which is worth a look even if you end up building the scaffold by hand in your own stack.
Typing Every Endpoint So the Agent Can't Misuse It
An agent calling your integration is still just calling a function, which means the function signature is the only thing standing between a correct call and a malformed one. This is where API client design decisions matter more for an agent than for a human developer, since an agent has no instinct for noticing a field quietly went missing. Grouping endpoints into domain namespaced, type safe endpoints fixes the discoverability problem and the typing problem at once:
const sdk = new MyServiceClient({ apiKey });
await sdk.customers.list({ limit: 20 });
await sdk.invoices.create({ customerId, amount });
Typing sdk. surfaces every resource the IDE knows about, and each method carries its own request and response contract rather than a generic options bag. Request types (CreateInvoiceRequest, ListCustomersRequest) and generic response wrappers (APIResponse<T>, PaginatedResponse<T>) keep the shape of what comes back predictable. If the provider already publishes an OpenAPI spec, OpenAPI code generation with a tool like openapi-typescript or Fern can produce these interfaces directly instead of hand typing every one, which is worth doing before writing anything by hand.
Static types disappear at runtime, though, and a third party API can change its response shape without warning. That is the case for Zod validation at the boundary rather than trusting the compiler alone: a schema catches a malformed or drifted payload before it reaches the agent's reasoning step, and infers its TypeScript type directly, so the interface and the validator never drift apart. The tradeoff is real: Zod adds a small runtime cost and a schema written too strictly will reject a harmless new field the provider adds later, so schemas are usually best kept permissive on fields you do not explicitly need.
Errors and retries deserve the same treatment as responses. A centralized request runner that maps HTTP status codes to a standardized SDKError, carrying an explicit code like RATE_LIMIT_ERROR or AUTHENTICATION_ERROR, gives an agent something it can actually branch on. That same runner is also where error handling retry logic belongs: transient failures like timeouts, 503s, and 429s get retried with backoff, while a 401 or a 400 is surfaced immediately rather than retried blindly.
Wiring Up Auth Without a Single Hardcoded Secret
Secrets belong in environment variables, injected at startup, never typed into a source file. The client constructor is the right place to enforce that: validate that the required keys are present the moment the client is created, and fail loudly before any network call goes out, rather than failing confusingly three requests later.
OAuth token refresh is where most custom integrations quietly break. Three things need to happen together.
Preemptive expiry checking stores the token's expiry timestamp when it arrives, and refreshes proactively once the current time gets within a few minutes of it, rather than waiting for a request to fail first.
Reactive retry on 401 catches the case where a request comes back unauthorized anyway, perhaps because a clock drifted or a token was revoked early, refreshes, and retries the original request once.
A single flight lock around the refresh itself matters once concurrency enters the picture: if several requests discover an expired token at the same moment, only one of them should actually call the token endpoint. The rest wait on that same in flight promise, since firing several simultaneous refresh calls against a provider that rotates refresh tokens can get some of those requests rejected or revoke the session outright.
if (Date.now() >= expiresAt - BUFFER_MS) {
await (refreshPromise ??= refreshToken());
}
In a real multi tenant product, none of this should be visible to the agent at all. Multi tenant credentials get resolved internally, scoped per tenant, at the moment a call is made, which is also the pattern Corsair's hosted and self hosted setup uses so an agent only ever sees method names and results, never the underlying token.
Shipping It Back Instead of Waiting on a Roadmap
Contributing to open source here follows the same shape as contributing anywhere else: claim the work before you start it. Most open source integration frameworks run a tracking board or an open issue list specifically so two contributors do not spend a weekend building the same Notion plugin independently. Opening an issue with the target API's docs, the endpoints you plan to cover, and the auth type it needs is usually enough to lock it under your name.
A pull request against an established framework has to clear a specific, unglamorous bar before anyone reviews the logic: a type check, a lint pass, and a plugin validation script that confirms the directory contract was followed. None of that is about whether your idea is good, it is about whether the next person who opens your plugin finds what they expect to find.
The actual review is where thin implementations get caught. Reviewers reject auth that is really just a header mapping hack dressed up as OAuth support, and they reject broad as any casts that quietly turn a typed endpoint back into an untyped one. Both defeat the point of the scaffold.
What you get in exchange for that bar is the thing a closed platform cannot offer: you are not filing a feature request and waiting, you are shipping the integration yourself, and the next developer who needs that same API gets it for free. Corsair is built around exactly that model: every plugin ships typed and pre verified, self hosting is free, and credentials never leave your own database. If an integration you need is not covered yet, you can scaffold and contribute it rather than wait on a vendor's roadmap. Take a look at the current plugin catalog and try it yourself at corsair.dev.
Frequently Asked Questions
Is it worth building a custom integration for a single API call, or should I just write it inline? For a genuine one off call with no auth beyond a static API key, a typed function is enough. The scaffolding and structure described here pay off once you need the integration authenticated, multi tenant, or maintained past the first release.
What is the actual advantage of Zod over just writing TypeScript interfaces? TypeScript types disappear at runtime and give no protection against a payload that does not match what you expected. Zod validates the response as it arrives and infers the TypeScript type from the same schema, so a provider's API drifting silently gets caught instead of corrupting data further down the pipeline.
How do I stop multiple refresh token requests from firing at once? Wrap the refresh call in a single shared promise. The first request that notices an expired token kicks off the refresh, and every other concurrent request awaits that same promise instead of calling the token endpoint again, which also avoids tripping providers that rotate and invalidate refresh tokens on reuse.
What do open source maintainers actually reject in a new plugin PR? The two most common rejections are authentication that is really just a header mapping shortcut rather than real OAuth handling, and broad type assertions like as any that bypass the typing the rest of the framework relies on.
Do I need webhook support to contribute a plugin, or is read only enough? Read only is enough if the provider's API has no events worth reacting to, or if your use case genuinely does not need them. If the provider does support webhooks and your integration omits them, reviewers will usually ask for signature verification and event routing before merging.
來源:DEV Community · dev.to
