<!-- Sema documentation — Tools, Skills & MCP
     Sema is a new AI-native, neurosymbolic programming language most models
     have not been trained on. Full corpus: https://sema.49.12.246.95.sslip.io/llms-full.txt
     Install language support: https://sema.49.12.246.95.sslip.io/install-skill/ -->

# Tools, Skills & MCP

> Give a model capabilities in Sema — a function is a tool, Markdown skills and MCP servers load through one uniform surface, all governed by effect rows.

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](/guides/agent-loops/), where
these tools drive a bounded loop.

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

```sema
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.trace
```

No 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

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.

:::note
The loop uses a model-agnostic text wire protocol
(`<tool_call>{"name","arguments"}</tool_call>` → execute →
`<tool_result>…</tool_result>` → repeat until a tool-free answer), so it works on
*any* model. Provider-native formats (OpenAI `tools`, Anthropic `tool_use`, local
GGUF chat templates) are adapters behind the same surface.
:::

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

```sema
import skills

docs_skills = skills.dir("skills")               # a folder of .md skills
one         = 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.

:::tip
`sema doc --skills` emits your own modules as skills (see
[Documents & Reports](/guides/documents/#documentation-as-a-reflected-artifact)),
so a program can load *its own reflected docs* as model context — the interfaces
and intent without the source bloat.
:::

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

```sema
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

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

```sema
import mcp
s = mcp.connect("npx @modelcontextprotocol/server-filesystem /data")
mcp.tools(s)                          # list once
mcp.call(s, "read_file", {"path": "a.txt"})
mcp.call(s, "read_file", {"path": "b.txt"})   # same process, no re-spawn
mcp.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

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:

```sema
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

Tools, skills, and MCP are stdlib modules, so any project using them checks the
same way:

```bash
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.

:::caution[Effect rows are the real permission boundary]
A tool's power is exactly its effect row. A tool that reads files must declare
`!{fs.read}`; the agent then can only invoke it where the policy grants `fs.read`.
Keep tool effect rows tight — that is the difference between a helpful tool and an
open side channel.
:::

## Variations

- **Give an agent loop its tools.** Feed a `toolset` to `tools.run` inside a
  `loop until` — see [Building an Agent Loop](/guides/agent-loops/).
- **Reuse an MCP server across a run.** `mcp.connect` once at startup, `mcp.close`
  at shutdown; call as many times as you like in between.
- **Ship reflected docs as skills.** `sema doc --skills` your library, then
  `skills.dir` the output — your API becomes model context.

## See also

- [Building an Agent Loop](/guides/agent-loops/) · [Documents & Reports](/guides/documents/)
- [Effects & Capabilities](/governance/effects/) · [Policies](/governance/policy/)
- [Protocols & Sessions](/governance/protocols/)
