Step-by-step guide for creating new internal MCP server integrations in Dust that connect to remote platforms (Jira, HubSpot, Salesforce, etc.)...
This runbook provides step-by-step instructions for creating new internal MCP server integrations in Dust that connect to remote platforms (e.g., Jira, HubSpot, Salesforce, etc.).
For a minimal new server (no OAuth, no external API yet ā just the skeleton to register and test):
front/lib/api/actions/servers/{provider}/metadata.ts with a literal metadata arrayfront/lib/api/actions/servers/{provider}/tools/index.ts with stub handlersfront/lib/api/actions/servers/{provider}/index.ts with createServerconstants.ts and servers/index.tsSERVER_SOURCES in bm25_tool_search_utils.test.tsbm25_tool_search.test.tsSee the BM25 Tests section below for the test setup. This gives you a runnable skeleton with type-checked tool descriptions before writing any real API calls.
front/lib/api/actions/servers/{provider}/
āāā metadata.ts # Tool metadata array and server info
āāā tools/index.ts # Schema-inferred handlers and built tools
āāā index.ts # Provider-local server creation and registration
āāā client.ts # API client (optional)
āāā helpers.ts # Helper functions (optional)
front/lib/actions/mcp_internal_actions/constants.ts - Add server config with metadata: YOUR_SERVERfront/lib/actions/mcp_internal_actions/servers/index.ts - Import and register in switch statementfront/lib/api/oauth/providers/{provider}.tscore/src/oauth/providers/{provider}.rsauthorization field must reference the OAuth providerAVAILABLE_INTERNAL_MCP_SERVER_NAMES arraynever_ask, low, medium, high)displayLabels, toolCostCategory, and freeUsageList, Get, Search,
Create, or UpdateResult typeswithAuth patternIf the remote platform requires OAuth authentication:
core/src/oauth/providers/ as {provider}.rsfront/lib/api/oauth/providers/{provider}.tsIf the OAuth provider does not exist, implement it first in core and front:
core/src/oauth/providers/{provider}.rscore/src/oauth/providers/mod.rsfront/lib/api/oauth/providers/{provider}.ts for the front-end OAuth setupSee existing providers like hubspot.rs or jira.rs for reference implementations.
Before starting implementation, research the platform API:
Document the operations you want to expose:
metadata.tsCreate front/lib/api/actions/servers/{provider}/metadata.ts:
import type { ServerMetadata } from "@app/lib/actions/mcp_internal_actions/tool_definition";
import { z } from "zod";
export const YOUR_PROVIDER_SERVER_NAME = "your_provider" as const;
export const YOUR_PROVIDER_TOOLS_METADATA = [
{
name: "list_items",
description: "List all items accessible to the user.",
schema: {
pageToken: z.string().optional().describe("Page token for pagination."),
maxResults: z.number().optional().describe("Maximum results to return."),
},
stake: "never_ask",
toolCostCategory: "advanced",
freeUsage: false,
displayLabels: {
running: "Listing Items",
done: "List items",
},
},
{
name: "get_item",
description: "Get a single item by ID.",
schema: {
itemId: z.string().describe("The ID of the item to retrieve."),
},
stake: "never_ask",
toolCostCategory: "advanced",
freeUsage: false,
displayLabels: {
running: "Retrieving item",
done: "Retrieve item",
},
},
{
name: "create_item",
description: "Create a new item.",
schema: {
name: z.string().describe("Name of the item."),
description: z.string().optional().describe("Description of the item."),
},
stake: "low",
toolCostCategory: "advanced",
freeUsage: false,
displayLabels: {
running: "Creating item",
done: "Create item",
},
},
] as const;
export const YOUR_PROVIDER_SERVER = {
serverInfo: {
name: YOUR_PROVIDER_SERVER_NAME,
version: "1.0.0",
description: "Short description of what this integration does.",
authorization: {
provider: "your_provider",
supported_use_cases: ["personal_actions", "platform_actions"],
},
icon: "YourProviderLogo",
documentationUrl: "https://docs.dust.tt/docs/your-provider",
instructions: null,
},
tools: YOUR_PROVIDER_TOOLS_METADATA,
} as const satisfies ServerMetadata;
Key points:
snake_case for the tool namesdisplayLabels; unlike remote MCP tools, these labels are requiredtoolCostCategory and freeUsage deliberately, following a comparable existing serverList, Get, Search,
Create, Update, or Retrieve; avoid noun phrases, articles, gerunds, and third-person
verbs because descriptions are part of the BM25 tool-search corpus (see BM25-Friendly
Descriptions below)stake values map to review/approval expectationsTool names and descriptions both drive BM25 retrieval. Names are the strongest signal ā they
must be consistent and follow the verb_noun convention (e.g., list_warehouses, get_workbook).
Descriptions are the secondary signal: write each one as if answering "what user intent does this
tool serve?"
Rules:
List, Get, Search, Create, Update, Send, Deletewarehouse, workbook, ticket, channelworksheets (sheets/tabs)Databricks workspace, Excel workbook),
but don't lead with the full brand name or repeat it redundantly across every toolin OneDrive,
in SharePoint) to every tool ā BM25 treats these as content tokens, so they widen the match
surface and cause your tools to surface on location-based queries (e.g., a Drive search) even
when the user intended a different toolExamples:
// BAD ā noun phrase, redundant "Microsoft Excel", location noise
description: "Microsoft Excel file listing from OneDrive and SharePoint."
// BAD ā gerund
description: "Listing all SQL warehouses in Databricks."
// BAD ā third-person verb
description: "Lists all SQL warehouses available in Databricks."
// GOOD ā bare infinitive, platform noun, no location noise
description: "List all SQL warehouses available in the Databricks workspace."
// GOOD ā synonym in parentheses helps BM25 match "sheets" and "tabs"
description: "Get a list of all worksheets (sheets/tabs) in an Excel workbook."
// GOOD ā verb + context + common synonyms
description: "Search Slack channels, messages, and threads by keyword or topic."
Test your descriptions: add a BM25 query case (see next section) before merging. If your expected tool doesn't score > 0 in its own server-scoped index, the description is too generic or missing the key tokens the user will type.
tools/index.tsCreate front/lib/api/actions/servers/{provider}/tools/index.ts:
import { MCPError } from "@app/lib/actions/mcp_errors";
import type { ToolHandlers } from "@app/lib/actions/mcp_internal_actions/tool_definition";
import { buildTools } from "@app/lib/actions/mcp_internal_actions/tool_definition";
import { YOUR_PROVIDER_TOOLS_METADATA } from "@app/lib/api/actions/servers/your_provider/metadata";
import { Err, Ok } from "@app/types/shared/result";
const handlers: ToolHandlers<typeof YOUR_PROVIDER_TOOLS_METADATA> = {
list_items: async ({ pageToken, maxResults }, { authInfo }) => {
const token = authInfo?.token;
if (!token) {
return new Err(new MCPError("No access token provided"));
}
try {
const items = [];
return new Ok([
{ type: "text" as const, text: `Found ${items.length} items` },
{ type: "text" as const, text: JSON.stringify({ items }, null, 2) },
]);
} catch (e) {
return new Err(new MCPError("Failed to list items"));
}
},
get_item: async ({ itemId }, { authInfo }) => {
const token = authInfo?.token;
if (!token) {
return new Err(new MCPError("No access token provided"));
}
try {
const item = {};
return new Ok([
{ type: "text" as const, text: `Retrieved item ${itemId}` },
{ type: "text" as const, text: JSON.stringify(item, null, 2) },
]);
} catch (e) {
return new Err(new MCPError("Failed to get item"));
}
},
create_item: async ({ name, description }, { authInfo }) => {
const token = authInfo?.token;
if (!token) {
return new Err(new MCPError("No access token provided"));
}
try {
const item = {};
return new Ok([
{ type: "text" as const, text: `Created item "${name}"` },
{ type: "text" as const, text: JSON.stringify(item, null, 2) },
]);
} catch (e) {
return new Err(new MCPError("Failed to create item"));
}
},
};
export const TOOLS = buildTools(YOUR_PROVIDER_TOOLS_METADATA, handlers);
Key points:
ToolHandlers<typeof YOUR_PROVIDER_TOOLS_METADATA> enforces one handler per metadata name and
infers each handler's parameters from that tool's Zod schemabuildTools combines metadata and handlers into ToolDefinition[]ToolMeta[] widens names and schemas and loses
per-handler parameter inferencebuildTools must preserve tool names as their own generic instead of
widening them to string; prefer calling buildTools directly unless a helper adds real valueextra.authInfo?.tokenindex.tsCreate front/lib/api/actions/servers/{provider}/index.ts:
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { makeInternalMCPServer } from "@app/lib/actions/mcp_internal_actions/utils";
import { registerTool } from "@app/lib/actions/mcp_internal_actions/wrappers";
import type { ToolContext } from "@app/lib/actions/types";
import { YOUR_PROVIDER_SERVER_NAME } from "@app/lib/api/actions/servers/your_provider/metadata";
import { TOOLS } from "@app/lib/api/actions/servers/your_provider/tools";
import type { Authenticator } from "@app/lib/auth";
function createServer(
auth: Authenticator,
toolContext?: ToolContext
): McpServer {
const server = makeInternalMCPServer(YOUR_PROVIDER_SERVER_NAME);
for (const tool of TOOLS) {
registerTool(auth, toolContext, server, tool, {
monitoringName: YOUR_PROVIDER_SERVER_NAME,
});
}
return server;
}
export default createServer;
constants.tsEdit front/lib/actions/mcp_internal_actions/constants.ts:
YOUR_PROVIDER_SERVERAVAILABLE_INTERNAL_MCP_SERVER_NAMESINTERNAL_MCP_SERVERSExample:
your_provider: {
id: 99,
availability: "manual",
allowMultipleInstances: true,
isRestricted: undefined,
isPreview: false,
tools_arguments_requiring_approval: undefined,
tools_retry_policies: undefined,
timeoutMs: undefined,
metadata: YOUR_PROVIDER_SERVER,
},
Important properties:
id: unique stable ID, never change after deploymentavailability: manual, auto, or auto_hidden_builderallowMultipleInstances: true for OAuth-based integrationsisRestricted: feature-flag or plan gating function, if neededisPreview: true for beta or preview integrationsINTERNAL_MCP_SERVERS is wrapped by ensureUniqueToolNames; preserving the metadata tuple lets
it reject duplicate tool names within a server at compile timeservers/index.tsEdit front/lib/actions/mcp_internal_actions/servers/index.ts:
case "your_provider":
return yourProviderServer(auth, toolContext);
client.ts and helpers.tsUse extra files when the integration grows beyond a few simple calls.
client.tsCreate a client when you need multiple API endpoints, response validation, auth header management, or retry logic.
helpers.tsCreate helpers for:
withAuth wrappersExample withAuth pattern:
import { MCPError } from "@app/lib/actions/mcp_errors";
import type {
ToolHandlerExtra,
ToolHandlerResult,
} from "@app/lib/actions/mcp_internal_actions/tool_definition";
import { Err } from "@app/types/shared/result";
export async function withAuth<T>(
{ authInfo }: ToolHandlerExtra,
action: (token: string) => Promise<ToolHandlerResult>
): Promise<ToolHandlerResult> {
const token = authInfo?.token;
if (!token) {
return new Err(new MCPError("No access token provided"));
}
try {
return await action(token);
} catch (e) {
return new Err(new MCPError("Operation failed"));
}
}
Use client.ts / helpers.ts based on complexity:
tools/index.tshelpers.tsclient.tsIf handlers need access to Authenticator directly, create tools through a function instead of a
constant.
See front/lib/api/actions/servers/github/tools/index.ts for a full example.
Use an existing similar icon temporarily, then request the final icon from design/Sparkle and
update the icon field once available.
When wiring up the icon:
sparkle/src/logo/platforms/ if it doesn't already exist, and re-export it
from sparkle/src/logo/platforms/index.ts.sparkle/src/logo/platforms/registry.ts and add the logo to PLATFORM_LOGOS.
The marketing site resolves icons by string name via getPlatformLogo(), so a missing
registry entry silently falls back to a placeholder puzzle-piece on /integrations.Gate preview or limited-access servers through isRestricted in the server config, using feature
flags or plan checks as needed.
Always convert API responses into focused, markdown-formatted output. Avoid returning raw
JSON.stringify(apiResponse) with everything the upstream API sent.
Do:
Do not:
Wrap failures in meaningful MCPErrors rather than exposing raw upstream errors.
never_ask: read-only operationslow: low-impact writesmedium: important writeshigh: destructive or high-impact actions.describe() to schema fieldsSchema descriptions help the model supply the right parameters.
Validate every external response to catch API drift and unexpected payloads early.
Every new server must be added to the BM25 test corpus. This is the only automated check that description quality is sufficient for tool-search retrieval.
bm25_tool_search_utils.test.tsAdd an import and a SERVER_SOURCES entry. SERVERS is derived from this array and performs the
Zod-to-JSON-Schema conversion used by the BM25 test corpus:
// At the top with the other imports:
import { YOUR_PROVIDER_SERVER } from "@app/lib/api/actions/servers/your_provider/metadata";
// In the SERVER_SOURCES array:
{ name: "your_provider", tools: YOUR_PROVIDER_SERVER.tools },
bm25_tool_search.test.tsAdd entries to the QUERIES array. Each entry needs:
query: the natural-language phrase a user would typeexpected: "<server_name>.<tool_name>" ā the tool that must score > 0{ query: "list databricks warehouses", expected: "databricks.list_warehouses" },
{ query: "what sql warehouses do I have", expected: "databricks.list_warehouses" },
Tips for writing good query cases:
maxRank: N to relax the
full-corpus ranking assertion"${query}" ā ${expected} is scored in ${serverName}-only index)
only checks score > 0, so focus on making sure the key tokens appear somewhere in the tool's
description or inputSchemacd front
NODE_ENV=test npm test -- --reporter verbose ./lib/api/actions/servers/bm25_tool_search.test.ts
If a case fails with "Expected tool to have a non-zero score but it was not found", the query tokens don't overlap with the tool's corpus tokens. Fix the description to include the missing token, or rephrase the query to use a term that's actually in the description.
Before marking implementation complete:
stake, toolCostCategory, freeUsage, and displayLabelstools/index.ts exists and uses ToolHandlers<typeof METADATA>buildTools(METADATA, handlers) builds the runtime tool definitionsindex.ts default-exports the server factoryAVAILABLE_INTERNAL_MCP_SERVER_NAMESINTERNAL_MCP_SERVERSservers/index.tsPLATFORM_LOGOS in sparkle/src/logo/platforms/registry.tsSERVER_SOURCES in bm25_tool_search_utils.test.tsbm25_tool_search.test.tscd front && NODE_ENV=test npm test -- --reporter verbose ./lib/api/actions/servers/bm25_tool_search.test.ts passescd front && npx tsgo --noEmit passesnpm run format:changed passes from the repo rootavailabilityisRestrictedAVAILABLE_INTERNAL_MCP_SERVER_NAMEScore and frontstakeauthInfo.token is propagatedAVAILABLE_INTERNAL_MCP_SERVER_NAMEScd front && npx tsgo --noEmitfront/lib/api/actions/servers/github/front/lib/api/actions/servers/snowflake/front/lib/api/actions/servers/google_calendar/front/lib/api/actions/servers/agent_sidekick_context/front/lib/api/actions/servers/agent_sidekick_agent_state/front/lib/api/actions/servers/front/lib/actions/mcp_internal_actions/servers/core/src/oauth/providers/