Skip to main content
Glama

render_document

Merge JSON data with a template to generate documents in various formats, with options for batch processing, language localization, and async rendering via webhook.

Instructions

Generate a document by merging a Carbone template with JSON data. Two modes: (1) pass templateId to use a previously uploaded template; (2) pass template (file path, URL, or base64) to upload and render in a single request without storing a template. Supports output format conversion, multilingual rendering, currency conversion, batch generation, and advanced PDF options (watermark, password, PDF/A). Async mode: pass webhookUrl to render asynchronously — Carbone will POST the renderId to your URL when the document is ready. Async mode is required when using batch generation (batchSplitBy).

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
templateIdNoThe ID of a previously uploaded template to render. Two ID formats are accepted: (1) Template ID (64-bit) — stable identifier shared across versions; Carbone automatically uses the deployed version. (2) Version ID (SHA-256) — pins rendering to a specific version regardless of deployment status. Both are returned by upload_template. Mutually exclusive with template — provide exactly one, never both.
templateNoInline template for one-shot render without storing a template first. Accepts a local file path (e.g. /home/user/invoice.docx), a URL (https://example.com/template.docx), or a base64-encoded string. The template is uploaded and rendered in a single API request — no Template ID is returned. Use this for ephemeral renders; use upload_template + templateId when you need to reuse the template. Supported formats: DOCX, XLSX, PPTX, ODT, ODS, ODP, ODG, HTML, XHTML, IDML, XML, Markdown (MD), PDF, and more. Mutually exclusive with templateId — provide exactly one, never both.
dataYesJSON data merged into the template. Access fields with {d.fieldName} tags. Nested objects: {d.customer.name}. Array loops: {d.items[i].description} … {d.items[i+1]}. Conditionals: {d.status == "active" ? "Yes" : "No"}. For pure document conversion without data injection, pass {}.
convertToNoOutput format. If omitted, the output matches the template format. Documents : "pdf", "docx", "xlsx", "pptx", "odt", "ods", "odp", "odg", "rtf", "epub". Web/text : "html", "xhtml", "txt", "csv", "md", "xml", "idml". Images : "png", "jpg", "jpeg", "webp", "svg", "tiff", "bmp", "gif". Archive : "zip" (use with batchSplitBy for batch output). Simple usage: "pdf". Advanced usage: { "formatName": "pdf", "formatOptions": { ... } } for PDF-specific options.
converterNoConverter engine. Only relevant when convertTo is "pdf" (or an image rasterised from a document). "L" — LibreOffice (default): best all-round engine for DOCX, XLSX, PPTX, ODT, ODS, ODP. "O" — OnlyOffice: highest fidelity for Microsoft Office formats (DOCX, XLSX, PPTX). "C" — Chromium: best for HTML/CSS/JS templates — full browser rendering. If omitted, LibreOffice is used by default.
timezoneNoIANA timezone used to convert dates in the rendered document. Default: "Europe/Paris". Applied when templates use the :formatD formatter, e.g. {d.date:formatD(YYYY-MM-DD HH:mm)}. Common values: "UTC", "America/New_York", "America/Los_Angeles", "Europe/London", "Europe/Paris", "Europe/Berlin", "Asia/Tokyo", "Asia/Shanghai", "Australia/Sydney". Full list (TZ identifier column): https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
langNoLocale of the generated document. Affects three things: (1) {t(key)} translation tags — selects the matching translation from the translations map. (2) :formatN number formatter — applies locale-specific thousand/decimal separators. (3) :formatC currency formatter — applies locale-specific currency symbols and formatting. Format: BCP-47 lowercase, e.g. "fr-fr", "en-us", "de-de", "es-es", "pt-br", "zh-cn", "ja-jp". Full list: https://github.com/carboneio/carbone/blob/master/formatters/_locale.js
complementNoExtra data object accessible in templates with {c.field} tags (as opposed to {d.field} for main data). Useful for static or shared values that should not be mixed into the main dataset: company info, logo URLs, footer text, configuration constants. Example: { "company": "Acme Corp", "address": "123 Main St", "vatNumber": "FR12345" }
variableStrNoCarbone alias expressions evaluated once before rendering, available everywhere in the template. Used to pre-compute reusable values or shorten repetitive paths. Syntax: "{#aliasName = expression}". Example: "{#fullName = d.firstName + \" \" + d.lastName}{#total = d.price * d.qty}". Aliases are then used in the template as {#fullName}, {#total}. Documentation: https://carbone.io/documentation.html#alias
reportNameNoFilename for the generated document, returned in the Content-Disposition header. Supports Carbone tags resolved against the data at render time. Examples: "invoice.pdf" (static), "{d.type}-{d.id}.pdf" (dynamic), "{d.client}-{d.date:formatD(YYYY-MM)}.docx".
enumNoEnumeration map used with the :convEnum(TYPE) formatter to translate code values into human-readable labels. Define one key per enum type; each value is an object mapping code → label. Example: { "STATUS": { "1": "Active", "2": "Inactive", "3": "Pending" }, "ROLE": { "A": "Admin", "U": "User" } }. Template usage: {d.status:convEnum(STATUS)}, {d.role:convEnum(ROLE)}. Documentation: https://carbone.io/documentation.html#convenum-type-
translationsNoTranslation map for multilingual documents. Requires "lang" to be set to select the active locale. Top-level keys are BCP-47 locale codes; values are key → translated-string maps. Template usage: {t(greeting)} is replaced by the matching string for the active locale. Example: { "fr-fr": { "greeting": "Bonjour", "total": "Total" }, "en-us": { "greeting": "Hello", "total": "Total" } }. Documentation: https://carbone.io/documentation.html#translations
currencySourceNoISO 4217 currency code of the monetary amounts in the JSON data. Used by the :formatC formatter as the conversion source. Must be set together with currencyTarget and currencyRates. Example: "EUR" if all prices in your data are in euros.
currencyTargetNoISO 4217 currency code of the output document. The :formatC formatter converts amounts from currencySource to this currency using currencyRates. Must be set together with currencySource and currencyRates. Example: "USD" to display prices in US dollars. Documentation: https://carbone.io/documentation.html#formatc-precisionorformat-
currencyRatesNoExchange rate table used by :formatC for currency conversion. Keys are ISO 4217 currency codes; values are rates relative to a common base. The base currency should have rate 1. Example: { "EUR": 1, "USD": 1.08, "GBP": 0.86, "JPY": 160.5 }.
hardRefreshNoIf true, Carbone recomputes pagination and refreshes the table of contents after rendering. Requires convertTo to be defined. Use this for DOCX/ODT templates that contain a TOC field or cross-references that need updating after data injection.
batchSplitByNoJSON path to the array in your data that drives batch generation. One document is generated per element of the array; all documents are bundled together. Use batchOutput: "zip" to receive a single ZIP archive. Use batchReportName to customise each filename inside the ZIP. Example: "d.invoices" — produces one PDF per item in data.invoices. Example: "d.employees" — produces one contract per employee.
batchOutputNoContainer format for the batch result. Use "zip" to receive all generated documents as a single ZIP archive. Must be used together with batchSplitBy.
batchReportNameNoFilename pattern for each individual document inside the batch ZIP. Supports Carbone tags. Tags are resolved against the item's data (relative path) or the full dataset (absolute path). Examples: "invoice-{d.id}.pdf", "{d.client.name}-{d.date}.docx". Must be used together with batchSplitBy.
webhookUrlNoWebhook URL to enable asynchronous rendering. When provided, Carbone returns immediately and POSTs { "success": true, "data": { "renderId": "..." } } to this URL when the document is ready. The default render timeout is extended to 5 minutes on Carbone Cloud (vs 60 s for synchronous requests). Download the document with GET /render/:renderId once the webhook is received. Required when using batchSplitBy (batch generation is always asynchronous). Example: "https://your-server.com/carbone-webhook".
webhookHeadersNoCustom headers Carbone will include when POSTing to your webhookUrl. Pass plain header names as keys — the prefix "carbone-webhook-header-" is added automatically before sending to Carbone, and Carbone forwards the original header names to your webhook endpoint. Example: { "authorization": "my-secret", "custom-id": "12345", "custom-name": "Jane Doe" } — Carbone will call your URL with headers: authorization: my-secret, custom-id: 12345, custom-name: Jane Doe. Requires webhookUrl to be set.

Implementation Reference

  • The main handler function `handleRenderDocument` that executes the render_document tool logic. It validates XOR of templateId/template, calls `client.renderDocument()`, and formats the result as MCP content.
    export async function handleRenderDocument(
      args: {
        templateId?: string;
        template?: string;
        data: Record<string, unknown>;
        convertTo?: z.infer<typeof renderDocumentSchema.convertTo>;
        converter?: string;
        timezone?: string;
        lang?: string;
        complement?: Record<string, unknown>;
        variableStr?: string;
        reportName?: string;
        enum?: Record<string, unknown>;
        translations?: Record<string, Record<string, string>>;
        currencySource?: string;
        currencyTarget?: string;
        currencyRates?: Record<string, number>;
        hardRefresh?: boolean;
        batchSplitBy?: string;
        batchOutput?: string;
        batchReportName?: string;
        webhookUrl?: string;
        webhookHeaders?: Record<string, string>;
      },
      client: CarboneClient,
      options?: CallOptions
    ) {
      // XOR: exactly one of templateId or template must be provided
      if ((args.templateId != null) === (args.template != null)) {
        return {
          isError: true,
          content: [{ type: 'text' as const, text: 'Provide either templateId or template, not both (and not neither).' }],
        };
      }
    
      try {
        const template = args.template ? await resolveFileInput(args.template) : undefined;
        const result = await client.renderDocument({ ...args, template }, options);
    
        if ('async' in result) {
          return { content: [{ type: 'text' as const, text: result.message }] };
        }
    
        const format = args.convertTo ?? result.filename.split('.').pop() ?? 'pdf';
        const content = toToolContent(result.buffer, result.filename, format);
        return { content: [content] };
      } catch (error) {
        return {
          isError: true,
          content: [{ type: 'text' as const, text: formatError(error) }],
        };
      }
    }
  • Input schema for render_document as a Zod-based object schema (renderDocumentSchema) defining all parameters: templateId, template, data, convertTo, converter, timezone, lang, complement, variableStr, reportName, enum, translations, currencySource/Target/Rates, hardRefresh, batch options, webhook options.
    export const renderDocumentSchema = {
      templateId: z
        .string()
        .min(1)
        .optional()
        .describe(
          'The ID of a previously uploaded template to render. Two ID formats are accepted: ' +
          '(1) Template ID (64-bit) — stable identifier shared across versions; Carbone automatically uses the deployed version. ' +
          '(2) Version ID (SHA-256) — pins rendering to a specific version regardless of deployment status. ' +
          'Both are returned by upload_template. ' +
          'Mutually exclusive with template — provide exactly one, never both.'
        ),
    
      template: z
        .string()
        .min(1)
        .optional()
        .describe(
          'Inline template for one-shot render without storing a template first. ' +
          'Accepts a local file path (e.g. /home/user/invoice.docx), ' +
          'a URL (https://example.com/template.docx), or a base64-encoded string. ' +
          'The template is uploaded and rendered in a single API request — no Template ID is returned. ' +
          'Use this for ephemeral renders; use upload_template + templateId when you need to reuse the template. ' +
          'Supported formats: DOCX, XLSX, PPTX, ODT, ODS, ODP, ODG, HTML, XHTML, IDML, XML, Markdown (MD), PDF, and more. ' +
          'Mutually exclusive with templateId — provide exactly one, never both.'
        ),
    
      data: z
        .record(z.string(), z.unknown())
        .describe(
          'JSON data merged into the template. ' +
          'Access fields with {d.fieldName} tags. ' +
          'Nested objects: {d.customer.name}. ' +
          'Array loops: {d.items[i].description} … {d.items[i+1]}. ' +
          'Conditionals: {d.status == "active" ? "Yes" : "No"}. ' +
          'For pure document conversion without data injection, pass {}.'
        ),
    
      convertTo: z
        .union([
          z.enum(OUTPUT_FORMATS),
          z.object({
            formatName: z.enum(OUTPUT_FORMATS).describe('Target format name.'),
            formatOptions: z
              .record(z.string(), z.unknown())
              .optional()
              .describe(
                'Advanced format options object. Examples by format: ' +
                'PDF — { "EncryptFile": true, "DocumentOpenPassword": "secret", "DocumentPermissionPassword": "owner" } password-protect; ' +
                'PDF — { "Watermarks": [{ "text": "DRAFT", "opacity": 0.2, "rotation": -45, "fontsize": 60 }] } up to 5 watermarks; ' +
                'PDF — { "SelectPdfVersion": 1 } PDF/A-1b compliance (use 2 for PDF/A-2, 3 for PDF/A-3); ' +
                'PDF — { "PageRange": "1-3,5" } export specific pages only; ' +
                'PDF — { "ConvertSlideshow": true } convert each slide to a separate PDF page; ' +
                'Images (PNG/JPG/WEBP) — { "Quality": 90 } compression quality 0-100; ' +
                'Images — { "density": 150 } DPI for rasterisation (default 96); ' +
                'CSV — { "fieldSeparator": ";" } custom column separator.'
              ),
          }),
        ])
        .optional()
        .describe(
          'Output format. If omitted, the output matches the template format. ' +
          'Documents : "pdf", "docx", "xlsx", "pptx", "odt", "ods", "odp", "odg", "rtf", "epub". ' +
          'Web/text  : "html", "xhtml", "txt", "csv", "md", "xml", "idml". ' +
          'Images    : "png", "jpg", "jpeg", "webp", "svg", "tiff", "bmp", "gif". ' +
          'Archive   : "zip" (use with batchSplitBy for batch output). ' +
          'Simple usage: "pdf". ' +
          'Advanced usage: { "formatName": "pdf", "formatOptions": { ... } } for PDF-specific options.'
        ),
    
      converter: z
        .enum(CONVERTERS)
        .optional()
        .describe(
          'Converter engine. Only relevant when convertTo is "pdf" (or an image rasterised from a document). ' +
          '"L" — LibreOffice (default): best all-round engine for DOCX, XLSX, PPTX, ODT, ODS, ODP. ' +
          '"O" — OnlyOffice: highest fidelity for Microsoft Office formats (DOCX, XLSX, PPTX). ' +
          '"C" — Chromium: best for HTML/CSS/JS templates — full browser rendering. ' +
          'If omitted, LibreOffice is used by default.'
        ),
    
      timezone: z
        .string()
        .optional()
        .describe(
          'IANA timezone used to convert dates in the rendered document. Default: "Europe/Paris". ' +
          'Applied when templates use the :formatD formatter, e.g. {d.date:formatD(YYYY-MM-DD HH:mm)}. ' +
          'Common values: "UTC", "America/New_York", "America/Los_Angeles", "Europe/London", ' +
          '"Europe/Paris", "Europe/Berlin", "Asia/Tokyo", "Asia/Shanghai", "Australia/Sydney". ' +
          'Full list (TZ identifier column): https://en.wikipedia.org/wiki/List_of_tz_database_time_zones'
        ),
    
      lang: z
        .string()
        .optional()
        .describe(
          'Locale of the generated document. Affects three things: ' +
          '(1) {t(key)} translation tags — selects the matching translation from the translations map. ' +
          '(2) :formatN number formatter — applies locale-specific thousand/decimal separators. ' +
          '(3) :formatC currency formatter — applies locale-specific currency symbols and formatting. ' +
          'Format: BCP-47 lowercase, e.g. "fr-fr", "en-us", "de-de", "es-es", "pt-br", "zh-cn", "ja-jp". ' +
          'Full list: https://github.com/carboneio/carbone/blob/master/formatters/_locale.js'
        ),
    
      complement: z
        .record(z.string(), z.unknown())
        .optional()
        .describe(
          'Extra data object accessible in templates with {c.field} tags (as opposed to {d.field} for main data). ' +
          'Useful for static or shared values that should not be mixed into the main dataset: ' +
          'company info, logo URLs, footer text, configuration constants. ' +
          'Example: { "company": "Acme Corp", "address": "123 Main St", "vatNumber": "FR12345" }'
        ),
    
      variableStr: z
        .string()
        .optional()
        .describe(
          'Carbone alias expressions evaluated once before rendering, available everywhere in the template. ' +
          'Used to pre-compute reusable values or shorten repetitive paths. ' +
          'Syntax: "{#aliasName = expression}". ' +
          'Example: "{#fullName = d.firstName + \\" \\" + d.lastName}{#total = d.price * d.qty}". ' +
          'Aliases are then used in the template as {#fullName}, {#total}. ' +
          'Documentation: https://carbone.io/documentation.html#alias'
        ),
    
      reportName: z
        .string()
        .optional()
        .describe(
          'Filename for the generated document, returned in the Content-Disposition header. ' +
          'Supports Carbone tags resolved against the data at render time. ' +
          'Examples: "invoice.pdf" (static), "{d.type}-{d.id}.pdf" (dynamic), "{d.client}-{d.date:formatD(YYYY-MM)}.docx".'
        ),
    
      enum: z
        .record(z.string(), z.unknown())
        .optional()
        .describe(
          'Enumeration map used with the :convEnum(TYPE) formatter to translate code values into human-readable labels. ' +
          'Define one key per enum type; each value is an object mapping code → label. ' +
          'Example: { "STATUS": { "1": "Active", "2": "Inactive", "3": "Pending" }, "ROLE": { "A": "Admin", "U": "User" } }. ' +
          'Template usage: {d.status:convEnum(STATUS)}, {d.role:convEnum(ROLE)}. ' +
          'Documentation: https://carbone.io/documentation.html#convenum-type-'
        ),
    
      translations: z
        .record(z.string(), z.record(z.string(), z.string()))
        .optional()
        .describe(
          'Translation map for multilingual documents. Requires "lang" to be set to select the active locale. ' +
          'Top-level keys are BCP-47 locale codes; values are key → translated-string maps. ' +
          'Template usage: {t(greeting)} is replaced by the matching string for the active locale. ' +
          'Example: { "fr-fr": { "greeting": "Bonjour", "total": "Total" }, "en-us": { "greeting": "Hello", "total": "Total" } }. ' +
          'Documentation: https://carbone.io/documentation.html#translations'
        ),
    
      currencySource: z
        .string()
        .optional()
        .describe(
          'ISO 4217 currency code of the monetary amounts in the JSON data. ' +
          'Used by the :formatC formatter as the conversion source. ' +
          'Must be set together with currencyTarget and currencyRates. ' +
          'Example: "EUR" if all prices in your data are in euros.'
        ),
    
      currencyTarget: z
        .string()
        .optional()
        .describe(
          'ISO 4217 currency code of the output document. ' +
          'The :formatC formatter converts amounts from currencySource to this currency using currencyRates. ' +
          'Must be set together with currencySource and currencyRates. ' +
          'Example: "USD" to display prices in US dollars. ' +
          'Documentation: https://carbone.io/documentation.html#formatc-precisionorformat-'
        ),
    
      currencyRates: z
        .record(z.string(), z.number())
        .optional()
        .describe(
          'Exchange rate table used by :formatC for currency conversion. ' +
          'Keys are ISO 4217 currency codes; values are rates relative to a common base. ' +
          'The base currency should have rate 1. ' +
          'Example: { "EUR": 1, "USD": 1.08, "GBP": 0.86, "JPY": 160.5 }.'
        ),
    
      hardRefresh: z
        .boolean()
        .optional()
        .describe(
          'If true, Carbone recomputes pagination and refreshes the table of contents after rendering. ' +
          'Requires convertTo to be defined. ' +
          'Use this for DOCX/ODT templates that contain a TOC field or cross-references that need updating after data injection.'
        ),
    
      batchSplitBy: z
        .string()
        .optional()
        .describe(
          'JSON path to the array in your data that drives batch generation. ' +
          'One document is generated per element of the array; all documents are bundled together. ' +
          'Use batchOutput: "zip" to receive a single ZIP archive. ' +
          'Use batchReportName to customise each filename inside the ZIP. ' +
          'Example: "d.invoices" — produces one PDF per item in data.invoices. ' +
          'Example: "d.employees" — produces one contract per employee.'
        ),
    
      batchOutput: z
        .string()
        .optional()
        .describe(
          'Container format for the batch result. ' +
          'Use "zip" to receive all generated documents as a single ZIP archive. ' +
          'Must be used together with batchSplitBy.'
        ),
    
      batchReportName: z
        .string()
        .optional()
        .describe(
          'Filename pattern for each individual document inside the batch ZIP. Supports Carbone tags. ' +
          'Tags are resolved against the item\'s data (relative path) or the full dataset (absolute path). ' +
          'Examples: "invoice-{d.id}.pdf", "{d.client.name}-{d.date}.docx". ' +
          'Must be used together with batchSplitBy.'
        ),
    
      webhookUrl: z
        .url()
        .optional()
        .describe(
          'Webhook URL to enable asynchronous rendering. ' +
          'When provided, Carbone returns immediately and POSTs { "success": true, "data": { "renderId": "..." } } to this URL when the document is ready. ' +
          'The default render timeout is extended to 5 minutes on Carbone Cloud (vs 60 s for synchronous requests). ' +
          'Download the document with GET /render/:renderId once the webhook is received. ' +
          'Required when using batchSplitBy (batch generation is always asynchronous). ' +
          'Example: "https://your-server.com/carbone-webhook".'
        ),
    
      webhookHeaders: z
        .record(z.string(), z.string())
        .optional()
        .describe(
          'Custom headers Carbone will include when POSTing to your webhookUrl. ' +
          'Pass plain header names as keys — the prefix "carbone-webhook-header-" is added automatically before sending to Carbone, ' +
          'and Carbone forwards the original header names to your webhook endpoint. ' +
          'Example: { "authorization": "my-secret", "custom-id": "12345", "custom-name": "Jane Doe" } — ' +
          'Carbone will call your URL with headers: authorization: my-secret, custom-id: 12345, custom-name: Jane Doe. ' +
          'Requires webhookUrl to be set.'
        ),
    };
  • Registration of the render_document tool on the MCP server via `server.registerTool()`, linking the name, description, schema, and handler together.
    server.registerTool(
      renderDocumentToolName,
      { description: renderDocumentDescription, inputSchema: renderDocumentSchema },
      (args, extra) => handleRenderDocument(args, client, { apiKey: extra.authInfo?.token })
    );
  • The `CarboneClient.renderDocument()` method on the client class that makes the actual HTTP API call to Carbone's render endpoint. Handles both stored-template (templateId) and inline-template (template) modes, async webhook mode, and returns binary buffer or async acknowledgment.
    async renderDocument(params: {
      templateId?: string;
      template?: string;
      data: object;
      convertTo?: OutputFormat;
      converter?: string;
      timezone?: string;
      lang?: string;
      complement?: Record<string, unknown>;
      variableStr?: string;
      reportName?: string;
      enum?: Record<string, unknown>;
      translations?: Record<string, Record<string, string>>;
      currencySource?: string;
      currencyTarget?: string;
      currencyRates?: Record<string, number>;
      hardRefresh?: boolean;
      batchSplitBy?: string;
      batchOutput?: string;
      batchReportName?: string;
      webhookUrl?: string;
      webhookHeaders?: Record<string, string>;
    }, options?: CallOptions): Promise<{ buffer: Buffer; filename: string } | { async: true; message: string }> {
      const body: Record<string, unknown> = { data: params.data };
      if (params.template)                   body['template']       = params.template;
      if (params.convertTo)                  body['convertTo']      = params.convertTo;
      if (params.converter)                  body['converter']      = params.converter;
      if (params.timezone)                   body['timezone']       = params.timezone;
      if (params.lang)                       body['lang']           = params.lang;
      if (params.complement)                 body['complement']     = params.complement;
      if (params.variableStr)                body['variableStr']    = params.variableStr;
      if (params.reportName)                 body['reportName']     = params.reportName;
      if (params.enum)                       body['enum']           = params.enum;
      if (params.translations)               body['translations']   = params.translations;
      if (params.currencySource)             body['currencySource'] = params.currencySource;
      if (params.currencyTarget)             body['currencyTarget'] = params.currencyTarget;
      if (params.currencyRates)              body['currencyRates']  = params.currencyRates;
      if (params.hardRefresh !== undefined)  body['hardRefresh']    = params.hardRefresh;
      if (params.batchSplitBy)               body['batchSplitBy']   = params.batchSplitBy;
      if (params.batchOutput)                body['batchOutput']    = params.batchOutput;
      if (params.batchReportName)            body['batchReportName'] = params.batchReportName;
    
      const isAsync = !!params.webhookUrl;
      const endpoint = params.template
        ? `/render/template${isAsync ? '' : '?download=true'}`
        : `/render/${params.templateId}${isAsync ? '' : '?download=true'}`;
    
      const headers: Record<string, string> = { 'Content-Type': 'application/json' };
      if (params.webhookUrl) {
        headers['carbone-webhook-url'] = params.webhookUrl;
        if (params.webhookHeaders) {
          for (const [name, value] of Object.entries(params.webhookHeaders)) {
            headers[`carbone-webhook-header-${name}`] = value;
          }
        }
      }
    
      const response = await this.request(endpoint, {
        method: 'POST',
        headers,
        body: JSON.stringify(body),
      }, options);
    
      if (isAsync) {
        const json = await response.json() as { success: boolean; message: string };
        return { async: true, message: json.message };
      }
    
      return this.handleBinaryResponse(response);
    }
  • Centralized Zod schema `RenderDocumentSchema` used for validation, with a refine() to enforce XOR of templateId and template.
    export const RenderDocumentSchema = z.object({
      templateId: z.string().min(1).optional(),
      template:   z.string().min(1).optional(),
      data: z.record(z.string(), z.unknown()),
      convertTo: OutputFormatSchema.optional(),
      converter: z.enum(CONVERTERS).optional(),
      timezone: z.string().optional(),
      lang: z.string().optional(),
      complement: z.record(z.string(), z.unknown()).optional(),
      variableStr: z.string().optional(),
      reportName: z.string().optional(),
      enum: z.record(z.string(), z.unknown()).optional(),
      translations: z.record(z.string(), z.record(z.string(), z.string())).optional(),
      currencySource: z.string().optional(),
      currencyTarget: z.string().optional(),
      currencyRates: z.record(z.string(), z.number()).optional(),
      hardRefresh: z.boolean().optional(),
      batchSplitBy: z.string().optional(),
      batchOutput: z.string().optional(),
      batchReportName: z.string().optional(),
      webhookUrl: z.string().url().optional(),
      webhookHeaders: z.record(z.string(), z.string()).optional(),
    }).refine(
      (d) => (d.templateId != null) !== (d.template != null),
      { message: 'Provide either templateId or template, not both', path: ['templateId'] }
    );
Behavior4/5

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

No annotations are provided, so the description carries full burden. It discloses the two modes, async behavior with webhook, batch generation always async, output format conversion, multilingual/currency support, and advanced PDF options. No contradictory or missing critical behavior.

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

Conciseness4/5

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

The description is a single block but front-loads the core verb-resource. It covers modes, features, and constraints without fluff. Could be slightly more structured with bullet points but remains efficient.

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

Completeness3/5

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

With 21 parameters and no output schema, the description explains both modes, async, batch, and advanced options. However, it does not describe the synchronous return value (presumably the document binary), only the async case. For a tool that can be used synchronously, this is a notable gap.

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

Parameters4/5

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

Schema coverage is 100% with detailed descriptions. The description adds value by explaining parameter groupings (e.g., mutual exclusivity of templateId and template, required combination of currencySource/currencyTarget/currencyRates, batch parameters dependency). This goes beyond mere schema repetition.

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

Purpose5/5

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

The description clearly states 'Generate a document by merging a Carbone template with JSON data.' It distinguishes two modes (templateId vs inline template) and mentions advanced features. This differentiates it from sibling tools like convert_document or upload_template.

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

Usage Guidelines4/5

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

The description explains when to use each mode (pre-uploaded vs one-shot template) and when async mode is required (batch generation). It does not explicitly state when not to use this tool compared to sibling tools like convert_document, but the context implies usage.

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/carboneio/carbone-mcp'

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