> ## Documentation Index
> Fetch the complete documentation index at: https://actelos.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Writing a Custom Adapter Module

> Skeleton and example for building a custom adapter

This page covers the adapter-specific parts of writing a custom module.
For the shared mechanics (file layout, registration, lifecycle) see
[Writing a custom module](/cyrnel/module-specs/writing-custom-modules). For the full
interface reference see [`AdapterModule`](/cyrnel/module-specs/adapter-module).

## Minimal Skeleton

```ts theme={null}
import type { AdapterModule, InvokeInput, ModuleSetupContext, ServiceDefinition, ServiceState } from "@cyrnel/sdk";

class MyAdapter implements AdapterModule {
  private state = new Map<string, ServiceState>();
  private logger: ModuleSetupContext["logger"] | null = null;

  async setup(context: ModuleSetupContext) {
    // Configure reduction for yourself from your own config field, then scope
    // a base phase. The host manages the rest of the correlation metadata.
    const patterns = (context.config.redactionPatterns as string[] | undefined) ?? [];
    this.logger = context.logger.redact(patterns).child({ phase: "setup" });
    this.logger?.info({ event: "adapter-ready" }, "Adapter initialized");
  }
  async teardown() { this.state.clear(); }

  async generateDefinition(input: string): Promise<ServiceDefinition> {
    return {
      name: "...",
      summary: "Short plain-text description",
      description: "...",
      configSchema: { type: "object", properties: {} },
      secretsSchema: { type: "object", properties: {} },
      adapterDomain: { /* parsed metadata */ },
      tools: [
        {
          id: "ping",
          name: "ping",
          summary: "Answers with a health status",
          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}`);
    const logger = this.logger?.child({
      serviceId: input.serviceId,
      toolId: input.toolId,
      phase: "invoke",
    });
    logger?.info({ event: "invoke-start", parameters: input.parameters }, "Invoking tool");
    // ...use svc.adapterDomain / svc.config / svc.secrets to issue the call
    return { ok: true };
  }
}

const configSchema = {
  type: "object",
  properties: {
    redactionPatterns: {
      type: "array",
      items: { type: "string" },
      description:
        "Path patterns (dot/bracket notation) merged additively with the host-enforced baseline for this module's logs.",
    },
  },
} as const;
const secretsSchema = { type: "null" } as const;

export default { configSchema, secretsSchema, instantiate: () => new MyAdapter() };
```

## Logging

The host owns all logging - your module never creates a logger. The logger it
receives in `setup` already carries your module's identity (`moduleId`,
`moduleType`, `adapterId`), and every entry is tagged `type: "module"`.

* Call `context.logger.redact(patterns)` to opt into **self-managed
  reduction**. Declare `redactionPatterns` in your own `configSchema` and read
  it back from `context.config`: the host never supplies patterns for you.
  Your patterns merge additively on top of a non-disableable baseline
  (secrets / tokens / passwords / authorization).
* Use `logger.child({ ... })` to scope a logger for a phase or a specific
  invocation. `child` accepts only `phase`/`event`; the host-owned correlation
  fields (`serviceId`, `toolId`, `executionId`, …) are merged in by the host
  and cannot be forged.
* Emit structured logs with `logger.info({ event, ...payload }, "message")`.
  All six levels are available: `trace`, `debug`, `info`, `warn`, `error`,
  `fatal`.

## Full Example

See the [HTTP adapter example](https://github.com/actelos/mci/tree/main/examples/adapter-module)
in the repository for a complete, working adapter that accepts a JSON service
definition describing REST endpoints and calls them over HTTP. It stores
per-service base URLs in `adapterDomain`, reads secrets for
`Authorization`, and routes each invocation to the correct endpoint.

See [`ServiceState`](/cyrnel/module-specs/types#servicestate-and-toolstate) for the shape
of the snapshot the host delivers to `hydrateService`, and
[`AdapterModule`](/cyrnel/module-specs/adapter-module) for the full interface contract.
