Template

Data table widget for MCP Apps

Rows and columns with a sticky header, where the caller decides the columns. One widget for invoices, leads, orders or any list of records your tools return.

Add a UI to your MCP server

Which invoices are still unpaid this month?

Used show_table

The house design: black outline, butter yellow and a marker swipe under the title.

When to use a table widget

Most tools an agent calls return lists of records. A table is the plainest way to show them: no interpretation, every field visible, easy to scan down a column. In a chat, it beats a Markdown table as soon as there are more than a few rows or the columns need alignment.

Use it for:

  • Unpaid invoices from an accounting tool, with amounts right-aligned.
  • New leads or deals from a CRM.
  • Open orders with their status and delivery date.
  • Search results from any internal API, shown as found.

When the rows are tasks that move between states, the kanban board is a better fit. When the point is a few totals, use metrics.

How the model fills it

This is the most generic template in the gallery, and that comes from its schema. Instead of fixed fields, the caller defines the columns:

  • columns: a list of key (the property of each row shown in that column), label (the header) and an optional align (left, right or center).
  • rows: one object per row, whose properties match the column keys. Anything else in a row is ignored.
  • title and an optional footnote for a line under the table, such as a note on taxes or the data source.

So the same widget shows invoices one minute and support tickets the next. Right-align numeric columns: they also get tabular figures, so amounts line up.

Cells are printed as text, exactly as the model sends them. That puts formatting in the model's hands: it should send amounts with their currency and dates written for the reader, not raw timestamps. If you narrow the widget to one kind of record, write that into the description (for example, "amounts formatted in euros"); the model reads it as the tool description. Whatever arrives is escaped by Liquid, so a stray tag in a record shows up as text instead of running.

What it does in the chat

The header stays pinned while you scroll, and a wide table scrolls sideways inside its panel instead of breaking the chat layout. Cells do not wrap, so each row stays on one line. Rows highlight under the pointer, the first column is bold as the row's identifier, and an empty list shows a short "no rows" message instead of a blank box.

There is no script and no library: the widget needs nothing declared in its CSP and renders as soon as the data arrives.

No host actions are declared out of the box, and this is the template where adding one pays off most.

Make it yours

A row can call a tool on your own server when clicked. Suppose your server has a get_record tool and your rows carry an id:

html
<tr data-wg-tool="get_record" data-wg-args='{"id": "{{ row.id }}"}'>

The widget calls that tool through the host. Or use data-wg-message on the row to post a follow-up question to the chat instead.

The designs change the table's character: hairlines and a monospace face for numbers in Nordic, a glass sheet with a lit header in Aurora, a black header bar and striped rows in Brutalist, an open table between two strong rules with a small-caps header in Editorial, a terminal listing with dashed lines in Neon.

Take it to your server

Link the published widget from your MCP server or download the HTML and manifest; the guide to adding a UI to your MCP server covers both. If an agent should build the widget, see creating widgets with an AI agent.

The data it takes

FieldTypeWhat it is
titlerequiredstring
columnsrequiredobject[]
└ keyrequiredstringProperty of each row shown in this column
└ labelrequiredstring
└ alignleft | right | center
rowsrequiredobject[]
footnotestring

Sample data

{
  "title": "Outstanding invoices",
  "columns": [
    {
      "key": "number",
      "label": "Invoice"
    },
    {
      "key": "client",
      "label": "Client"
    },
    {
      "key": "due",
      "label": "Due"
    },
    {
      "key": "amount",
      "label": "Amount",
      "align": "right"
    }
  ],
  "rows": [
    {
      "number": "F260031",
      "client": "Fjord Furniture",
      "due": "Oct 15",
      "amount": "€4,200.00"
    },
    {
      "number": "F260032",
      "client": "Aurora Café",
      "due": "Oct 20",
      "amount": "€2,750.00"
    },
    {
      "number": "F260033",
      "client": "Prism Studio",
      "due": "Oct 31",
      "amount": "€6,480.00"
    },
    {
      "number": "F260034",
      "client": "Brio Workshops",
      "due": "Nov 5",
      "amount": "€1,320.00"
    }
  ],
  "footnote": "Amounts include VAT."
}

The tool that shows it

A widget made from this template is shown by one tool. The model reads its description to decide when to call it and fills the input schema with the data. This is the definition Widgetry exports:

{
  "name": "show_table",
  "title": "Table",
  "description": "Shows tabular data: the caller defines the columns (key and header) and passes one object per row. Use it for lists of records such as invoices, leads or orders.",
  "inputSchema": {
    "type": "object",
    "required": [
      "title",
      "columns",
      "rows"
    ],
    "properties": {
      "title": {
        "type": "string"
      },
      "columns": {
        "type": "array",
        "items": {
          "type": "object",
          "required": [
            "key",
            "label"
          ],
          "properties": {
            "key": {
              "type": "string",
              "description": "Property of each row shown in this column"
            },
            "label": {
              "type": "string"
            },
            "align": {
              "type": "string",
              "enum": [
                "left",
                "right",
                "center"
              ]
            }
          }
        }
      },
      "rows": {
        "type": "array",
        "items": {
          "type": "object"
        }
      },
      "footnote": {
        "type": "string"
      }
    }
  }
}

Your first widget, in the chat in minutes

Pick a template, drop in your data and see it as ChatGPT or Claude will show it. Then link it from your MCP server, or let your agent build the next one.