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:
{
"@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_TOKENis set or[http].auth_tokenis configured intrails.toml, the TD usesbearer_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¶
- ADR-0015 -- WoT AgentCard alignment
- ADR-0015a -- MCP-to-WoT projection rule
- ADR-0016 -- WoT Discovery endpoint
- MCP Integration -- the MCP transport layer
- Federation -- peer-to-peer TD exchange
- Capabilities -- the
@capabilitydecorator that feeds WoT actions