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.
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.
"I need to check energy community eligibility for these coordinates."
{"latitude": 32.7767, "longitude": -96.7970}
Calls Census Bureau API, cross-references DOE data, validates IRS appendix. All in readable English.
{"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.
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 errorsWhen 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.
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.
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.
Toolrun wraps commands, stores the full output locally (for audit), and returns a compact structured summary to the LLM. The ladder works like this:
# 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.
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.
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"]
}
}
}
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" }
The server also exposes utility tools for development:
| compile | Compile .dvl source to bytecode, return diagnostics |
| run | Execute a compiled program with JSON input |
| validate | Check syntax without executing |
| lint | Return style and correctness suggestions |
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.
# 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.
| 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