interface AdapterModule extends Module {
generateDefinition(input: string): Promise<ServiceDefinition>;
hydrateService(state: ServiceState): Promise<void>;
dehydrateService(id: string): Promise<void>;
invoke(input: InvokeInput): Promise<unknown>;
}
Adapter modules translate service definitions and tool invocations into
calls to the underlying end service. See
Adapter Modules for the user-facing
description.
Convert a raw definition string (an OpenAPI document, a custom schema, a
URL to fetch and parse, whatever this adapter understands) into a
ServiceDefinition.
- Called from
POST /services (direct install), POST /services/install
(registry install), PATCH /services/:serviceId (direct update),
POST /services/:serviceId/update (registry update), and
during PATCH /modules/:moduleId / POST /modules/:moduleId/update
to regenerate every service that targets the updated adapter.
- Expected to be pure, given the same input string, return the same
definition. Adapters should not rely on instance state here.
- Throw to reject the install. The host returns the thrown message in
the error response.
- Populate
adapterDomain (on the service and on each tool) with
whatever the adapter needs at invoke time. Treat it as your private
scratch space. The host never inspects it.
The host validates that the service id and every tool id is a valid
TypeScript identifier before persisting.
hydrateService(state)
Receive a snapshot of one service. Called when:
- A service that targets this adapter is enabled.
- Configuration or secrets on an already-enabled service are patched.
- This adapter is activated while the service is already enabled.
interface ServiceState {
id: string;
adapterDomain: Record<string, unknown>;
tools: Record<string, ToolState>;
config: Record<string, unknown>;
secrets: Record<string, unknown>;
}
Implementations should replace any internal state they kept for
state.id, not merge into it. secrets is decrypted plaintext; do not
log it.
Throw to reject the operation, the host rolls back enabled on the
service before surfacing the error.
dehydrateService(id)
Drop any internal state for service id. Called when:
- A service is disabled.
- A service is deleted.
- A service is updated (immediately before the new tools are inserted).
Should be idempotent. If the adapter never saw the service, return
without error.
interface InvokeInput {
serviceId: string;
toolId: string;
parameters: Record<string, unknown>;
}
Perform one tool call. The host has already verified:
- The service exists and is enabled.
- The tool exists and is enabled.
- The service is owned by this adapter.
The adapter is free to validate parameters against the stored
inputSchema itself (recommended) before issuing the outbound call. The
returned value is whatever the environment receives back from tool invokes.
Throw on any failure. Throwing an Error with a useful message is fine;
the host surfaces it to the calling process.
Security
Adapters run with full host privileges and receive decrypted secrets.
See Security → Adapter modules for
operator-facing guidance and what a well-behaved adapter must not do.
Minimal Skeleton
import type { AdapterModule, InvokeInput, ModuleSetupContext, ServiceDefinition, ServiceState } from "@cyrnel/sdk";
class MyAdapter implements AdapterModule {
private state = new Map<string, ServiceState>();
async setup(_context: ModuleSetupContext) {}
async teardown() { this.state.clear(); }
async generateDefinition(input: string): Promise<ServiceDefinition> {
return {
name: "...",
description: "...",
configSchema: { type: "object", properties: {} },
secretsSchema: { type: "object", properties: {} },
adapterDomain: { /* parsed metadata */ },
tools: [
{
id: "ping",
name: "ping",
description: "Health check",
inputSchema: { type: "object" },
outputSchema: { type: "object" },
adapterDomain: { /* per-tool routing info */ },
},
],
};
}
async hydrateService(state: ServiceState): Promise<void> {
this.state.set(state.id, state);
}
async dehydrateService(id: string): Promise<void> {
this.state.delete(id);
}
async invoke(input: InvokeInput): Promise<unknown> {
const svc = this.state.get(input.serviceId);
if (!svc) throw new Error(`unknown service ${input.serviceId}`);
// ...use svc.adapterDomain / svc.config / svc.secrets to issue the call
return { ok: true };
}
}
export function instantiate(): AdapterModule {
return new MyAdapter();
}
Note: The manifest metadata (name, version, type, etc.) goes in
module.json for custom modules. Only built-in modules ship export const manifest from code. The skeleton above omits it, see
Writing a custom adapter module
for a step-by-step walkthrough and a full working example.
Last modified on June 24, 2026