mlld speaks MCP in both directions: serve your exe functions as tools for any MCP client, or import tools from external servers as callable functions. Tool collections control what agents see. Reshaping customizes tool interfaces.

MCP

mlld speaks MCP in both directions: serve your functions as tools, or import tools from external servers.

Export — serve functions as MCP tools:

exe @greet(name: string) = js { return "Hello " + name; }
export { @greet }
mlld mcp tools.mld

Any MCP client can now call greet. See mcp-export.

Import — use external MCP tools as functions:

import tools { @echo } from mcp "npx -y @modelcontextprotocol/server-everything"
show @echo("hello")

Imported tool outputs carry src:mcp taint automatically. See mcp-import.

Reading order:

Want to... Read
Serve functions as tools mcp-export
Import external MCP tools mcp-import
Control what agents see mcp-tool-gateway, tool-reshaping
Secure MCP data flows mcp-security, mcp-policy, mcp-guards
Track tool usage tool-call-tracking
End-to-end example pattern-guarded-tool-export

Exporting MCP Tools

Export exe functions, run mlld mcp. Every exported function becomes an MCP tool.

exe @status() = js { return "ok"; }
exe @greet(name: string) = js { return "Hello " + name; }
export { @status, @greet }
mlld mcp tools.mld

Clients see two tools: status (no params) and greet (one string param). Name conversion is automatic — @greetUser becomes greet_user over MCP.

Type annotations generate JSON Schema:

exe @search(query: string, limit: number) = cmd {
  gh issue list --search "@query" -L @limit --json number,title
} with { description: "Search issues" }
export { @search }

The with { description } clause populates the tool description. Type annotations (string, number, boolean, object, array) generate the input schema. See exe-metadata.

Serve a directory:

mlld mcp llm/mcp/
mlld mcp "llm/mcp/*.mld.md"

If llm/mcp/ exists, mlld mcp with no arguments serves every module in it.

Environment overrides:

mlld mcp tools.mld --env MLLD_GITHUB_TOKEN=ghp_xxx

Keys must start with MLLD_. Modules read them with import { @MLLD_GITHUB_TOKEN } from @input.

Filter tools:

mlld mcp tools.mld --tools status,greet

Serve reshaped tool collections:

mlld mcp tools.mld --tools-collection @agentTools

Uses bind/expose definitions from a var tools collection instead of raw exports. See mcp-tool-gateway and tool-reshaping.

Client configuration (Claude Code):

{
  "mcpServers": {
    "my-tools": {
      "command": "npx",
      "args": ["mlld", "mcp", "tools.mld"]
    }
  }
}

Security: MCP-served tool inputs keep the caller's existing labels/taint; they do not gain synthetic src:mcp. src:mcp is applied to values returned from imported MCP tools. See mcp-security.

Tool Collections

var tools defines a named collection of tools with metadata. Use it to control what an agent sees and attach labels for guards.

exe @readData() = js { return "ok"; }
exe @deleteData() = js { return "deleted"; }

var tools @agentTools = {
  safeRead: { mlld: @readData },
  dangerousDelete: {
    mlld: @deleteData,
    labels: ["destructive"],
    description: "Deletes records"
  }
}

Tool definition fields:

  • mlld — executable reference
  • labels — guard/policy signals (destructive, net:w)
  • bind — pre-fill parameters (see tool-reshaping)
  • expose — limit visible parameters (see tool-reshaping)
  • description — override tool description

Scope tools to an agent with env:

box @agent with { tools: @agentTools } [
  run cmd { claude -p @task }
]

The agent only sees tools in @agentTools. Guards check @mx.op.labels on each call.

Serve a collection over MCP:

mlld mcp tools.mld --tools-collection @agentTools

The --tools-collection flag serves the reshaped collection instead of raw exports. Bound parameters are hidden; only exposed parameters appear in the tool schema. See mcp-export for basic serving, pattern-guarded-tool-export for a complete example.

Guard on labels:

guard @blockDestructive before op:exe = when [
  @mx.op.labels.includes("destructive") => deny "Blocked"
  * => allow
]

Labels from the tool definition flow to @mx.op.labels in guard context. See mcp-guards.

Create a collection directly from a runtime MCP spec:

var @serverSpec = "node ./calendar-server.cjs"
var tools trusted @calendarTools = mcp @serverSpec
show @calendarTools.createEvent.description

This asks the MCP server for its tool schema and builds the ToolCollection directly from the discovered tools. It is not object-literal normalization:

  • Use object literals when you want bind, expose, optional, controlArgs, or per-tool labels.
  • Use var tools @t = mcp @expr when the server command is only known at runtime and you want the discovered collection as a value.
  • Normal var labels still apply to the collection variable itself, as in trusted @calendarTools.

Use import tools from mcp "..." when you want callable functions or a namespace in the current scope. Use var tools @t = mcp @expr when you want to pass a runtime-built collection into box with { tools: @t } or other tool-collection APIs.

Tool Reshaping

Reshape tool interfaces using bind and expose to control what parameters agents see. Used in var tools collections (see mcp-tool-gateway).

bind - Pre-fill parameters:

exe @createIssue(owner: string, repo: string, title: string, body: string) = cmd {
  gh issue create -R @owner/@repo -t "@title" -b "@body"
}

var tools @agentTools = {
  createIssue: {
    mlld: @createIssue,
    bind: { owner: "mlld", repo: "infra" }
  }
}

The agent sees only title and body. The bound parameters owner and repo are fixed.

expose - Limit visible parameters:

var tools @agentTools = {
  createIssue: {
    mlld: @createIssue,
    bind: { owner: "mlld", repo: "infra" },
    expose: ["title", "body"]
  }
}

Explicitly list which parameters appear in the tool schema. Parameters not in expose are hidden from the agent.

Default behavior:

Without expose, all parameters except those in bind are visible. Adding expose overrides this - only listed parameters appear.

Variable binding:

Bound values can reference variables:

var @org = "mlld"
var @defaultRepo = "main"

var tools @agentTools = {
  createIssue: {
    mlld: @createIssue,
    bind: { owner: @org, repo: @defaultRepo }
  }
}

Variables are resolved when the tool collection is defined, not when called.

Nested objects in bind:

var tools @agentTools = {
  configure: {
    mlld: @configure,
    bind: { config: { timeout: 30, retries: 3 } }
  }
}

Complete example:

exe @searchDocs(index: string, query: string, limit: number, format: string) = cmd {
  search-tool --index @index -q "@query" -n @limit --format @format
}

var tools @agentTools = {
  searchDocs: {
    mlld: @searchDocs,
    bind: { index: "production", format: "json" },
    expose: ["query", "limit"],
    description: "Search documentation"
  }
}

The agent sees:

  • query (string, required)
  • limit (number, required)

Hidden from agent:

  • index (always "production")
  • format (always "json")

Importing MCP Tools

Import tools from an MCP server as callable exe functions. The server spec is a shell command that launches the server.

Selected import:

import tools { @echo } from mcp "npx -y @modelcontextprotocol/server-everything"
show @echo("hello")

Namespace import:

import tools from mcp "npx -y @modelcontextprotocol/server-filesystem /workspace" as @fs
show @fs.listDirectory("/workspace")

Namespace import requires as @alias.

Dynamic specs already worked through interpolation:

var @serverSpec = "node ./calendar-server.cjs"
import tools from mcp "@serverSpec" as @calendar

That form remains useful when you want imported functions or a namespace in the current scope.

Name conversion is automatic. MCP's list_directory becomes mlld's @listDirectory. The mapping works in both directions.

Type coercion is automatic. Arguments are coerced to match the MCP tool's inputSchema types before dispatch. A string where the schema expects an array is wrapped ("x"["x"]), string numbers are parsed, "true"/"false" become booleans, and JSON strings are parsed for object/array types.

Name-based argument matching: When calling an MCP tool with variable references whose names match schema properties, arguments are matched by name instead of position:

exe createEvent(title, participants, date) =
  @mcp.createCalendarEvent(@title, @participants, @date)

Even if the MCP schema declares participants before title, mlld matches @title to the title property and @participants to participants. Falls back to positional mapping when arg names don't match.

SDK server injection: When using the SDK, mcpServers maps logical names to commands per-execution. import tools from mcp "name" checks the map before treating the spec as a shell command:

client.execute('./agent.mld', payload,
    mcp_servers={'tools': f'uv run python3 server.py {config}'})
import tools from mcp "tools" as @t

Each execute() call gets an independent server lifecycle, enabling parallel executions with isolated MCP state.

import tools vs var tools = mcp @expr:

  • import tools ... imports callable functions or a namespace into the current scope.
  • var tools @t = mcp @expr builds a ToolCollection value from runtime discovery.
  • Use the var tools form when you want to hand a discovered collection to box with { tools: @t } or another API that expects a tool collection object.

Security: All MCP tool outputs carry src:mcp taint automatically. See mcp-security for propagation details, mcp-guards for filtering, mcp-policy for flow restrictions.