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/sdk @sapphire/pieces pino zod
JobRoot exports
ServerbuildServer, BuildServerDeps, BuildServerResult, VERSION
ConfigurationloadConfig, Config
Tools and resultsdefineTool, ToolDefinition, Tool, ToolRunContext, dualResult
IDs and paginationSnowflake, ApplicationId, ChannelId, EmojiId, GuildId, MessageId, RoleId, UserId, WebhookId, encodeCursor, decodeCursor
ErrorsDiscordError families, formatErrorForUser, FormatErrorContext
ResiliencebuildPolicy, wrapRestWithResilience, classifyDiscordError
Middlewarecompose, telemetryMiddleware, validateMiddleware, preconditionMiddleware, auditMiddleware
Gateway notificationscreateGatewayClient, GatewayClient, SubscriptionRegistry
AuditcreateAuditSink, sink classes, redactArgs
Safety helperswrapMessages, wrapUntrusted, redactRoute

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

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 the current audit middleware. It is not synonymous with read-only: an idempotent PUT can write, and the current audit layer skips all tools marked idempotent: 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/sdk/server/stdio.js';
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.

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, 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:

FamilyCodes
ClientDISCORD_PERMISSION_DENIED, DISCORD_RATE_LIMITED, DISCORD_NOT_FOUND, VALIDATION_FAILED, DISCORD_AUTH_INVALID, DISCORD_CLOUDFLARE_BLOCKED, SCOPE_REJECTED, GUILD_NOT_ALLOWED, DRY_RUN_PREVIEW, CANCELLED
ServerDISCORD_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 six stages in this order:

telemetry → default guild → validation → category gate → 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.