The interface is localized. Skill descriptions and documents are machine-translated by AI into your language; code, commands, paths and technical terms stay in the original, and the author's document governs.
Explore skills
Data & research · SKILL.md

add-provider

add-provider

Add a new storage or service provider to the core package. Use when implementing a new backend for StorageService (e.g., a new database) or a new service provider (e.g., a new LLM backend).

Independent GitHub snapshot · Automated classification · Original documentation governs

Ccyanheads / mcp-ts-coreApache-2.0

Skill documentation

GitHub

name: add-provider description: > Add a new storage or service provider to the core package. Use when implementing a new backend for StorageService (e.g., a new database) or a new service provider (e.g., a new LLM backend). metadata: author: cyanheads version: "1.2" audience: internal type: reference

Context

Providers implement interfaces defined in core. They are selected at runtime via config (e.g., STORAGE_PROVIDER_TYPE). Tier 3 providers lazy-load their dependencies to keep the core bundle small.

Providers live inside the package source tree — import the interface via relative path (e.g., import type { IStorageProvider } from '../core/IStorageProvider.js'), not via the package subpath exports (those are for consumers).

Provider interfaces

DomainInterface file
Storagesrc/storage/core/IStorageProvider.ts
LLMsrc/services/llm/core/ILlmProvider.ts
Speechsrc/services/speech/core/ISpeechProvider.ts

Read the relevant interface fully before implementing — each has distinct required members. ISpeechProvider in particular requires readonly name, readonly supportsTTS, readonly supportsSTT, and healthCheck() in addition to the capability methods; these flags drive routing in SpeechService.

File conventions

Provider file location and naming differ by domain:

  • Storage — nested subdirectory, camelCase directory name, PascalCase-suffixed provider file. Each provider gets its own subdirectory for the provider file plus any co-located types: src/storage/providers/{{providerName}}/{{providerName}}Provider.ts (e.g., src/storage/providers/inMemory/inMemoryProvider.ts, src/storage/providers/supabase/supabaseProvider.ts + supabase.types.ts)

  • LLM / Speech — flat directory, kebab-case with .provider.ts suffix: src/services/llm/providers/{{provider-name}}.provider.ts src/services/speech/providers/{{provider-name}}.provider.ts (e.g., src/services/llm/providers/openrouter.provider.ts, src/services/speech/providers/elevenlabs.provider.ts)

Steps

  1. Identify the provider interface — read the interface file for the target domain (see table above).

  2. Create the provider file following the file convention for its domain (see above).

  3. Implement the interface — all methods must be implemented. Storage providers build on src/storage/core/providerHelpers.ts rather than re-deriving what the existing providers share:

    • serializeValue / encodeEntries — the JSON guard every provider runs before it writes. serializeValue returns the value's JSON text and throws McpError(SerializationError) for a bigint, a cyclic reference, or a top-level undefined, function, or symbol. encodeEntries encodes a whole setMany batch first, so one bad value rejects the batch before anything is written. Store and read back the JSON form; never keep the caller's object.
    • getManyViaGet / setManyViaSet / deleteManyViaDelete — the batch methods as a parallel fan-out of single-key operations, for backends with no native batch API. Pass setManyViaSet the output of encodeEntries and a private per-key write. Calling this.set per entry would let a bad entry fail after the others have landed.
    • encodeEnvelope / decodeEnvelope — the TTL envelope for backends with no TTL of their own (R2, filesystem). encodeEnvelope takes the JSON text from serializeValue and returns the stored document. decodeEnvelope returns { kind: 'expired' } so the provider can delete on read, returns pre-envelope JSON as a plain value, and throws SyntaxError on invalid JSON so the provider can attach the key to the error it raises.
    • paginateSortedKeys — one list() page over an already-sorted key set, with the cursor for the page that follows.
    • escapeLikePattern — escapes %, _, and \ in a prefix before a SQL LIKE.
  4. Lazy-load dependencies if Tier 3:

    let _client: SomeClient | undefined;
    async function getClient(): Promise<SomeClient> {
      if (!_client) {
        const { SomeClient } = await import('some-package');
        _client = new SomeClient(/* config */);
      }
      return _client;
    }
    
  5. Register the provider — the registration point differs by domain:

    • Storage — two changes required:

      1. Add the new provider string to the z.enum for STORAGE_PROVIDER_TYPE in src/config/index.ts — without this, the config schema rejects the env var at runtime.
      2. Add a case to the switch in src/storage/core/storageFactory.ts inside createStorageProvider(). Import the new provider class at the top of that file.
    • Speech — two changes required:

      1. Add the new provider string literal to the provider union in SpeechProviderConfig (src/services/speech/types.ts, field provider).
      2. Add a case to the switch in createSpeechProvider() (src/services/speech/core/SpeechService.ts). Import the new provider class at the top of that file.
    • LLM — currently only one provider exists (OpenRouterProvider); it is instantiated directly in src/core/app.ts rather than through a factory switch. There is no factory pattern yet — adding a second provider requires introducing one (a selector env var, a factory function, and a conditional in app.ts). Read src/core/app.ts to understand the current instantiation site before designing the wiring.

  6. Update the Worker-compatible provider list if the new storage provider runs in Cloudflare Workers. The list is an inline array in storageFactory.ts at the isServerless() guard:

    // src/storage/core/storageFactory.ts
    !['in-memory', 'cloudflare-r2', 'cloudflare-kv', 'cloudflare-d1'].includes(providerType)
    

    Add the new provider string to this array. Non-storage providers have no equivalent gate.

  7. Add the dependency if Tier 3: add to both peerDependencies and peerDependenciesMeta (with { "optional": true }) in package.json. Without the peerDependenciesMeta entry, the dep appears required rather than optional.

  8. Run bun run rebuild — since this is package source, verify the build output compiles.

  9. Run bun run devcheck to verify.

Checklist

  • Provider file created with JSDoc @fileoverview + @module header
  • Interface fully implemented (including name, supportsTTS/supportsSTT for speech)
  • Tier 3 dependencies lazy-loaded (not top-level imports)
  • Registered in the correct factory for the domain (see Step 5)
  • Storage: provider string added to z.enum in src/config/index.ts
  • Storage: Worker-compatible array in storageFactory.ts updated if applicable
  • Storage: value encoding, batch, TTL envelope, paging, and LIKE escaping come from providerHelpers.ts, not a local copy
  • Storage: the shared compliance suite (tests/compliance/storage-provider.ts) runs against the provider
  • Speech: provider literal added to SpeechProviderConfig union in types.ts
  • LLM: src/core/app.ts instantiation logic updated if adding a second LLM provider
  • Optional peer dependency added to both peerDependencies and peerDependenciesMeta in package.json if Tier 3
  • bun run rebuild succeeds
  • bun run devcheck passes
  • Tests added under tests/unit/storage/providers/{{providerName}}/ (storage) or tests/unit/services/{{domain}}/providers/{{provider-name}}.provider.test.ts (LLM / speech), and bun run test passes
Saved skills