@dtpr/ui

@dtpr/ui/html

Server-side rendering of datachains as standalone HTML documents for MCP Apps.

@dtpr/ui/html

SSR the same Vue components as an iframe-renderable HTML document. Used by the MCP server's render_datachain tool.

Import

import { renderDatachainDocument, trustAsHtml } from '@dtpr/ui/html'
import type { RenderedSection, RenderDatachainOptions, SafeHtml } from '@dtpr/ui/html'

Requires @vue/server-renderer (installed as a direct dep of @dtpr/ui). Runs in any Node-compatible runtime — Node, Bun, Cloudflare Workers (via nodejs_compat).

renderDatachainDocument

Render a complete standalone HTML document — <!doctype html>, embedded stylesheet, SSR'd body, and a tiny client-side accordion script.

Signature

async function renderDatachainDocument(
  sections: readonly RenderedSection[],
  options?: RenderDatachainOptions,
): Promise<string>

Parameters

ParamTypeDescription
sectionsreadonly RenderedSection[]Ordered list of sections with their element displays.
options.localestring (default 'en')<html lang> and the locale used by Intl formatting inside element detail.
options.titlestring (default 'DTPR datachain')<title> text.
options.emptyHtmlSafeHtmlTrusted HTML inserted when sections is empty. Declare trust via trustAsHtml(...).

Returns

Promise<string> — a complete HTML document.

RenderedSection

interface RenderedSection {
  id: string
  title: string
  elements: readonly ElementDisplay[]
}

Produce elements by running each element through deriveElementDisplay from @dtpr/ui/core.

trustAsHtml

Brand a string as SafeHtml so it can be passed to options.emptyHtml. The brand is a phantom type — there is no runtime sanitization.

function trustAsHtml(html: string): SafeHtml
Only call trustAsHtml on content you have sanitized (DOMPurify, etc.) or strings you control (static constants, generated markup from trusted sources). Do not wrap raw user input.

MCP Apps example

The produced HTML is served with the text/html;profile=mcp-app mime type (SEP-1865):

import { renderDatachainDocument } from '@dtpr/ui/html'
import { deriveElementDisplay } from '@dtpr/ui/core'

const sections = categories.map((c) => ({
  id: c.id,
  title: extractWithLocale(c.name, 'en').value,
  elements: instance.elements
    .filter((p) => elementById.get(p.element_id)?.category_id === c.id)
    .map((p) => deriveElementDisplay(elementById.get(p.element_id)!, p, 'en')),
}))

const html = await renderDatachainDocument(sections, { locale: 'en' })

return new Response(html, {
  headers: { 'content-type': 'text/html;profile=mcp-app' },
})

This is exactly how render_datachain produces the body that resources/read returns.

Empty state

import { renderDatachainDocument, trustAsHtml } from '@dtpr/ui/html'

const html = await renderDatachainDocument([], {
  emptyHtml: trustAsHtml('<p>No datachain to display.</p>'),
})

Omit emptyHtml to get a neutral <p class="dtpr-empty" role="status"> placeholder.

See also

Copyright © 2026