execute_dql
Execute parameterized DQL queries to retrieve data from Ditto databases with configurable timeouts and transaction consistency.
Instructions
Runs a query in Ditto using a parameterized DQL statement
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| statement | Yes | DQL statement to execute (single statement; no trailing ';') | |
| args | No | Named parameters for the DQL | |
| transactionId | No | Optional X-DITTO-TXN-ID for consistency | |
| apiKey | No | Ditto API key (prefer env over passing here) | |
| baseUrl | No | Overrides DITTO_BASE_URL for this call | |
| timeoutMs | No | Per-call timeout override (ms) |
Implementation Reference
- src/ditto/client.ts:21-98 (handler)Core handler function that executes the DQL query by making an authenticated POST request to Ditto's /api/v4/store/execute endpoint. Handles timeouts, errors, and returns structured DittoResponse.export async function executeDql( req: ExecuteRequest, opts: ExecuteOptions, ): Promise<DittoResponse> { const url = buildUrl(opts.baseUrl); const controller = new AbortController(); const timeoutMs = opts.timeoutMs ?? 20000; const id = setTimeout(() => controller.abort(), timeoutMs); try { const headers: Record<string, string> = { Authorization: `Bearer ${opts.apiKey}`, 'Content-Type': 'application/json', 'User-Agent': `ditto-mcp-server/${SERVER_VERSION}`, }; if (opts.transactionId !== undefined) { headers['X-DITTO-TXN-ID'] = String(opts.transactionId); } const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(req), signal: controller.signal, }); const text = await res.text(); let json: DittoResponse | undefined; try { json = text ? (JSON.parse(text) as DittoResponse) : undefined; } catch { // Fallback plain-text error if (!res.ok) { throw Object.assign(new Error(text || `HTTP ${res.status}`), { status: res.status, }); } throw new Error('Ditto response was not valid JSON'); } if (!res.ok) { const errMsg = json?.error?.description || `HTTP ${res.status}`; class HttpStatusError extends Error { status?: number; constructor(message: string, status?: number) { super(message); this.status = status; } } throw new HttpStatusError(errMsg, res.status); } return json!; } catch (err: unknown) { let message = 'Request failed'; if (typeof err === 'object' && err !== null) { const maybe = err as { message?: string }; if (typeof maybe.message === 'string') message = maybe.message; } else if (typeof err === 'string') { message = err; } logger.error(`[ditto] request failed: ${message}`); // Return a Ditto-like error envelope to the tool handler const errorResponse: DittoResponse = { queryType: 'unknown', items: [], mutatedDocumentIds: [], totalWarningsCount: 0, warnings: [], error: { description: message }, transactionId: undefined, }; return errorResponse; } finally { clearTimeout(id); } }
- src/mcpServer.ts:76-178 (registration)Registers the 'execute_dql' MCP tool, including input schema, description, and the wrapper handler function that applies guards, resolves config, calls executeDql, and formats the MCP response.server.registerTool( 'execute_dql', { title: 'Ditto: Execute DQL query', description: 'Runs a query in Ditto using a parameterized DQL statement', inputSchema: executeInput, }, async ({ statement, args, transactionId, apiKey, baseUrl, timeoutMs, }: ExecuteToolInput) => { // Guard the statement & allowed operations const cleaned = ensureSingleStatement(statement); const op = detectOperation(cleaned); if (op === 'UNKNOWN') { return { isError: true, content: [ { type: 'text', text: 'Statement must start with SELECT/INSERT/UPDATE/DELETE/EVICT.', }, ], }; } try { assertAllowedOperation(op, cfg.allowedOps); assertAllowPatternsIfAny(cleaned, cfg.queryAllowPatterns); } catch (e) { const message = e instanceof GuardError ? e.message : String(e); return { isError: true, content: [{ type: 'text', text: message }] }; } // Resolve base URL & key const url = baseUrl ?? cfg.dittoBaseUrl; if (!url) { return { isError: true, content: [ { type: 'text', text: 'DITTO_BASE_URL not set. Pass baseUrl or set env DITTO_BASE_URL.', }, ], }; } const envKey = process.env[cfg.apiKeyEnvVar]; const key = apiKey || envKey; if (!key) { return { isError: true, content: [ { type: 'text', text: `No API key. Set env ${cfg.apiKeyEnvVar} or pass apiKey.`, }, ], }; } // Call Ditto const res = await executeDql( { statement: cleaned, args: args ?? undefined }, { baseUrl: url, apiKey: key, transactionId, timeoutMs: timeoutMs ?? cfg.timeoutMs, }, ); if (res.error?.description) { return { isError: true, content: [{ type: 'text', text: `Ditto error: ${res.error.description}` }], }; } return { content: [ { type: 'text', text: JSON.stringify( { transactionId: res.transactionId, queryType: res.queryType, items: res.items, mutatedDocumentIds: res.mutatedDocumentIds, warnings: res.warnings, totalWarningsCount: res.totalWarningsCount, }, null, 2, ), }, ], }; }, );
- src/mcpServer.ts:37-66 (schema)Zod schema defining the input parameters for the 'execute_dql' tool.const executeInput = { statement: z .string() .min(1) .describe("DQL statement to execute (single statement; no trailing ';')"), args: z.record(z.unknown()).optional().describe('Named parameters for the DQL'), transactionId: z .number() .int() .nonnegative() .optional() .describe('Optional X-DITTO-TXN-ID for consistency'), apiKey: z .string() .min(1) .optional() .describe('Ditto API key (prefer env over passing here)'), baseUrl: z .string() .url() .optional() .describe('Overrides DITTO_BASE_URL for this call'), timeoutMs: z .number() .int() .positive() .max(60000) .optional() .describe('Per-call timeout override (ms)'), };
- src/ditto/types.ts:1-24 (schema)TypeScript type definitions for ExecuteRequest, ExecuteOptions, and DittoResponse used by the executeDql handler.export type DittoQueryArgs = Record<string, unknown>; export type ExecuteRequest = { statement: string; args?: DittoQueryArgs; }; export type DittoResponse = { transactionId?: number; queryType: string; // "select" | "insert" | "update" | ... items: unknown[]; mutatedDocumentIds: unknown[]; warnings: { _id?: unknown; description: string }[]; totalWarningsCount: number; error?: { description?: string }; }; export type ExecuteOptions = { baseUrl: string; // canonical root, e.g. https://APP.cloud.ditto.live apiKey: string; // Ditto API key (Authorization: Bearer ...) timeoutMs?: number; transactionId?: number; // optional X-DITTO-TXN-ID };
- src/guards/dql.ts:1-46 (helper)Helper functions for DQL statement validation and guarding: single-statement enforcement, operation detection, allowed ops/patterns checks. Used in the tool handler.import type { DqlOp } from '../config.js'; export class GuardError extends Error { status = 400; constructor(message: string, status = 400) { super(message); this.status = status; } } /** Remove trailing semicolons and ensure a single statement. */ export function ensureSingleStatement(statement: string): string { const cleaned = statement.replace(/;+\s*$/, '').trim(); if (!cleaned) throw new GuardError('DQL statement cannot be empty'); if (cleaned.includes(';')) throw new GuardError('Multiple DQL statements are not allowed'); return cleaned; } /** Very small lexer: strip block/line comments, then return first token uppercased. */ export function detectOperation(statement: string): DqlOp | 'UNKNOWN' { const noBlock = statement.replace(/\/\*[\s\S]*?\*\//g, ' '); const noLine = noBlock.replace(/--.*$/gm, ' ').replace(/#[^\n]*$/gm, ' '); const firstToken = noLine.trim().split(/\s+/)[0]?.toUpperCase(); const ops = new Set(['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'EVICT']); return firstToken && ops.has(firstToken) ? (firstToken as DqlOp) : 'UNKNOWN'; } export function assertAllowedOperation(op: DqlOp, allowed: Set<DqlOp>) { if (!allowed.has(op)) { const allowedList = [...allowed].join(', '); throw new GuardError( `Operation "${op}" is disabled. Allowed operations: ${allowedList}`, 403, ); } } export function assertAllowPatternsIfAny(statement: string, allowPatterns: RegExp[]) { if (allowPatterns.length === 0) return; const ok = allowPatterns.some((rx) => rx.test(statement)); if (!ok) { throw new GuardError('Statement does not match any allowed pattern (policy).', 403); } }