Skip to content

Core API

@discord-mcp/core is the programmatic package behind the CLI server. Import only from the package root; internal file paths are not exported.

Terminal window
pnpm add @discord-mcp/core @discordjs/rest @modelcontextprotocol/server @sapphire/pieces pino zod
Job Root exports
Server buildServer, buildCatalogServer, BuildServerDeps, BuildServerResult, BuildCatalogServerResult, VERSION
Configuration loadConfig, Config
Access preflight getToolAccessRequirement, listKnownToolAccessRequirements, evaluateBotPermissions, named permission/intent registries
Approval and fingerprints payloadConfirmationMiddleware, fingerprintPayload, canonicalizePayload, reviewComponentsV2
Resource runtime ResourceStore (internal server seam), static Components V2 resources, allowlisted live guild snapshots
Tools and results defineTool, ToolDefinition, Tool, ToolRunContext, dualResult
IDs and pagination Snowflake, ApplicationId, ChannelId, EmojiId, GuildId, MessageId, RoleId, UserId, WebhookId, encodeCursor, decodeCursor
Errors DiscordError families, formatErrorForUser, FormatErrorContext
Resilience buildPolicy, wrapRestWithResilience, classifyDiscordError
Middleware compose, telemetryMiddleware, validateMiddleware, preconditionMiddleware, auditMiddleware
Gateway notifications createGatewayClient, GatewayClient, SubscriptionRegistry
Audit createAuditSink, sink classes, redactArgs
Safety helpers wrapMessages, wrapUntrusted, redactRoute
Activity Evidence assertGuildBlueprintActivityEvidence, GuildBlueprintActivityEvidence

The authoritative export list is packages/mcp-core/src/index.ts.

assertGuildBlueprintActivityEvidence is a read-only semantic validator for completion-only Activity Evidence. It checks the trusted plan identity, blueprint safety invariants, exact resource bindings, checkpoint completion, and final readback continuity. It does not contact Discord or mutate local state; callers that need live verification must perform their own Discord readback first.

defineTool accepts bare Zod field maps. A handler should return an MCP CallToolResult; dualResult creates the text plus structuredContent shape used throughout the project.

import { ChannelId, defineTool, dualResult } from '@discord-mcp/core';
import { z } from 'zod';
export default defineTool({
name: 'channel_describe',
description: '**Purpose**: Return a short channel description.',
category: 'channels',
inputSchema: {
channel_id: ChannelId,
},
outputSchema: {
summary: z.string(),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
idempotent: true,
async handler({ channel_id }, ctx) {
const response = await fetch(`https://example.internal/channels/${channel_id}`, {
signal: ctx.signal,
});
const channel = (await response.json()) as { name: string };
const summary = `#${channel.name}`;
return dualResult({ text: summary, data: { summary } });
},
});

Important distinctions:

  • annotations.readOnlyHint describes semantics to MCP clients.
  • annotations.idempotentHint says repeated calls have the same intended effect.
  • def.idempotent is runtime metadata used by telemetry and audit correlation. It is not synonymous with read-only: an idempotent PUT can write. The audit layer skips only tools whose annotations.readOnlyHint is explicitly true.
  • ToolRunContext guarantees signal. The built-in pipeline path augments its internal context with invocation support, but custom handlers should not rely on an undocumented ctx.invoke.

In tests, defineTool validates successful structuredContent against the declared output schema. Production does not turn a schema mismatch into a new runtime failure.

import {
buildPolicy,
buildServer,
createLogger,
loadConfig,
wrapRestWithResilience,
} from '@discord-mcp/core';
import { REST } from '@discordjs/rest';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
const config = loadConfig();
const logger = createLogger(config);
const token = config.DISCORD_TOKEN.startsWith('Bot ')
? config.DISCORD_TOKEN.slice(4)
: config.DISCORD_TOKEN;
const baseRest = new REST({ version: '10', retries: 0 }).setToken(token);
const rest = wrapRestWithResilience(baseRest, buildPolicy(config, logger), {
circuitHalfOpenAfterMs: config.MCP_CIRCUIT_HALF_OPEN_AFTER_MS,
});
const { server, auditSink } = await buildServer({ rest, logger, config });
await server.connect(new StdioServerTransport());
async function shutdown() {
await server.close();
await auditSink.shutdown?.();
}

buildServer returns the unconnected SDK server, registered tool and precondition names, a resource-notification function, the subscription registry, and the audit sink. The normal CLI transport also starts optional OpenTelemetry and Gateway services and shuts them down in order.

buildCatalogServer() returns the same shape without accepting configuration or credentials. It advertises the complete shipped schema and static resources, but every tool call fails with CATALOG_ONLY before validation or dispatch. It is intended for registry inspection, not Discord execution.

new REST({ retries: 0 }) disables discord.js retries for server errors and timeouts. It does not disable the library’s route/global rate-limit queue.

function loadConfig(env: NodeJS.ProcessEnv = process.env): Config

The parser validates DISCORD_TOKEN, coerces supported booleans and numbers, and fills declared defaults. Fields whose schema is optional remain optional; for example DISCORD_DEFAULT_GUILD_ID, ALLOWED_GUILDS, MCP_CATEGORIES, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, and MCP_AUDIT_FILE can be absent.

Invalid input throws a plain Error containing a line for each Zod issue. ConfigSchema itself is not exported.

All custom errors extend DiscordError. Current stable codes include:

Family Codes
Client DISCORD_PERMISSION_DENIED, DISCORD_RATE_LIMITED, DISCORD_NOT_FOUND, VALIDATION_FAILED, DISCORD_AUTH_INVALID, DISCORD_CLOUDFLARE_BLOCKED, SCOPE_REJECTED, GUILD_NOT_ALLOWED, GUILD_SCOPE_UNRESOLVED, BOT_SCOPE_UNRESOLVED, DRY_RUN_PREVIEW, CANCELLED
Server DISCORD_SERVER_ERROR, CIRCUIT_OPEN, BULKHEAD_FULL, INTERNAL_ERROR

formatErrorForUser guarantees code, retriable, and category in structuredContent; diagnosis and recovery prose is in content[0].text.

import { DiscordPermissionError, formatErrorForUser } from '@discord-mcp/core';
const error = new DiscordPermissionError(
['SEND_MESSAGES'],
['VIEW_CHANNEL'],
'channels/123456789012345678',
);
const result = formatErrorForUser(error, {
toolName: 'messages_send',
transport: 'stdio',
});

See Error handling for the wire shape and retry guidance.

The built-in server invokes the shared policy stages in this order:

telemetry → default guild → blueprint target → validation → guild scope → category gate → payload approval → write mode → preconditions → audit

buildServer registers the shipped tools explicitly. The runtime does not scan the filesystem for new tool files; adding a built-in tool requires exporting its class and adding it to server registration.