Skip to content

Web of Things Integration

Trails aligns its agent self-description with the W3C Web of Things Thing Description 1.1 spec. Every Trails host can publish a machine-readable TD that enumerates its capabilities as td:ActionAffordance entries, and every @capability maps cleanly to a WoT action. The design lives in ADR-0015 (AgentCard alignment), ADR-0015a (MCP-to-WoT projection), and ADR-0016 (discovery endpoint).

WoT integration is additive -- it does not replace the MCP tool projection or the canonical CapabilityDescriptor. Both MCP (tools/list) and WoT (td:actions) derive from the same handler metadata; neither is authoritative over the descriptor.

AgentCard

AgentCard is a WoT-aligned serializer declared rdfs:subClassOf wot:Thing in the Trails ontology. It aggregates all registered capabilities as td:ActionAffordance entries with trails-specific extensions under the trails: namespace.

Building from the registry

from trails.wot import AgentCard

card = AgentCard.from_registry(
    base_iri="https://example.org/agents/my-agent",
    title="My Agent",
    description="Evidence analysis agent",
)
print(card)
# <AgentCard id='https://example.org/agents/my-agent' actions=3>

from_registry() walks the live trails.decorators._handlers dict and converts each handler's _trails_meta into an action affordance. No explicit tool list is needed -- all registered @capability handlers are included.

With MCP endpoint

When an MCP endpoint is configured, each action gains a forms entry pointing to the MCP transport (ADR-0015a projection rule):

card = AgentCard.from_registry(
    base_iri="https://example.org/agents/my-agent",
    mcp_endpoint="mcp://localhost:8080",
)
td = card.to_td()
# Each action has forms: [{"href": "mcp://localhost:8080/tools/call", ...}]

Direct construction

For testing or offline use, construct an AgentCard directly:

card = AgentCard(
    agent_id="did:web:example.org:agents:test",
    title="Test Agent",
    description="Unit test agent",
    actions={
        "greet": {
            "description": "Say hello",
            "idempotent": True,
            "input": {"type": "object"},
        },
    },
    security_scheme="biscuit_sc",
)

Thing Description serialization

AgentCard provides two serialization methods:

to_td() -- W3C TD 1.1 JSON-LD

Returns a dict conforming to the TD 1.1 structure:

td = card.to_td()
{
  "@context": [
    "https://www.w3.org/2022/wot/td/v1.1",
    {
      "trails": "https://trails.dev/ont/core#",
      "prov": "http://www.w3.org/ns/prov#",
      "sh": "http://www.w3.org/ns/shacl#",
      "dct": "http://purl.org/dc/terms/",
      "schema": "https://schema.org/"
    }
  ],
  "@type": ["trails:AgentCard", "Thing"],
  "id": "https://example.org/agents/my-agent",
  "title": "My Agent",
  "securityDefinitions": {
    "biscuit_sc": {"scheme": "bearer", "format": "biscuit"}
  },
  "security": "biscuit_sc",
  "actions": {
    "greet": {
      "description": "Say hello",
      "idempotent": true
    }
  }
}

to_jsonld() -- full JSON-LD with extensions

A superset of to_td() that includes any extra metadata stored in card.extra:

card.extra["trails:endpoint"] = "https://example.org/mcp"
doc = card.to_jsonld()
# doc["trails:endpoint"] == "https://example.org/mcp"

Capability-to-action mapping

capability_to_action() converts a _trails_meta dict into a td:ActionAffordance. The mapping table:

Trails field WoT / standard field Notes
id action key Used as dict key, not a TD field
description td:description Direct mapping
input_shape td:input (sh:node) SHACL IRI preserved
output_shape td:output (sh:node) Same
idempotent td:idempotent Boolean, TD 1.1 native
side_effects td:safe + trails:sideEffect Non-empty = safe: false
version schema:softwareVersion Semantic version string
preconditions trails:precondition No WoT analogue
cost_estimate trails:costEstimate Trails extension
policy_required trails:policyRequired Trails extension
reasoning trails:reasoningMode none / rdfs / owl-rl
assurance trails:assurance L1 / L2 / L3 (ADR-0013)
deprecates prov:wasRevisionOf Provenance link

MCP-to-WoT form projection

mcp_tool_to_form() converts an MCP tool schema into a td:Form entry, per ADR-0015a:

from trails.wot import mcp_tool_to_form

form = mcp_tool_to_form(
    {"name": "greet"},
    mcp_endpoint="mcp://localhost:8080",
)
# {
#   "href": "mcp://localhost:8080/tools/call",
#   "contentType": "application/json",
#   "op": "invokeaction",
#   "htv:methodName": "POST",
#   "mcp:toolName": "greet",
# }

The form describes how to invoke the action via the MCP transport. When AgentCard.from_registry(mcp_endpoint=...) is used, forms are attached to each action automatically.

Discovery endpoint

The WoT discovery module (trails.wot_discovery) exposes two HTTP routes conforming to W3C WoT Discovery:

Method Path Content-Type Description
GET /.well-known/wot application/td+json Host TD -- self-describing directory
GET /agents/{agent_id}/td application/td+json Individual agent TD

Mounting the router

from fastapi import FastAPI
from trails.wot_discovery import create_wot_router

app = FastAPI()
app.include_router(create_wot_router())

Host TD

GET /.well-known/wot returns a self-describing Thing Description whose links[] array enumerates all registered capabilities. Each link points to the individual agent TD endpoint:

from trails.wot_discovery import build_host_td

td = build_host_td()
# td["links"] == [
#   {"rel": "item", "href": "trails://myapp/agents/greet", "type": "application/td+json"},
#   {"rel": "item", "href": "trails://myapp/agents/search", "type": "application/td+json"},
# ]

The base IRI is resolved from trails.toml ([project].base_iri or trails://<project.name>/), falling back to trails://local.

Individual agent TD

GET /agents/{agent_id}/td returns a full TD for a specific capability, including its invoke action with input schema derived from the handler's parameters:

from trails.wot_discovery import build_agent_td

td = build_agent_td("greet")
# td["actions"]["invoke"]["input"]["properties"] == {"name": {"type": "string"}}

Returns None (HTTP 404) if the agent is not registered.

Security definitions

Security definitions are auto-detected from the environment:

  • If TRAILS_HTTP_TOKEN is set or [http].auth_token is configured in trails.toml, the TD uses bearer_sc (HTTP bearer).
  • Otherwise, nosec_sc (no security) is used.

CLI commands

# Print the host TD (same as GET /.well-known/wot)
trails wot describe

# Print an individual agent TD
trails wot describe --agent greet

# Override the base IRI
trails wot describe --base-iri https://myapp.example

# Compact JSON (no indentation, pipe-friendly)
trails wot describe --compact

# Pipe to jq for filtering
trails wot describe | jq .links

# List all registered agents
trails wot list

Both commands auto-discover capabilities from the current project directory before generating output.

Federation: peer TD exchange

WoT TDs integrate with federation (see Federation guide). When federation is enabled, AgentCard instances can be exchanged between peers during schema advertisement. The TD serves as a machine-readable contract: a remote instance can parse the TD to discover available actions, their input schemas, and security requirements before invoking capabilities via MCP relay.

from trails.wot import AgentCard

# Build a card with MCP endpoint for federation
card = AgentCard.from_registry(
    base_iri="https://my-instance.example/agents/main",
    mcp_endpoint="https://my-instance.example/mcp",
)

# Serialize for peer exchange
import json
td_json = json.dumps(card.to_td())

The Session object in the agent runtime accepts a card= parameter for attaching a WoT card to the agent's session context.

Example: full working setup

from fastapi import FastAPI
from trails import capability
from trails.wot import AgentCard
from trails.wot_discovery import create_wot_router

# Register capabilities
@capability(id="evidence.search", description="Search evidence by topic.")
def search(ctx, topic: str) -> dict:
    return {"hits": ctx.kg.query(f"SELECT ?s WHERE {{ ?s a :Evidence ; :topic '{topic}' }}")}

@capability(id="evidence.summarize", description="Summarize evidence.", idempotent=True)
def summarize(ctx, evidence_id: str) -> dict:
    return {"summary": "..."}

# Build the agent card
card = AgentCard.from_registry(
    base_iri="https://evidence.example/agents/main",
    title="Evidence Agent",
    description="Search and summarize clinical evidence",
    mcp_endpoint="https://evidence.example/mcp",
)

# Print the TD
import json
print(json.dumps(card.to_td(), indent=2))

# Mount the discovery endpoint
app = FastAPI()
app.include_router(create_wot_router())

# Now:
#   GET /.well-known/wot           -> host TD with links to both agents
#   GET /agents/evidence.search/td -> individual TD for evidence.search

Reference

Symbol Description
AgentCard(agent_id, title, description, actions, security_scheme, security_definitions, base_iri, extra) WoT-aligned agent self-description
AgentCard.from_registry(base_iri, *, title, description, agent_id, mcp_endpoint) Build from the live capability registry
AgentCard.to_td() Serialize to W3C TD 1.1 JSON-LD
AgentCard.to_jsonld() Serialize with all trails: extensions
AgentCard.action_names() Sorted list of action names
AgentCard.get_action(name) Look up an action by name
capability_to_action(cap_descriptor) Convert a capability descriptor to td:ActionAffordance
mcp_tool_to_form(tool, *, mcp_endpoint, content_type) Convert an MCP tool schema to a td:Form
build_host_td(capabilities, base_iri) Build the host TD for /.well-known/wot
build_agent_td(agent_id, base_iri) Build a TD for a specific agent
get_registered_agents() List all registered capability IDs
create_wot_router() Create a FastAPI router with WoT discovery routes

See also