MDAN MDAN Docs
Site

Server Runtime

@mdanai/sdk/server is the main TypeScript entry point when you want to model an MDAN app on the server.

It registers and handles operations by the explicit HTTP paths written in the MDAN page.

MDAN officially supports Node and Bun with the same server model:

The app model stays the same across both runtimes. What changes is only the outer host adapter.

Basic Usage

import { composePage } from "@mdanai/sdk/core";
import { createHostedApp } from "@mdanai/sdk/server";

const server = createHostedApp({
  pages: {
    "/guestbook": pageHandler
  },
  actions: [
    {
      target: "/list",
      methods: ["GET"],
      routePath: "/guestbook",
      blockName: "guestbook",
      handler: listHandler
    },
    {
      target: "/post",
      methods: ["POST"],
      routePath: "/guestbook",
      blockName: "guestbook",
      handler: postHandler
    }
  ]
});

Handler Shape

Handlers receive a context object that includes:

If you use createHostedApp() directly, action handlers also receive:

The most common block operation can be written like this:

const page = composePage(source, {
  blocks: {
    guestbook: "## 2 live messages\n\n- Welcome\n- Hello"
  }
});

const server = createHostedApp({
  pages: {
    "/guestbook": () => page
  },
  actions: [
    {
      target: "/list",
      methods: ["GET"],
      routePath: "/guestbook",
      blockName: "guestbook",
      handler: ({ block }) => block()
    }
  ]
});

The runtime serializes that into a Markdown fragment ready to return.

createHostedApp() does not try to infer action bindings by rendering a page first and guessing from visible state. actions must explicitly declare target / methods / routePath / blockName, so registration stays stable.

Use createHostedApp() when your app is naturally a set of pages plus operations. Drop down to createMdanServer() when you need full manual control.

Request Bridge

If you want full control, your framework adapter only needs to call server.handle() with a neutral request shape:

const response = await server.handle({
  method: "POST",
  url: "https://example.com/login",
  headers: {
    accept: "text/markdown",
    "content-type": "text/markdown"
  },
  body: 'nickname: "guest", message: "hello"',
  cookies: {}
});

The returned object contains:

If you are on Node http, use the Node adapter:

import { createHost } from "@mdanai/sdk/server/node";

http.createServer(
  createHost(server, {
    rootRedirect: "/guestbook",
    transformHtml: injectEnhancement,
    staticFiles: {
      "/starter/client.js": join(exampleRoot, "dist", "client.js")
    },
    staticMounts: [{ urlPrefix: "/sdk/", directory: join(repoRoot, "sdk") }]
  })
);

If you are on Bun, use the Bun adapter:

import { createHost } from "@mdanai/sdk/server/bun";

Bun.serve({
  port: 3000,
  fetch: createHost(server, {
    rootRedirect: "/guestbook",
    transformHtml: injectEnhancement
  })
});

Runtime Entry Points

Use the shared server runtime for page and action logic:

import { createHostedApp } from "@mdanai/sdk/server";

Then choose the runtime adapter that matches your deployment target:

import { createHost } from "@mdanai/sdk/server/node";
import { createHost } from "@mdanai/sdk/server/bun";

Built-In Responsibilities

@mdanai/sdk/server already handles:

Custom Markdown Renderer

When the browser goes through the HTML path, @mdanai/sdk/server renders Markdown into HTML. You can inject that renderer:

const server = createHostedApp({
  markdownRenderer: {
    render(markdown) {
      return marked.parse(markdown);
    }
  },
  pages,
  actions
});

If you also use the default @mdanai/sdk/elements UI, pass the same markdownRenderer object to mountMdanElements(...) so server-side and default UI rendering stay consistent.

Auto Resolution

auto is an explicit server-host instruction.

The runtime should resolve auto operations before returning results to any client, so agent and browser consumers observe the same final state.

Use auto only for safe, idempotent, zero-input GET dependencies. label remains presentation-only.

The normative definition lives in the MDAN spec:

This page only describes the runtime consequence: server hosts are responsible for resolving auto, not browser code.

When To Wrap It

If you later want Express, Hono, or Next support, build that as a thin adapter around server.handle() instead of forking the runtime logic.