name: gen-mcp-tool description: > Scaffold a new Lightdash MCP tool module following the project's established patterns. Use when adding a new domain tool file to packages/mcp/src/tools/ and wiring it into the registration barrel. Provide the resource name (e.g. "validations", "ai-agents") and the desired operations (read-only list/get vs. write upsert/delete).
Generate MCP Tool
Purpose
Create a new packages/mcp/src/tools/<resource>.ts and register it in
packages/mcp/src/tools/index.ts, following the project's guardrail patterns
(registerToolSafe, wrapTool, Zod schemas, annotation presets).
Key Conventions
| Concern | Pattern |
|---|---|
| Tool name | ldt__<resource>_<action> (prefix added automatically by TOOL_PREFIX) |
| Read-only tools | annotations: READ_ONLY_DEFAULT |
| Idempotent writes | annotations: WRITE_IDEMPOTENT |
| Destructive writes | annotations: WRITE_DESTRUCTIVE (reversible only; see ADR-0037) |
| Irrecoverable ops | Do not add — register as client-only in packages/common/src/operations/ |
| Error handling | Handled by wrapTool — no try/catch in the handler body |
| Client access | c.v1.<resource>.* or c.v2.<resource>.* from @lightdash-tools/client |
| Output format | { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } |
Workflow
Identify the client API Browse
packages/client/src/api/v1/andpackages/client/src/api/v2/to find the relevant client class and its methods. Note the method signatures and return types.Create the tool file Create
packages/mcp/src/tools/<resource>.tsusing the template below.- Use
READ_ONLY_DEFAULTfor list/get operations. - Use
WRITE_IDEMPOTENTfor create/upsert operations. - Use
WRITE_DESTRUCTIVEonly for reversible delete/revoke operations (ADR-0037). - For irrecoverable deletes, do not add an MCP tool; add a
client-onlyentry inpackages/common/src/operations/instead.
- Use
Register in the barrel In
packages/mcp/src/tools/index.ts:- Add the import:
import { register<Resource>Tools } from './<resource>.js'; - Add the call inside
registerTools():register<Resource>Tools(server, client);
- Add the import:
Build and test Run
pnpm build && pnpm testto verify the scaffold compiles and the guardrail suite still passes.
File Template
/**
* MCP tools: <resource> (<list of operations>).
*/
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { LightdashClient } from '@lightdash-tools/client';
import { z } from 'zod';
import { wrapTool, registerToolSafe, READ_ONLY_DEFAULT } from './shared.js';
// Import WRITE_IDEMPOTENT and/or WRITE_DESTRUCTIVE if this file has write tools.
export function register<Resource>Tools(server: McpServer, client: LightdashClient): void {
// ── Read-only example ────────────────────────────────────────────────────
registerToolSafe(
server,
'list_<resource>',
{
title: 'List <resource>',
description: 'List <resource> in a project',
inputSchema: { projectUuid: z.string().describe('Project UUID') },
annotations: READ_ONLY_DEFAULT,
},
wrapTool(client, (c) => async ({ projectUuid }: { projectUuid: string }) => {
const result = await c.v1.<resource>.list<Resource>(projectUuid);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}),
);
// ── Write example (idempotent) ────────────────────────────────────────────
// registerToolSafe(
// server,
// 'upsert_<resource>',
// {
// title: 'Upsert <resource>',
// description: 'Create or update a <resource>',
// inputSchema: {
// projectUuid: z.string().describe('Project UUID'),
// payload: z.record(z.string(), z.unknown()).describe('<Resource> payload'),
// },
// annotations: WRITE_IDEMPOTENT,
// },
// wrapTool(
// client,
// (c) =>
// async ({ projectUuid, payload }: { projectUuid: string; payload: Record<string, unknown> }) => {
// type UpsertBody = Parameters<LightdashClient['v1']['<resource>']['upsert<Resource>']>[1];
// const result = await c.v1.<resource>.upsert<Resource>(projectUuid, payload as UpsertBody);
// return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
// },
// ),
// );
}
Checklist Before Finishing
- Tool names follow
<resource>_<action>(short name; prefix is added automatically) - All inputs have explicit Zod types with
.describe()strings - Annotation preset matches the operation type (read / idempotent write / destructive write)
- Irrecoverable operations are not exposed; registry entry uses
agentExposure: 'client-only'(ADR-0037) -
wrapToolis used — no bare try/catch in handler bodies -
register<Resource>Toolsis imported and called intools/index.ts -
pnpm build && pnpm testpasses
Resources
- shared.ts:
registerToolSafe,wrapTool, annotation presets - index.ts: Registration barrel to update
- dashboards.ts: Minimal read-only example
- charts.ts: Mixed read + write example
- pnpm Commands: Build and test commands