Authoring an adapter
Authoring a migration adapter
Section titled “Authoring a migration adapter”A migration adapter helps users switch from another Discord MCP server onto discord-mcp. If your favourite Discord MCP project becomes unmaintained, an adapter for it is one of the highest-impact contributions you can make to this codebase: small (~200 LOC + tests + a docs page), self-contained (no runtime coupling to other adapters), and immediately useful to anyone left on the abandoned project.
Why contribute an adapter
Section titled “Why contribute an adapter”- Help users migrate quickly. A working adapter turns a multi-day
prompt-rewriting project into a one-command
discord-mcp migratereport. - Document the source. Even a partial NAME_MAP is the most useful documentation a community member can produce — it captures which tools matter, which fold onto which destination calls, and where the rough edges are.
- Low surface area. An adapter is one file under
packages/mcp-server/src/lib/migrate-adapters/, one test file, one fixture underpackages/mcp-server/test-fixtures/, and one MDX page on this site.
The MigrationSource interface
Section titled “The MigrationSource interface”Every adapter implements the same interface, exported from
packages/mcp-server/src/lib/migrate-adapters/types.ts:
export interface MigrationSource { readonly id: string; readonly description: string; readonly homepage?: string; readonly languages: readonly ('typescript' | 'go' | 'python' | 'mixed')[]; readonly toolCountEstimate?: number;
detect(rootPath: string): Promise<boolean>; migrate(rootPath: string): Promise<MigrationResult>;}Required fields:
id— kebab-case identifier passed via--from <id>. Must be unique across all adapters.description— single-line human-readable description shown bymigrate --list.languages— what the source project is written in. Helps users understand at a glance whether they need a Go clone, a TS clone, etc.detect(rootPath)— fast yes/no answer to “is this filesystem the shape I understand?” Must NOT throw — wrap fs ops in try/catch.migrate(rootPath)— walk the source tree and return aMigrationResultlisting mapped / unmapped / manual-review tools plus warnings.
Optional fields:
homepage— link to the upstream repo, surfaced by--list.toolCountEstimate— best-effort tool count for--list; helps users gauge migration scope.
See the four shipping adapters for working examples: hubdustry-go-mcp, pasympa, quadslab, discord-ops.
Detection
Section titled “Detection”detect() must answer true for a real clone of your source project and
false for absolutely everything else — including the other shipping
adapters’ fixtures.
Multi-signal requirement
Section titled “Multi-signal requirement”A single signal is too weak. Every shipping adapter combines AT LEAST two independent signals:
- Signal 1 — package metadata:
package.jsonnamefield matches a pattern (literal name OR/regex/icovering forks). - Signal 2 — content match: at least one source file under
src/contains a syntactic pattern unique to this project. Patterns we use today:- PaSympa:
'discord_<x>'literals (thediscord_prefix is the fingerprint). - quadslab:
<x>Toolsarray exports +execute<X>Toolfunction exports in the same file. - discord-ops:
defineTool(calls + acategory:field literal. - hubdustry-go-mcp: Go file extension +
mcp.NewTool(regex (Go is wholly disjoint from TypeScript adapters).
- PaSympa:
Disjointness is mandatory. Your detect function MUST return false on
all four existing adapter fixtures. Add tests in both directions
(see Cross-detection below).
Fast-fail patterns
Section titled “Fast-fail patterns”detect() is called for every adapter on every migrate invocation. Keep
it cheap:
- Wrap every fs op in
try { ... } catch { return false; }— never let an IO failure bubble up. - Bail early on the cheapest signal first (typically
package.jsonexistence + name pattern). - Cap directory walks at the smallest scope you can —
src/tools/first, fall back tosrc/only if missing. - Return
trueon first match — don’t keep walking.
Tool extraction
Section titled “Tool extraction”migrate() walks the source tree and extracts every tool name it can find.
Two patterns work:
Pattern A — generic regex over a known prefix
Section titled “Pattern A — generic regex over a known prefix”Use when the source server names every tool with a unique prefix
(discord_<x> for PaSympa). The regex /['"]discord_[a-z_0-9]+['"]/g
matches every literal in the codebase. Risk: false positives on string
literals that happen to match the pattern. Acceptable when the prefix is
distinctive.
Pattern B — known-name intersection
Section titled “Pattern B — known-name intersection”Use when the source server uses generic snake_case (send_message,
list_channels) that a regex would over-match. Maintain a
KNOWN_<SOURCE>_TOOLS: ReadonlySet<string> of every tool name you’ve
catalogued. Match [a-z_]+ literals and intersect with the set. quadslab
and discord-ops both use this pattern.
Trade-off: pattern B is high-precision but requires the catalog to be updated for every new source release. Document the cutoff date in the file’s research-note JSDoc so future maintainers know when it was last checked.
Best-effort: missing tools is OK, false positives are NOT
Section titled “Best-effort: missing tools is OK, false positives are NOT”If your regex misses a multi-line defineTool({...}) call or a
concatenated tool name, that’s acceptable — the user sees a slightly
shorter unmapped list and notices the gap manually. False positives are
worse: they pollute the report with non-existent tools and erode trust.
NAME_MAP
Section titled “NAME_MAP”Maps source tool name → discord-mcp <category>_<verb> with confidence.
const NAME_MAP: Record< string, { mapped: string; confidence: 'high' | 'medium' | 'low'; notes?: string }> = { send_message: { mapped: 'messages_send', confidence: 'high' }, send_embed: { mapped: 'messages_send', confidence: 'medium', notes: 'pass embeds[] in messages_send payload', }, notify_owners: { mapped: 'messages_send', confidence: 'low', notes: 'first call users_create_dm to get the DM channel ID', },};Confidence levels
Section titled “Confidence levels”high— name and argument shape are a 1:1 match. Caller can swap names without thinking.medium— name maps cleanly but the caller may need to massage args (e.g. embed-specific helper folds onto the generalmessages_send). Always includenotes.low— best-guess mapping; user MUST verify. Always includenotesdescribing the caveat.
Notes are mandatory for medium and low
Section titled “Notes are mandatory for medium and low”A medium- or low-confidence entry without notes is a bug. The note must
explain what the caller has to change about argument shape.
Intentionally unmapped
Section titled “Intentionally unmapped”If a source tool has no discord-mcp equivalent, omit it from NAME_MAP.
migrate() will surface it under unmappedTools. Document the omissions
in your file’s research-note JSDoc — explain why each one is unmapped (no
equivalent / different surface / architectural mismatch). Future maintainers
will read your notes when discord-mcp adds a new tool that closes the gap.
Test fixture pattern
Section titled “Test fixture pattern”Every adapter ships a synthetic fixture under
packages/mcp-server/test-fixtures/<adapter-id>/. Layout mirrors the
upstream’s minimal surface:
test-fixtures/<adapter-id>/ package.json # FIXTURE: synthetic <name>-shaped package.json src/ tools/ <example>.ts # FIXTURE: synthetic <name>-style codeHeader comment is mandatory
Section titled “Header comment is mandatory”Every fixture file MUST start with the comment:
// FIXTURE: synthetic <name>-style code for adapter testing — not real codeThis prevents future grep-greppers from mistaking the fixture for live
code. Same applies to package.json (use the description field).
Cover happy path AND edge cases
Section titled “Cover happy path AND edge cases”Your fixture must exercise:
- Happy path — at least one tool that maps to
high, one tomedium, one tolow. - Unmapped path — at least one tool intentionally omitted from NAME_MAP, so
the test asserts it surfaces in
unmappedTools. - Detection — the fixture must satisfy your
detect()(positive case).
Synthetic, minimal, public-domain
Section titled “Synthetic, minimal, public-domain”Don’t copy real upstream code. Synthetic snippets are smaller, faster, and free of license complications.
Cross-detection (CRITICAL)
Section titled “Cross-detection (CRITICAL)”This is the rule that prevents an adapter from claiming a tree that belongs to someone else.
Your adapter’s detect() MUST return false on every existing adapter’s
fixture. Conversely, every existing adapter’s detect() MUST return
false on your fixture.
Tests look like:
describe('myAdapter detect()', () => { it('returns false on hubdustry fixture', async () => { expect( await myAdapter.detect('packages/mcp-server/test-fixtures/hubdustry-go-mcp'), ).toBe(false); }); it('returns false on pasympa fixture', async () => { expect( await myAdapter.detect('packages/mcp-server/test-fixtures/pasympa'), ).toBe(false); }); // ... and so on for every shipping adapter.});
describe('existing adapters reject myAdapter fixture', () => { for (const adapter of [hubdustryGoMcpAdapter, pasympaAdapter, /* ... */]) { it(`${adapter.id} returns false on my fixture`, async () => { expect( await adapter.detect('packages/mcp-server/test-fixtures/my-adapter'), ).toBe(false); }); }});CI runs both directions; failures here block the merge.
Submitting via PR
Section titled “Submitting via PR”-
Fork the repo and create a branch.
Terminal window gh repo fork cappylab/discord-mcp --clonecd discord-mcpgit checkout -b feature/adapter-myname -
Add the adapter file.
packages/mcp-server/src/lib/migrate-adapters/<id>.tswith the research note JSDoc, NAME_MAP,detect(),migrate(), and the singleton export. Use one of the four shipping adapters as a starting template. -
Add the test fixture.
packages/mcp-server/test-fixtures/<id>/withpackage.json(synthetic metadata) andsrc/tools/(synthetic tool definitions). Every file gets the FIXTURE header comment. -
Add tests.
packages/mcp-server/src/lib/migrate-adapters/<id>.test.tscovering:detect()positive on your fixture.detect()negative on every other adapter’s fixture (cross-detection).migrate()produces expected mapped / unmapped counts on your fixture.- At least one
mediumand onelowconfidence assertion.
-
Register in
ALL_ADAPTERS.Edit
packages/mcp-server/src/lib/migrate-adapters/index.tsand add your adapter to the array. Order doesn’t matter functionally, but keep it alphabetical-ish for readability. -
Add the docs page.
site/src/content/docs/migrate/<id>.mdxwith sidebar order matching your registration. Use one of the existing per-adapter pages as a template. -
Run the verification suite locally.
Terminal window pnpm exec biome check --write .pnpm exec tsc --noEmit -p packages/mcp-server/tsconfig.jsonpnpm testpnpm --filter site buildAll four green is the bar.
-
Open a PR.
Title:
feat(migrate): adapter for <source name>. Body includes a link to the upstream repo, the cutoff commit you researched against, and a summary of mapped / unmapped tool counts.
See also
Section titled “See also”- Migration overview — what migration means and the three-step CLI flow.
MigrationSourcesource — the canonical interface definition.- Shipping adapters — read these for working patterns:
hubdustry-go-mcp.ts— minimal reference (Go, empty NAME_MAP).pasympa.ts— TypeScript with prefix-based extraction (Pattern A).quadslab.ts— TypeScript with known-name intersection (Pattern B).discord-ops.ts— TypeScript with architectural-mismatch warnings.