Tools, Skills & MCP
Giving a model capabilities — calling your functions, loading Markdown skills, talking to MCP servers — is something every harness re-implements. Sema makes all three first-class, and it does so without new grammar: a function is a tool, and skills and MCP tools are data loaded by a handful of verbs. Crucially, because a tool is a governed Sema function, its effect row still applies when the agent calls it — tool calling inherits the language’s governance rather than being an ungoverned side channel.
This guide covers native tool calling, Markdown skills, and MCP (one-shot and persistent). It pairs with Building an Agent Loop, where these tools drive a bounded loop.
A function is a tool
Section titled “A function is a tool”Pass functions to tools.run. The runtime introspects each one — its name,
typed parameters, and a leading sem "…" as the description — into a
schema, drives the agentic loop, executes the real functions, and returns the
answer plus a trace:
import tools
def get_weather(city: str) -> str !{net.connect}: sem "Get the current weather for a city" return fetch_weather(city)
result = tools.run("what's the weather in Berlin?", [get_weather, add], max_steps=6)# result.answer, result.steps, result.status, result.traceNo separate schema DSL: the function already declares its name, params, and
effects, so the runtime introspects it. The sem "…" line is doing double duty —
it is the tool’s description the model sees.
Governance and guardrails
Section titled “Governance and guardrails”Because get_weather carries !{net.connect}, the agent can only call it where
that capability is granted — the tool cannot reach the network unless the policy
envelope allows it. On top of that the loop is guarded (drawn from the prior-art
gap list):
- Bounded
max_steps— every real agent caps iterations. - Unknown-tool and tool-error recovery — logged and recovered, never a crash.
- Same-tool-same-args loop detection — stops the agent spinning.
- Tool-result truncation with a marker — a huge result can’t blow the context.
Markdown skills
Section titled “Markdown skills”Skills load from Markdown with YAML frontmatter (name, description, body =
instructions) — exactly the format today’s tools ship, so existing skill
libraries work unchanged. Import skills and load a folder or a file:
import skills
docs_skills = skills.dir("skills") # a folder of .md skillsone = skills.load("skills/summarize.md")skills.context([...]) merges several skills into one instruction block;
skills.register(model, [...]) attaches that to a model value’s context so its
invocations carry the skills.
MCP: one-shot and persistent
Section titled “MCP: one-shot and persistent”import mcp gives you a real stdio JSON-RPC client. The one-shot form spawns a
server per call — fine for a single lookup:
import mcp
tools = mcp.tools("npx @modelcontextprotocol/server-filesystem /data")out = mcp.call("npx ...server-weather", "forecast", {"city": "Berlin"})mcp.tools(cmd) runs the initialize → tools/list handshake and returns the
server’s tools; mcp.call(cmd, tool, args) invokes one.
Persistent sessions
Section titled “Persistent sessions”Spawning per call is wasteful in a loop. mcp.connect opens a persistent
session and returns a handle; subsequent calls reuse the one live process, and
mcp.close ends it (any still-open sessions are killed at program exit):
import mcps = mcp.connect("npx @modelcontextprotocol/server-filesystem /data")mcp.tools(s) # list oncemcp.call(s, "read_file", {"path": "a.txt"})mcp.call(s, "read_file", {"path": "b.txt"}) # same process, no re-spawnmcp.close(s)The handle is an opaque integer index into the runtime’s session registry — the
live child and its stdio live in the runtime, not in a Sema value. Passing a
string command to mcp.call still works as the one-shot form.
One uniform surface
Section titled “One uniform surface”Skills and MCP tools register through the same path. mcp.as_skills(cmd)
exposes an MCP server’s tools as skill dicts, so Markdown skills and MCP tools
merge with + and attach in one call — the model doesn’t care where a capability
came from:
agent = skills.register(model, docs_skills + mcp.as_skills("npx ...server-weather"))And because tools.run (§“A function is a tool”) also folds MCP tools and skills
into its loop, native Sema functions, Markdown skills, and MCP tools all reach the
model through one governed mechanism.
Run and verify
Section titled “Run and verify”Tools, skills, and MCP are stdlib modules, so any project using them checks the same way:
sema check <your-project>SEMA_STRICT=1 sema run <your-project>sema check verifies the imports and effect rows; SEMA_STRICT=1 turns an
unknown-tool recovery or a truncation into a hard error while you debug.
Variations
Section titled “Variations”- Give an agent loop its tools. Feed a
toolsettotools.runinside aloop until— see Building an Agent Loop. - Reuse an MCP server across a run.
mcp.connectonce at startup,mcp.closeat shutdown; call as many times as you like in between. - Ship reflected docs as skills.
sema doc --skillsyour library, thenskills.dirthe output — your API becomes model context.