Builtin functions
Complete reference for all built-in functions available in Harn.
Output
| Function | Parameters | Returns | Description |
|---|---|---|---|
log(msg) | msg: any | nil | Print with [harn] prefix and newline |
print(msg) | msg: any | nil | Print without prefix or newline |
println(msg) | msg: any | nil | Print with newline, no prefix |
Type conversion
| Function | Parameters | Returns | Description |
|---|---|---|---|
type_of(value) | value: any | string | Returns type name: "int", "float", "string", "bool", "nil", "list", "dict", "closure", "taskHandle", "duration", "enum", "struct" |
to_string(value) | value: any | string | Convert to string representation |
to_int(value) | value: any | int or nil | Parse/convert to integer. Floats truncate, bools become 0/1 |
to_float(value) | value: any | float or nil | Parse/convert to float |
Runtime shape validation
Function parameters with structural type annotations (shapes) are validated at runtime. If a dict or struct argument is missing a required field or has the wrong field type, a descriptive error is thrown before the function body executes.
fn greet(u: {name: string, age: int}) {
println("${u.name} is ${u.age}")
}
greet({name: "Alice", age: 30}) // OK
greet({name: "Alice"}) // Error: parameter 'u': missing field 'age' (int)
See Error handling – Runtime shape validation errors for more details.
Result
Harn has a built-in Result type for representing success/failure values
without exceptions. Ok and Err create Result.Ok and Result.Err
enum variants respectively. When called on a non-Result value, unwrap
and unwrap_or pass the value through unchanged.
| Function | Parameters | Returns | Description |
|---|---|---|---|
Ok(value) | value: any | Result.Ok | Create a Result.Ok value |
Err(value) | value: any | Result.Err | Create a Result.Err value |
is_ok(result) | result: any | bool | Returns true if value is Result.Ok |
is_err(result) | result: any | bool | Returns true if value is Result.Err |
unwrap(result) | result: any | any | Extract Ok value. Throws on Err. Non-Result values pass through |
unwrap_or(result, default) | result: any, default: any | any | Extract Ok value. Returns default on Err. Non-Result values pass through |
unwrap_err(result) | result: any | any | Extract Err value. Throws on non-Err |
Example:
let good = Ok(42)
let bad = Err("something went wrong")
println(is_ok(good)) // true
println(is_err(bad)) // true
println(unwrap(good)) // 42
println(unwrap_or(bad, 0)) // 0
println(unwrap_err(bad)) // something went wrong
JSON
| Function | Parameters | Returns | Description |
|---|---|---|---|
json_parse(str) | str: string | value | Parse JSON string into Harn values. Throws on invalid JSON |
json_stringify(value) | value: any | string | Serialize Harn value to JSON. Closures and handles become null |
json_validate(data, schema) | data: any, schema: dict | bool | Validate data against a schema. Returns true if valid, throws with details if not |
json_extract(text, key?) | text: string, key: string (optional) | value | Extract JSON from text (strips markdown code fences). If key given, returns that key’s value |
Type mapping:
| JSON | Harn |
|---|---|
| string | string |
| integer | int |
| decimal/exponent | float |
| true/false | bool |
| null | nil |
| array | list |
| object | dict |
json_validate schema format
The schema is a plain Harn dict (not JSON Schema). Supported keys:
| Key | Type | Description |
|---|---|---|
type | string | Expected type: "string", "int", "float", "bool", "list", "dict", "any" |
required | list | List of required key names (for dicts) |
properties | dict | Dict mapping property names to sub-schemas (for dicts) |
items | dict | Schema to validate each item against (for lists) |
Example:
let schema = {
type: "dict",
required: ["name", "age"],
properties: {
name: {type: "string"},
age: {type: "int"},
tags: {type: "list", items: {type: "string"}}
}
}
json_validate(data, schema) // throws if invalid
json_extract
Extracts JSON from LLM responses that may contain markdown code fences
or surrounding prose. Handles ```json ... ```, ``` ... ```,
and bare JSON with surrounding text.
let response = llm_call("Return JSON with name and age")
let data = json_extract(response) // parse, stripping fences
let name = json_extract(response, "name") // extract just one key
Math
| Function | Parameters | Returns | Description |
|---|---|---|---|
abs(n) | n: int or float | int or float | Absolute value |
ceil(n) | n: float | int | Ceiling (rounds up). Ints pass through unchanged |
floor(n) | n: float | int | Floor (rounds down). Ints pass through unchanged |
round(n) | n: float | int | Round to nearest integer. Ints pass through unchanged |
sqrt(n) | n: int or float | float | Square root |
pow(base, exp) | base: number, exp: number | int or float | Exponentiation. Returns int when both args are int and exp is non-negative |
min(a, b) | a: number, b: number | int or float | Minimum of two values. Returns float if either argument is float |
max(a, b) | a: number, b: number | int or float | Maximum of two values. Returns float if either argument is float |
random() | none | float | Random float in [0, 1) |
random_int(min, max) | min: int, max: int | int | Random integer in [min, max] inclusive |
Trigonometry
| Function | Parameters | Returns | Description |
|---|---|---|---|
sin(n) | n: float | float | Sine (radians) |
cos(n) | n: float | float | Cosine (radians) |
tan(n) | n: float | float | Tangent (radians) |
asin(n) | n: float | float | Inverse sine |
acos(n) | n: float | float | Inverse cosine |
atan(n) | n: float | float | Inverse tangent |
atan2(y, x) | y: float, x: float | float | Two-argument inverse tangent |
Logarithms and exponentials
| Function | Parameters | Returns | Description |
|---|---|---|---|
log2(n) | n: float | float | Base-2 logarithm |
log10(n) | n: float | float | Base-10 logarithm |
ln(n) | n: float | float | Natural logarithm |
exp(n) | n: float | float | Euler’s number raised to the power n |
Constants and utilities
| Function | Parameters | Returns | Description |
|---|---|---|---|
pi() | none | float | The constant pi (3.14159…) |
e() | none | float | Euler’s number (2.71828…) |
sign(n) | n: int or float | int | Sign of a number: -1, 0, or 1 |
is_nan(n) | n: float | bool | Check if value is NaN |
is_infinite(n) | n: float | bool | Check if value is infinite |
Sets
| Function | Parameters | Returns | Description |
|---|---|---|---|
set(items?) | items: list (optional) | set | Create a new set, optionally from a list |
set_add(s, value) | s: set, value: any | set | Add a value to a set, returns new set |
set_remove(s, value) | s: set, value: any | set | Remove a value from a set, returns new set |
set_contains(s, value) | s: set, value: any | bool | Check if set contains a value |
set_union(a, b) | a: set, b: set | set | Union of two sets |
set_intersect(a, b) | a: set, b: set | set | Intersection of two sets |
set_intersection(a, b) | a: set, b: set | set | Alias for set_intersect |
set_difference(a, b) | a: set, b: set | set | Difference (elements in a but not b) |
set_symmetric_difference(a, b) | a: set, b: set | set | Elements in either but not both |
set_is_subset(a, b) | a: set, b: set | bool | True if all elements of a are in b |
set_is_superset(a, b) | a: set, b: set | bool | True if a contains all elements of b |
set_is_disjoint(a, b) | a: set, b: set | bool | True if a and b share no elements |
to_list(s) | s: set | list | Convert a set to a list |
Set methods (dot syntax)
Sets also support method syntax: my_set.union(other).
| Method | Parameters | Returns | Description |
|---|---|---|---|
.count() / .len() | none | int | Number of elements |
.empty() | none | bool | True if set is empty |
.contains(val) | val: any | bool | Check membership |
.add(val) | val: any | set | New set with val added |
.remove(val) | val: any | set | New set with val removed |
.union(other) | other: set | set | Union |
.intersect(other) | other: set | set | Intersection |
.difference(other) | other: set | set | Elements in self but not other |
.symmetric_difference(other) | other: set | set | Elements in either but not both |
.is_subset(other) | other: set | bool | True if self is a subset of other |
.is_superset(other) | other: set | bool | True if self is a superset of other |
.is_disjoint(other) | other: set | bool | True if no shared elements |
.to_list() | none | list | Convert to list |
.map(fn) | fn: closure | set | Transform elements (deduplicates) |
.filter(fn) | fn: closure | set | Keep elements matching predicate |
.any(fn) | fn: closure | bool | True if any element matches |
.all(fn) / .every(fn) | fn: closure | bool | True if all elements match |
String functions
| Function | Parameters | Returns | Description |
|---|---|---|---|
len(value) | value: string, list, or dict | int | Length of string (chars), list (items), or dict (keys) |
trim(str) | str: string | string | Remove leading and trailing whitespace |
lowercase(str) | str: string | string | Convert to lowercase |
uppercase(str) | str: string | string | Convert to uppercase |
split(str, sep) | str: string, sep: string | list | Split string by separator |
starts_with(str, prefix) | str: string, prefix: string | bool | Check if string starts with prefix |
ends_with(str, suffix) | str: string, suffix: string | bool | Check if string ends with suffix |
contains(str, substr) | str: string, substr: string | bool | Check if string contains substring. Also works on lists |
replace(str, old, new) | str: string, old: string, new: string | string | Replace all occurrences |
join(list, sep) | list: list, sep: string | string | Join list elements with separator |
substring(str, start, len?) | str: string, start: int, len: int | string | Extract substring from start position |
format(template, ...) | template: string, args: any | string | Format string with {} placeholders |
String methods (dot syntax)
These are called on string values with dot notation: "hello".uppercase().
| Method | Parameters | Returns | Description |
|---|---|---|---|
.trim() | none | string | Remove leading/trailing whitespace |
.trim_start() | none | string | Remove leading whitespace only |
.trim_end() | none | string | Remove trailing whitespace only |
.lines() | none | list | Split string by newlines |
.char_at(index) | index: int | string or nil | Character at index (nil if out of bounds) |
.index_of(substr) | substr: string | int | First character offset of substring (-1 if not found) |
.last_index_of(substr) | substr: string | int | Last character offset of substring (-1 if not found) |
.len() | none | int | Character count |
.chars() | none | list | List of single-character strings |
.reverse() | none | string | Reversed string |
.repeat(n) | n: int | string | Repeat n times |
.pad_left(width, char?) | width: int, char: string | string | Pad to width with char (default space) |
.pad_right(width, char?) | width: int, char: string | string | Pad to width with char (default space) |
List methods (dot syntax)
| Method | Parameters | Returns | Description |
|---|---|---|---|
.map(fn) | fn: closure | list | Transform each element |
.filter(fn) | fn: closure | list | Keep elements where fn returns truthy |
.reduce(init, fn) | init: any, fn: closure | any | Fold with accumulator |
.find(fn) | fn: closure | any or nil | First element matching predicate |
.find_index(fn) | fn: closure | int | Index of first match (-1 if not found) |
.any(fn) | fn: closure | bool | True if any element matches |
.all(fn) / .every(fn) | fn: closure | bool | True if all elements match |
.none(fn?) | fn: closure | bool | True if no elements match (no arg: checks emptiness) |
.first(n?) | n: int (optional) | any or list | First element, or first n elements |
.last(n?) | n: int (optional) | any or list | Last element, or last n elements |
.partition(fn) | fn: closure | list | Split into [[truthy], [falsy]] |
.group_by(fn) | fn: closure | dict | Group into dict keyed by fn result |
.sort() / .sort_by(fn) | fn: closure (optional) | list | Sort (natural or by key function) |
.min() / .max() | none | any | Minimum/maximum value |
.min_by(fn) / .max_by(fn) | fn: closure | any | Min/max by key function |
.chunk(size) | size: int | list | Split into chunks of size |
.each_cons(size) | size: int | list | Sliding windows of size |
.compact() | none | list | Remove nil values |
.unique() | none | list | Remove duplicates |
.flatten() | none | list | Flatten one level of nesting |
.flat_map(fn) | fn: closure | list | Map then flatten |
.tally() | none | dict | Frequency count: {value: count} |
.zip(other) | other: list | list | Pair elements from two lists |
.enumerate() | none | list | List of {index, value} dicts |
.take(n) / .skip(n) | n: int | list | First/remaining n elements |
.sum() | none | int or float | Sum of numeric values |
.join(sep?) | sep: string | string | Join to string |
.reverse() | none | list | Reversed list |
.push(item) / .pop() | item: any | list | New list with item added/removed (immutable) |
.contains(item) | item: any | bool | Check if list contains item |
.index_of(item) | item: any | int | Index of item (-1 if not found) |
.slice(start, end?) | start: int, end: int | list | Slice with negative index support |
Path functions
| Function | Parameters | Returns | Description |
|---|---|---|---|
dirname(path) | path: string | string | Directory component of path |
basename(path) | path: string | string | File name component of path |
extname(path) | path: string | string | File extension including dot (e.g., .harn) |
path_join(parts...) | parts: strings | string | Join path components |
File I/O
| Function | Parameters | Returns | Description |
|---|---|---|---|
read_file(path) | path: string | string | Read entire file as UTF-8 string. Throws on failure |
write_file(path, content) | path: string, content: string | nil | Write string to file. Throws on failure |
append_file(path, content) | path: string, content: string | nil | Append string to file, creating it if it doesn’t exist. Throws on failure |
copy_file(src, dst) | src: string, dst: string | nil | Copy a file. Throws on failure |
delete_file(path) | path: string | nil | Delete a file or directory (recursive). Throws on failure |
file_exists(path) | path: string | bool | Check if a file or directory exists |
list_dir(path?) | path: string (default ".") | list | List directory contents as sorted list of file names. Throws on failure |
mkdir(path) | path: string | nil | Create directory and all parent directories. Throws on failure |
stat(path) | path: string | dict | File metadata: {size, is_file, is_dir, readonly, modified}. Throws on failure |
temp_dir() | none | string | System temporary directory path |
render(path, bindings?) | path: string, bindings: dict | string | Read a template file and replace {{key}} placeholders with values from bindings dict. Without bindings, just reads the file |
Environment and system
| Function | Parameters | Returns | Description |
|---|---|---|---|
env(name) | name: string | string or nil | Read environment variable |
timestamp() | none | float | Unix timestamp in seconds with sub-second precision |
elapsed() | none | int | Milliseconds since VM startup |
exec(cmd, args...) | cmd: string, args: strings | dict | Execute external command. Returns {stdout, stderr, status, success} |
shell(cmd) | cmd: string | dict | Execute command via shell. Returns {stdout, stderr, status, success} |
exit(code) | code: int (default 0) | never | Terminate the process |
username() | none | string | Current OS username |
hostname() | none | string | Machine hostname |
platform() | none | string | OS name: "darwin", "linux", or "windows" |
arch() | none | string | CPU architecture (e.g., "aarch64", "x86_64") |
home_dir() | none | string | User’s home directory path |
pid() | none | int | Current process ID |
cwd() | none | string | Current working directory |
source_dir() | none | string | Directory of the currently-executing .harn file (falls back to cwd) |
project_root() | none | string or nil | Nearest ancestor directory containing harn.toml |
date_iso() | none | string | Current UTC time in ISO 8601 format (e.g., "2026-03-29T14:30:00.123Z") |
Regular expressions
| Function | Parameters | Returns | Description |
|---|---|---|---|
regex_match(pattern, text) | pattern: string, text: string | list or nil | Find all non-overlapping matches. Returns nil if no matches |
regex_replace(pattern, replacement, text) | pattern: string, replacement: string, text: string | string | Replace all matches. Throws on invalid regex |
regex_captures(pattern, text) | pattern: string, text: string | list | Find all matches with capture group details |
regex_captures
Returns a list of dicts, one per match. Each dict contains:
match– the full matched stringgroups– a list of positional capture group values (from(...))- Named capture groups (from
(?P<name>...)) appear as additional keys
let results = regex_captures("(\\w+)@(\\w+)", "alice@example bob@test")
// [
// {match: "alice@example", groups: ["alice", "example"]},
// {match: "bob@test", groups: ["bob", "test"]}
// ]
Named capture groups are added as top-level keys on each result dict:
let named = regex_captures("(?P<user>\\w+):(?P<role>\\w+)", "alice:admin")
// [{match: "alice:admin", groups: ["alice", "admin"], user: "alice", role: "admin"}]
Returns an empty list if there are no matches. Throws on invalid regex.
Encoding
| Function | Parameters | Returns | Description |
|---|---|---|---|
base64_encode(string) | string: string | string | Base64 encode a string (standard alphabet with padding) |
base64_decode(string) | string: string | string | Base64 decode a string. Throws on invalid input |
Example:
let encoded = base64_encode("Hello, World!")
println(encoded) // SGVsbG8sIFdvcmxkIQ==
println(base64_decode(encoded)) // Hello, World!
Hashing
| Function | Parameters | Returns | Description |
|---|---|---|---|
sha256(string) | string: string | string | SHA-256 hash, returned as a lowercase hex-encoded string |
md5(string) | string: string | string | MD5 hash, returned as a lowercase hex-encoded string |
Example:
println(sha256("hello")) // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
println(md5("hello")) // 5d41402abc4b2a76b9719d911017c592
Date/Time
| Function | Parameters | Returns | Description |
|---|---|---|---|
date_now() | none | dict | Current UTC datetime as dict with year, month, day, hour, minute, second, weekday, and timestamp fields |
date_parse(str) | str: string | float | Parse a datetime string (e.g., "2024-01-15 10:30:00") into a Unix timestamp. Extracts numeric components from the string. Throws if fewer than 3 parts (year, month, day). Validates month (1-12), day (1-31), hour (0-23), minute (0-59), second (0-59) |
date_format(dt, format?) | dt: float, int, or dict; format: string (default "%Y-%m-%d %H:%M:%S") | string | Format a timestamp or date dict as a string. Supports %Y, %m, %d, %H, %M, %S placeholders. Throws for negative timestamps |
Testing
| Function | Parameters | Returns | Description |
|---|---|---|---|
assert(condition, msg?) | condition: any, msg: string (optional) | nil | Assert value is truthy. Throws with message on failure |
assert_eq(a, b, msg?) | a: any, b: any, msg: string (optional) | nil | Assert two values are equal. Throws with message on failure |
assert_ne(a, b, msg?) | a: any, b: any, msg: string (optional) | nil | Assert two values are not equal. Throws with message on failure |
HTTP
| Function | Parameters | Returns | Description |
|---|---|---|---|
http_get(url, options?) | url: string, options: dict | dict | GET request |
http_post(url, body, options?) | url: string, body: string, options: dict | dict | POST request |
http_put(url, body, options?) | url: string, body: string, options: dict | dict | PUT request |
http_patch(url, body, options?) | url: string, body: string, options: dict | dict | PATCH request |
http_delete(url, options?) | url: string, options: dict | dict | DELETE request |
http_request(method, url, options?) | method: string, url: string, options: dict | dict | Generic HTTP request |
All HTTP functions return {status: int, headers: dict, body: string, ok: bool}.
Options: timeout (ms), retries, backoff (ms), headers (dict),
auth (string or {bearer: "token"} or {basic: {user, password}}),
follow_redirects (bool), max_redirects (int), body (string).
Throws on network errors.
Mock HTTP
For testing pipelines that make HTTP calls without hitting real servers.
| Function | Parameters | Returns | Description |
|---|---|---|---|
http_mock(method, url_pattern, response) | method: string, url_pattern: string, response: dict | nil | Register a mock. Use * in url_pattern for glob matching (supports multiple * wildcards, e.g., https://api.example.com/*/items/*) |
http_mock_clear() | none | nil | Clear all mocks and recorded calls |
http_mock_calls() | none | list | Return list of {method, url, body} for all intercepted calls |
http_mock("GET", "https://api.example.com/users", {
status: 200,
body: "{\"users\": [\"alice\"]}",
headers: {}
})
let resp = http_get("https://api.example.com/users")
assert_eq(resp.status, 200)
Interactive input
| Function | Parameters | Returns | Description |
|---|---|---|---|
prompt_user(msg) | msg: string (optional) | string | Display message, read line from stdin |
Async and timing
| Function | Parameters | Returns | Description |
|---|---|---|---|
sleep(duration) | duration: int (ms) or duration literal | nil | Pause execution |
Concurrency primitives
Channels
| Function | Parameters | Returns | Description |
|---|---|---|---|
channel(name?) | name: string (default "default") | dict | Create a channel with name, type, and messages fields |
send(ch, value) | ch: dict, value: any | nil | Send a value to a channel |
receive(ch) | ch: dict | any | Receive a value from a channel (blocks until data available) |
close_channel(ch) | ch: channel | nil | Close a channel, preventing further sends |
try_receive(ch) | ch: channel | any or nil | Non-blocking receive. Returns nil if no data available |
select(ch1, ch2, ...) | channels: channel | dict or nil | Wait for data on any channel. Returns {index, value, channel} for the first ready channel, or nil if all closed |
Atomics
| Function | Parameters | Returns | Description |
|---|---|---|---|
atomic(initial?) | initial: any (default 0) | dict | Create an atomic value |
atomic_get(a) | a: dict | any | Read the current value |
atomic_set(a, value) | a: dict, value: any | dict | Returns new atomic with updated value |
atomic_add(a, delta) | a: dict, delta: int | dict | Returns new atomic with incremented value |
Persistent store
| Function | Parameters | Returns | Description |
|---|---|---|---|
store_get(key) | key: string | any | Retrieve value from store, nil if missing |
store_set(key, value) | key: string, value: any | nil | Store value, auto-saves to .harn/store.json |
store_delete(key) | key: string | nil | Remove key from store |
store_list() | none | list | List all keys (sorted) |
store_save() | none | nil | Explicitly flush store to disk |
store_clear() | none | nil | Remove all keys from store |
The store is backed by .harn/store.json relative to the script’s
directory. The file is created lazily on first store_set. In bridge mode,
the host can override these builtins.
LLM
See LLM calls and agent loops for full documentation.
| Function | Parameters | Returns | Description |
|---|---|---|---|
llm_call(prompt, system?, options?) | prompt: string, system: string, options: dict | string | Single LLM request |
agent_loop(prompt, system?, options?) | prompt: string, system: string, options: dict | dict | Multi-turn agent loop with ##DONE## sentinel. Returns {status, text, iterations, duration_ms, tools_used} |
llm_info() | — | dict | Current LLM config: {provider, model, api_key_set} |
llm_usage() | — | dict | Cumulative usage: {input_tokens, output_tokens, total_duration_ms, call_count} |
llm_resolve_model(alias) | alias: string | dict | Resolve model alias to {id, provider} via providers.toml |
llm_infer_provider(model_id) | model_id: string | string | Infer provider from model ID (e.g. "claude-*" → "anthropic") |
llm_model_tier(model_id) | model_id: string | string | Get capability tier: "small", "mid", or "frontier" |
llm_healthcheck(provider?) | provider: string | dict | Validate API key. Returns {valid, message, metadata} |
llm_providers() | — | list | List all configured provider names |
llm_config(provider?) | provider: string | dict | Get provider config (base_url, auth_style, etc.) |
llm_cost(model, input_tokens, output_tokens) | model: string, input_tokens: int, output_tokens: int | float | Estimate USD cost from embedded pricing table |
llm_session_cost() | — | dict | Session totals: {total_cost, input_tokens, output_tokens, call_count} |
llm_budget(max_cost) | max_cost: float | nil | Set session budget in USD. LLM calls throw if exceeded |
llm_budget_remaining() | — | float or nil | Remaining budget (nil if no budget set) |
Provider configuration
LLM provider endpoints, model aliases, inference rules, and default parameters are configured via a TOML file. The VM searches for config in this order:
HARN_PROVIDERS_CONFIGenv var (explicit path)~/.config/harn/providers.toml- Built-in defaults (Anthropic, OpenAI, OpenRouter, HuggingFace, Ollama)
See harn init to generate a default config file, or create one manually:
[providers.anthropic]
base_url = "https://api.anthropic.com/v1"
auth_style = "header"
auth_header = "x-api-key"
auth_env = "ANTHROPIC_API_KEY"
chat_endpoint = "/messages"
[aliases]
sonnet = { id = "claude-sonnet-4-20250514", provider = "anthropic" }
[[inference_rules]]
pattern = "claude-*"
provider = "anthropic"
[[tier_rules]]
pattern = "claude-*"
tier = "frontier"
[model_defaults."qwen/*"]
temperature = 0.3
Timers
| Function | Parameters | Returns | Description |
|---|---|---|---|
timer_start(name?) | name: string | dict | Start a named timer |
timer_end(timer) | timer: dict | int | Stop timer, prints elapsed, returns milliseconds |
elapsed() | — | int | Milliseconds since process start |
Structured logging
| Function | Parameters | Returns | Description |
|---|---|---|---|
log_json(key, value) | key: string, value: any | nil | Emit a JSON log line with timestamp |
Metadata
Project metadata store backed by .burin/metadata/ sharded JSON files.
Supports hierarchical namespace resolution (child directories inherit
from parents).
| Function | Parameters | Returns | Description |
|---|---|---|---|
metadata_get(dir, namespace?) | dir: string, namespace: string | dict | nil | Read metadata with inheritance |
metadata_set(dir, namespace, data) | dir: string, namespace: string, data: dict | nil | Write metadata for directory/namespace |
metadata_save() | — | nil | Flush metadata to disk |
metadata_stale(project) | project: string | dict | Check staleness: {any_stale, tier1, tier2} |
metadata_refresh_hashes() | — | nil | Recompute content hashes |
compute_content_hash(dir) | dir: string | string | Hash of directory contents |
invalidate_facts(dir) | dir: string | nil | Mark cached facts as stale |
MCP (Model Context Protocol)
Connect to external tool servers using the Model Context Protocol. Supports stdio transport (spawns a child process).
| Function | Parameters | Returns | Description |
|---|---|---|---|
mcp_connect(command, args?) | command: string, args: list | mcp_client | Spawn an MCP server and perform the initialize handshake |
mcp_list_tools(client) | client: mcp_client | list | List available tools from the server |
mcp_call(client, name, arguments?) | client: mcp_client, name: string, arguments: dict | string or list | Call a tool and return the result |
mcp_list_resources(client) | client: mcp_client | list | List available resources from the server |
mcp_list_resource_templates(client) | client: mcp_client | list | List resource templates (URI templates) from the server |
mcp_read_resource(client, uri) | client: mcp_client, uri: string | string or list | Read a resource by URI |
mcp_list_prompts(client) | client: mcp_client | list | List available prompts from the server |
mcp_get_prompt(client, name, arguments?) | client: mcp_client, name: string, arguments: dict | dict | Get a prompt with optional arguments |
mcp_server_info(client) | client: mcp_client | dict | Get connection info (name, connected) |
mcp_disconnect(client) | client: mcp_client | nil | Kill the server process and release resources |
Example:
let client = mcp_connect("npx", ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"])
let tools = mcp_list_tools(client)
println(tools)
let result = mcp_call(client, "read_file", {"path": "/tmp/hello.txt"})
println(result)
mcp_disconnect(client)
Notes:
mcp_callreturns a string when the tool produces a single text block, a list of content dicts for multi-block results, or nil when empty.- If the tool reports
isError: true,mcp_callthrows the error text. mcp_connectthrows if the command cannot be spawned or the initialize handshake fails.
Auto-connecting MCP servers via harn.toml
Instead of calling mcp_connect manually, you can declare MCP servers in
harn.toml. They will be connected automatically before the pipeline executes
and made available through the global mcp dict.
Add a [[mcp]] entry for each server:
[[mcp]]
name = "filesystem"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
[[mcp]]
name = "github"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
Each entry requires:
| Field | Type | Description |
|---|---|---|
name | string | Identifier used to access the client (e.g., mcp.filesystem) |
command | string | Executable to spawn |
args | list of strings | Command-line arguments (default: empty) |
The connected clients are available as properties on the mcp global dict:
pipeline default() {
let tools = mcp_list_tools(mcp.filesystem)
println(tools)
let result = mcp_call(mcp.github, "list_issues", {repo: "harn"})
println(result)
}
If a server fails to connect, a warning is printed to stderr and that
server is omitted from the mcp dict. Other servers still connect
normally. The mcp global is only defined when at least one server
connects successfully.
MCP Server Mode
Harn pipelines can expose tools, resources, resource templates, and prompts
as an MCP server using harn mcp-serve. The CLI serves them over stdio
using the MCP protocol, making them callable by Claude Desktop, Cursor,
or any MCP client.
| Function | Parameters | Returns | Description |
|---|---|---|---|
tool_registry() | — | dict | Create an empty tool registry |
tool_define(registry, name, desc, config) | registry, name, desc: string, config: dict | dict | Add a tool (config: {params, handler, annotations?}) |
mcp_tools(registry) | registry: dict | nil | Register tools for MCP serving |
mcp_resource(config) | config: dict | nil | Register a static resource ({uri, name, text, description?, mime_type?}) |
mcp_resource_template(config) | config: dict | nil | Register a resource template ({uri_template, name, handler, description?, mime_type?}) |
mcp_prompt(config) | config: dict | nil | Register a prompt ({name, handler, description?, arguments?}) |
Tool annotations (MCP spec annotations field) can be passed in the
tool_define config to describe tool behavior:
tools = tool_define(tools, "search", "Search files", {
params: { query: "string" },
handler: { args -> "results for " + args.query },
annotations: {
title: "File Search",
readOnlyHint: true,
destructiveHint: false
}
})
Example (agent.harn):
pipeline main(task) {
var tools = tool_registry()
tools = tool_define(tools, "greet", "Greet someone", {
params: { name: "string" },
handler: { args -> "Hello, " + args.name + "!" }
})
mcp_tools(tools)
mcp_resource({
uri: "docs://readme",
name: "README",
text: "# My Agent\nA demo MCP server."
})
mcp_resource_template({
uri_template: "config://{key}",
name: "Config Values",
handler: { args -> "value for " + args.key }
})
mcp_prompt({
name: "review",
description: "Code review prompt",
arguments: [{ name: "code", required: true }],
handler: { args -> "Please review:\n" + args.code }
})
}
Run as an MCP server:
harn mcp-serve agent.harn
Configure in Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"my-agent": {
"command": "harn",
"args": ["mcp-serve", "agent.harn"]
}
}
}
Notes:
mcp_tools(registry)(or the aliasmcp_serve) must be called to register tools.- Resources, resource templates, and prompts are registered individually.
- All
print/printlnoutput goes to stderr (stdout is the MCP transport). - The server supports the
2024-11-05MCP protocol version over stdio. - Tool handlers receive arguments as a dict and should return a string result.
- Prompt handlers receive arguments as a dict and return a string (single
user message) or a list of
{role, content}dicts. - Resource template handlers receive URI template variables as a dict and return the resource text.