MCP + Structured Output

Give your LLM real tools, not just prompts.

Write a Devlish program. It becomes an MCP tool your AI can call. The AI sends JSON in, gets JSON back, never sees your credentials, and every execution is deterministic and auditable. The program IS the tool.

How LLMs use Devlish tools

The Model Context Protocol (MCP) is the standard way LLMs call external tools. Claude, GPT, Gemini, and local models all support it. Devlish has a built-in MCP server: run devlish mcp and every .dvl file in your tools directory becomes a callable tool.

1
LLM decides to call a tool

"I need to check energy community eligibility for these coordinates."

2
MCP sends JSON input to Devlish

{"latitude": 32.7767, "longitude": -96.7970}

3
Devlish executes the program

Calls Census Bureau API, cross-references DOE data, validates IRS appendix. All in readable English.

4
Returns structured JSON to the LLM

{"eligible": true, "category": "coal_closure", "confidence": "high"}

The LLM never sees the API keys, never parses raw HTML, and never hallucinates the eligibility logic. The Devlish program is the single source of truth for what the tool does.

Structured JSON responses (the LLM output format)

When an LLM calls a Devlish tool, the response comes back as structured JSON that the model can parse directly. No scraping, no regex, no hoping the format is consistent.

Respond with for success

The Respond with statement exits the program and returns a value to the caller. When called via MCP, this becomes the tool's JSON response.

# The tool receives: {"deal_size": 500000, "customer_tier": "enterprise"}

discount_rate equals 0.05
If customer_tier is "enterprise"
  discount_rate equals 0.15

discount equals deal_size times discount_rate

Respond with record with discount as amount and customer_tier as tier

The LLM receives:

{
  "success": true,
  "responded": true,
  "response": {
    "amount": 75000,
    "tier": "enterprise"
  }
}

Fail with for structured errors

When a tool cannot complete (bad input, validation failure, missing data), it returns a structured error the LLM can reason about and retry with corrected input.

If deal_size is less than 10000
  Fail with record with "minimum_not_met" as code and "Deal size must be at least $10,000" as message and 10000 as minimum

The LLM receives:

{
  "success": false,
  "error": "Validation failed",
  "response": {
    "code": "minimum_not_met",
    "message": "Deal size must be at least $10,000",
    "minimum": 10000
  }
}

The LLM can read the code field, understand what went wrong, fix its input, and call the tool again. No ambiguous error strings to parse.

Token efficiency: Toolrun compression

LLM context windows are expensive. Every token in the context costs money and displaces useful reasoning. Devlish includes devlish-toolrun, a compression layer that sits between shell commands and the LLM.

The problem

An LLM asks for "the contents of the inbox folder." The raw ls -la output is 200 lines of permissions, dates, sizes, and filenames. The LLM only needs the filenames and sizes. You just burned 3,000 tokens on noise.

The solution

Toolrun wraps commands, stores the full output locally (for audit), and returns a compact structured summary to the LLM. The ladder works like this:

  1. Raw command (most tokens): LLM sees everything. Expensive.
  2. Compressed tool call: Toolrun strips noise, returns structured JSON. 80% fewer tokens.
  3. Discovered flow: Repeated patterns become a Devlish program the LLM reviews once.
  4. Compiled artifact: The program runs as bytecode. Zero tokens per execution after the first review.
# Instead of the LLM parsing raw command output:
#   drwxr-xr-x  5 admin staff  160 Jul 18 09:00 receipts
#   -rw-r--r--  1 admin staff 2048 Jul 18 08:55 invoice_001.pdf
#   ...200 more lines...

# Devlish returns structured data:
files equals list files in "/inbox/"
pdf_files equals filter files where item ends with ".pdf"
Respond with record with count of pdf_files as total and pdf_files as files

The LLM gets {"total": 47, "files": [...]} instead of parsing ls output. Fewer tokens, no parsing errors, deterministic every time.

Checkpoints: pause execution for LLM review

Some workflows should not run to completion without oversight. A Checkpoint pauses execution and returns the current state to the calling LLM (or human). The caller inspects the data, makes a decision, and resumes.

# Verify spreadsheet formulas, pause for LLM to inspect
Read XLSX cell "B2" from "model.xlsx" as raw_value
expected_value equals 1500000

If raw_value does not equal expected_value
  Checkpoint "Formula mismatch detected. Review before proceeding." saving context as review_state

# Execution resumes here after the LLM approves
Export raw_value to "verified_values.json"

When the checkpoint fires, the LLM receives the full execution context (all variables, the checkpoint message, the state). It can:

This is how you build human-in-the-loop AI workflows where the logic is readable and the pause points are explicit in the source code.

The MCP server

Start the built-in MCP server and every .dvl file becomes a tool:

# Start the MCP server (connects to Claude, Cursor, etc.)
devlish mcp --tools-dir ./tools/

# Or register in your MCP config (claude_desktop_config.json):
{
  "mcpServers": {
    "devlish-tools": {
      "command": "devlish",
      "args": ["mcp", "--tools-dir", "/path/to/tools"]
    }
  }
}

Tool manifest (devlish.toml)

Describe your tools with types so the LLM knows what to send:

name = "pricing-tools"
version = "0.1.0"

[[tools]]
source = "calculate_discount.dvl"
description = "Calculate the discount amount for a deal based on tier and size"

[tools.inputs]
customer_tier = { type = "string", description = "Customer tier: standard, premium, or enterprise" }
deal_size = { type = "number", description = "Total deal value in dollars" }

Built-in MCP tools

The server also exposes utility tools for development:

compileCompile .dvl source to bytecode, return diagnostics
runExecute a compiled program with JSON input
validateCheck syntax without executing
lintReturn style and correctness suggestions

Permissions: what the tool can and cannot do

Every Devlish program declares its permissions at the top of the file. The VM enforces these at runtime. An LLM cannot trick a tool into accessing something it was not designed to access.

# Program manifest (top of file)
# Permissions:
#   Read files from "/data/clients/"
#   HTTP requests to "api.census.gov"
#   HTTP requests to "api.energy.gov"
#
# Boundaries:
#   No writes outside "/tmp/reports/"
#
# Callers:
#   Any MCP client

# If the program tries to read "/etc/passwd" or call a different API,
# the VM returns a permission error. The LLM cannot override this.

This is the opposite of giving an LLM a shell. The tool can only do what its manifest allows, the boundaries are visible in the source, and an auditor can verify them by reading English.

Full example: Energy Community verification tool

# Permissions:
#   HTTP requests to "api.census.gov"
#   HTTP requests to "developer.nrel.gov"
#
# Callers:
#   Any MCP client

Ask "Latitude?" as latitude
Ask "Longitude?" as longitude

# Step 1: Get census tract from coordinates
census_url equals "https://geocoding.geo.census.gov/geocoder/geographies/coordinates"
Get the url at census_url with record with latitude as x and longitude as y as census_response
tract_id equals geoid of result of census_response

# Step 2: Check DOE energy community database
doe_url equals "https://developer.nrel.gov/api/energy-communities/v1/check"
Get the url at doe_url with record with tract_id as tract as doe_response

is_energy_community equals eligible of doe_response
category equals category of doe_response

# Step 3: Build structured response for the LLM
result equals record with is_energy_community as eligible and category as type and tract_id as census_tract

If is_energy_community
  Respond with result
Otherwise
  Fail with record with "not_eligible" as code and tract_id as census_tract and "Location is not in a designated energy community" as reason

The LLM calls this tool with coordinates. It gets back either a success with eligibility details, or a structured failure explaining why. The LLM never touches the Census or DOE APIs directly.

Why not just write Python tools?

Readability A compliance officer can read and verify a Devlish tool. They cannot read Python, even "simple" Python.
Sandboxing Python has full system access by default. Devlish tools can only do what their manifest allows. No monkey-patching, no import tricks.
Determinism Same input always produces same output. No hidden state, no module-level variables that drift between calls.
Token cost A Devlish tool returns compact JSON. A Python script might print debug output, warnings, stack traces. Toolrun compresses further.
Audit trail The event stream records every operation. Who asked, what ran, what was returned, what permissions were used.
No runtime No virtualenv, no pip install, no version conflicts. One binary, runs anywhere.

Build your first LLM tool in 5 minutes.

# Install
curl -sSL https://devlish.com/install.sh | sh

# Create a tool
devlish new my-tool
cd my-tool

# Start the MCP server
devlish mcp

See more examples View source on GitHub