Quickstart: Your First Trails App¶
Build a working knowledge-graph app with MCP transport in under 10 minutes.
Prerequisites¶
- Python 3.11+
- Rust 1.85+ (for the native kernel extension)
- pip (or uv)
Step 1: Install Trails (2 min)¶
From source (dev install):
git clone https://github.com/XORwell/trails.git
cd trails
# Build the Rust kernel + install the Python package
pip install maturin
cd rust && maturin develop --release && cd ..
pip install -e python/
If you use uv:
Step 2: Create a project (1 min)¶
This generates a minimal scaffold:
blog/
app.py — one @capability handler
trails.toml — project config
tests/ — pytest smoke test
README.md
Four templates are available (--template minimal|agent|kg|full).
The default minimal gives you the smallest possible starting point.
Step 3: Your first capability (2 min)¶
Open app.py. The scaffolder already wrote one for you:
from trails import capability
@capability
def hello(ctx, name: str) -> dict:
return {"message": f"Hello, {name}!"}
Three decorator forms work — pick whichever reads best:
# Bare — id is inferred from the function name
@capability
def hello(ctx, name: str) -> dict: ...
# Positional id
@capability("greet")
def hello(ctx, name: str) -> dict: ...
# Keyword id + description
@capability(id="greet", description="Say hi")
def hello(ctx, name: str) -> dict: ...
The leading ctx parameter is special: Trails injects a
Context with access to the knowledge graph (ctx.kg), the
current trace id, and the calling principal. Every other parameter
becomes a tool argument.
Capabilities integrate directly with models: annotate a parameter
with an @app.model class and Trails validates the incoming data and
auto-derives the input schema — no extra wiring needed (see Step 4).
Step 4: Add a domain model (2 min)¶
Define your data model directly in Python using @app.model and typed
field annotations. Trails compiles these to JSON-Schema validation, SHACL
constraints, and an ORM layer — no Turtle, no SHACL vocabulary required.
Add this to app.py (or a new models.py):
from typing import Annotated
from trails import App
from trails.gac import required, optional, min_length, max_value, one_of, pattern
app = App("blog")
@app.model
class Post:
title: Annotated[str, required(), min_length(1)]
body: Annotated[str, required()]
status: Annotated[str, one_of("draft", "published", "archived")]
views: Annotated[int, optional(), max_value(10_000_000)]
Trails auto-generates: - A node type with type-checked fields and an ORM - A SHACL shape with all the annotated constraints - MCP tool schema from the type annotations
Constraint reference:
| Annotation | SHACL | Meaning |
|---|---|---|
required() |
sh:minCount 1 |
Field must have a value |
optional() |
sh:minCount 0 |
Field may be absent |
min_length(n) |
sh:minLength |
String ≥ n chars |
max_length(n) |
sh:maxLength |
String ≤ n chars |
min_value(v) |
sh:minInclusive |
Number ≥ v |
max_value(v) |
sh:maxInclusive |
Number ≤ v |
pattern(r) |
sh:pattern |
Must match regex |
one_of(*vs) |
sh:in |
One of listed values |
For cross-property rules, use @constraint:
from trails.gac import constraint, require
@constraint(Post)
def published_requires_title(post):
if post.status == "published":
require(post.title, "published posts must have a title")
For advanced SHACL (sh:or, sh:xone, SPARQL-based rules), the raw
@shape + predicate() API is still available — see the
Shapes guide and GaC guide.
Step 5: Run the server (1 min)¶
Autoload discovers every @capability, @shape, and @node_type in
app.py or app/ before starting. Transport is auto-detected:
- stdin is piped (MCP client talking to you) -> stdio
- stdin is a TTY (you're in a terminal) -> HTTP on port 8080
Override with --transport {stdio,sse,http}, --host, or --port:
The back-compat alias trails serve still works.
Add --watch during development for hot-reload: the server clears
registries and re-runs autoload on every .py change under app/.
Step 6: Test it (1 min)¶
Open tests/test_app.py (the scaffolder wrote one):
import trails
import app # noqa: F401 — registers capabilities
def test_hello():
envelope = trails.invoke("hello", {"name": "World"})
assert envelope["capability"] == "hello"
assert envelope["payload"]["message"] == "Hello, World!"
For tests that need a Context (to call ctx.kg methods directly),
use fresh_context():
from trails.testing import fresh_context
def test_kg_round_trip():
ctx = fresh_context()
iri = ctx.kg.node(labels=["Note"], properties={"title": "test"})
assert iri.startswith("trails://")
fresh_context() returns a Context bound to the kernel store with
a fresh trace id. Optional kwargs: principal= (default
"did:local:test") and trace_id=.
For full registry isolation between tests, use the isolated_kernel()
context manager or the trails_isolated pytest fixture:
from trails.testing import isolated_kernel
def test_isolated():
with isolated_kernel():
@capability("ephemeral")
def handler(ctx) -> dict:
return {}
# handler is only visible inside this block
Run tests:
Step 7: Call it from an MCP client (1 min)¶
Point any MCP-compatible client (Claude Desktop, custom agent) at your
server. For stdio, configure the client to launch trails server.
For SSE/HTTP, point at http://localhost:8080.
List available tools:
Call a tool:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "hello",
"arguments": {"name": "World"}
}
}
Response:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "{\"message\": \"Hello, World!\"}"
}
]
}
}
What's next¶
- GaC guide —
@app.model, constraint markers,@constraint, and migrating from@shape+predicate(). - Shapes guide — SHACL property shapes,
value constraints (
one_of,pattern, numeric bounds), andtrails onto exportfor Turtle output. - ORM guide —
@node_type,Model.where(), typed CRUD withctx.kg.add. - Knowledge Graph guide — label-first nodes,
edges,
ctx.kg.match, raw SPARQL. - Agents guide —
LLMClient, ReAct planner, tool-use loops. - Policy guide — Cedar-based authorization on capabilities.
- Testing guide —
isolated_kernel,mock_llm,capture_events, pytest fixtures. - Middleware guide —
@before,@after,@on_error,@aroundfor cross-cutting concerns. - MCP guide — resources, prompts, and transport details.
- Growing your KG app — tutorial taking a project from label-first to typed ORM.