Skip to main content
Glama
evtapps

Ditto MCP Server

by evtapps

Ditto: Execute DQL query

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
NameRequiredDescriptionDefault
statementYesDQL statement to execute (single statement; no trailing ';')
argsNoNamed parameters for the DQL
transactionIdNoOptional X-DITTO-TXN-ID for consistency
apiKeyNoDitto API key (prefer env over passing here)
baseUrlNoOverrides DITTO_BASE_URL for this call
timeoutMsNoPer-call timeout override (ms)

Implementation Reference

  • 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);
      }
    }
  • 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,
              ),
            },
          ],
        };
      },
    );
  • 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)'),
    };
  • 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
    };
  • 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);
      }
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden for behavioral disclosure. 'Runs a query' implies execution but doesn't specify whether this is read-only, can modify data, requires authentication, has rate limits, or what happens on failure. The description lacks critical behavioral context that should be provided when annotations are absent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that communicates the core purpose without unnecessary words. It's appropriately sized for a tool with comprehensive schema documentation and gets straight to the point with zero wasted text.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a query execution tool with 6 parameters, no annotations, and no output schema, the description is insufficient. It doesn't explain what DQL is, what types of operations it supports, what the expected return format is, or any behavioral characteristics. The context signals indicate significant complexity that isn't addressed in the description.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all 6 parameters thoroughly. The description adds minimal value beyond what's in the schema - it mentions 'parameterized DQL statement' which hints at the 'args' parameter, but doesn't provide additional semantic context about parameter relationships or usage patterns.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('Runs a query') and the target system ('in Ditto'), with the specific query type ('parameterized DQL statement'). It distinguishes from the only sibling 'ping' by focusing on query execution rather than connectivity testing. However, it doesn't specify what kind of queries (read vs write) or the resource being queried.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. With only one sibling ('ping'), it doesn't explain that 'ping' is for connectivity testing while this is for data operations. There's no mention of prerequisites, error conditions, or typical use cases for DQL queries.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/evtapps/ditto-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server