Skip to content

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:

uv pip install maturin
cd rust && maturin develop --release && cd ..
uv pip install -e python/

Step 2: Create a project (1 min)

trails new blog
cd blog

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)

trails server

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:

trails server --transport sse --port 9000

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:

pytest 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:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list"
}

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), and trails onto export for Turtle output.
  • ORM guide@node_type, Model.where(), typed CRUD with ctx.kg.add.
  • Knowledge Graph guide — label-first nodes, edges, ctx.kg.match, raw SPARQL.
  • Agents guideLLMClient, ReAct planner, tool-use loops.
  • Policy guide — Cedar-based authorization on capabilities.
  • Testing guideisolated_kernel, mock_llm, capture_events, pytest fixtures.
  • Middleware guide@before, @after, @on_error, @around for cross-cutting concerns.
  • MCP guide — resources, prompts, and transport details.
  • Growing your KG app — tutorial taking a project from label-first to typed ORM.