Skip to content

Proposal: resolved-schema MCP tools for LLM/agent discoverability. #808

Description

@phongnt1809

Problem

The Publisher MCP API exposes models to LLM agents primarily through malloy_modelGetText, which returns raw .malloy source. To plan a query, an agent must mentally compile that source — resolving extend:, walking imports, classifying fields as dimensions vs. measures, inferring types, traversing joins, and reconciling givens and source parameters.

This works for large frontier models with enough context and reasoning capacity, but it has real costs:

  • Prompt size. A non-trivial model can run several thousand tokens of source text, much of it irrelevant to any one query.
  • Hallucination surface. Agents invent fields that don't exist, put measures in group_by:, or miss joined-field paths because they didn't trace the graph.
  • SLM viability. Smaller models (7B–14B) lack the capacity to compile mentally and fail at a much higher rate than on equivalent SQL tasks — despite Malloy's semantic layer being structurally well-suited to LLM use.
  • Givens (experimental) raise the stakes. A model with given: TENANT :: string used for row-level access control depends on the host supplying values correctly. An agent that doesn't see the given contract can construct a query that fails at runtime, or — in misconfigured deployments — silently bypasses intended tenancy boundaries. This is a safety-relevant gap, not just an ergonomic one.

Proposal

Add tool — malloy_schemaGetResolved — as both an MCP tool, returning a flat, typed view of a model derived from the compiled Model object.

Inputs

{
  projectName: string,
  packageName: string,
  modelPath: string,
  sourceName?: string   // optional; if omitted, return all sources in the model
}

Output (proposed shape — open to revision)

{
  "modelPath": "ecommerce/orders.malloy",
  "givens": [
    {
      "name": "TENANT",
      "type": {"kind": "string_type"},
      "default_value": {"kind": "stringLiteral", "number_value": "TENANT_A"},
      "default_text": "\"TENANT_A\"",
      "finalized": true,
      "annotations": [{"value": "# label=\"Tenant ID\"\n"}],
    },
    {
      "name": "MAX_ROWS",
      "type": {"kind": "number_type"},
      "default_value": {"kind": "number_literal", "number_value": 1000},
      "default_text": "1000",
      "finalized": false,
      "annotations": []
    }
  ],
  "sources": [
    {
      "kind": "source",
      "name": "orders",
      "annotations": [],
      "parameters": [
        {
          "name": "min_date",
          "type": {"kind": "date_type"}
        }
      ],
      "schema": {
        "fields": [
          {
            "kind": "dimension",
            "name": "order_id",
            "type": {"kind": "string_type"}
          },
          {
            "kind": "measure",
            "name": "order_total",
            "type": {"kind": "number_type", "subtype": "decimal"}
          },
          {
            "kind": "join",
            "name": "users",
            "relationship": "one",
            "schema": {
              "fields": [
                {
                  "kind": "dimension",
                  "name": "country",
                  "type": {"kind": "string_type"}
                }
              ]
            }
          },
          {
            "kind": "view",
            "name": "recent",
            "annotations": [{"value": "# bar_chart\n"}],
          }
        ]
      }
    }
  ],
  "queries": []
}

Looking forward to hearing your thoughts on this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions