# Model Context Protocol (MCP)
Deft can expose any of its functions as tools callable by LLM
clients — Claude Desktop, VS Code Copilot, Goose, and any other
client that speaks the
[Model Context Protocol](https://modelcontextprotocol.io/).
The integration is the `mcp/*` namespace: build a tool server, add
Deft functions to it, and serve over stdio or HTTP. The protocol is
JSON-RPC 2.0; you don't have to think about that — `mcp/*` handles
it.
## `mcp/*` Stdlib functions
| Function | Returns | Purpose |
|---|---|---|
| `[mcp/create :name]` | `:ok` | Create an empty tool server |
| `[mcp/add :name $fn ?description]` | `:ok` | Add a Deft function as a tool |
| `[mcp/serve :name]` | blocks | Start stdio server |
| `[mcp/http-serve :name $port]` | blocks | Start HTTP server |
| `[mcp/dispatch :name $json-string]` | json-string | One-shot dispatch (testing) |
## Minimal Example
def hello {|name^string| @doc "Greet someone by name." [str "Hello, $name!"] } def add {|a^number b^number| @doc "Add two numbers." ($a + $b) } mcp/create :my-tools mcp/add :my-tools $hello mcp/add :my-tools $add mcp/serve :my-tools # blocks, serving over stdio
Save as `my-tools.dft`. When an MCP client invokes `hello` with
`{:name "Alice"}`, the function is called and the result returned to
the LLM as JSON.
## How Tools Are Described
`mcp/add` reads:
- The function's **name** (the `def` name).
- The **description** from `@doc` if you don't pass one explicitly.
- The **parameter list** from the function signature — names and
type hints become the JSON schema the LLM sees.
So a well-typed function with a clear `@doc` becomes a well-described
MCP tool with no extra boilerplate:
def search-codebase {|query^string max-results^number| @doc "Search the codebase for a text query. Returns up to max-results matches." # ... implementation ... @{} } mcp/add :my-tools $search-codebase
The LLM sees a tool named `search-codebase` with parameters
`query:string` and `max-results:number` and your description as the
prompt hint.
## Stdio Transport
`mcp/serve` runs the server over stdin/stdout, blocking forever.
This is what Claude Desktop and most editor integrations expect:
mcp/serve :my-tools
### Claude Desktop config
In `claude_desktop_config.json`:
```json
{
"mcpServers": {
"my-tools": {
"command": "/path/to/deft",
"args": ["/abs/path/to/my-tools.dft"]
}
}
}
```
### VS Code config
In `.vscode/mcp.json`:
```json
{
"servers": {
"my-tools": {
"type": "stdio",
"command": "/path/to/deft",
"args": ["my-tools.dft"]
}
}
}
```
## HTTP Transport
`mcp/http-serve` runs an HTTP listener on the given port. Useful
when you want multiple concurrent clients or a long-running service:
mcp/http-serve :my-tools 3001
In `.vscode/mcp.json`:
```json
{
"servers": {
"my-tools": {
"type": "http",
"url": "http://localhost:3001/mcp"
}
}
}
```
### Stdio vs HTTP — when to use which
| Aspect | Stdio | HTTP |
|---|---|---|
| Clients per process | One | Many |
| Lifecycle | Spawned by client | Long-running service |
| Setup complexity | Simplest | Slightly more |
| Latency | Fresh process each start | Warm process |
For local editor integration, **stdio is the default** — it's the
lowest-friction option and matches what every MCP client supports.
Reach for HTTP when you want a shared service across multiple
clients or a long-lived background process.
## Tool Namespacing
For tools that share a prefix, name functions with `::` to get dotted
namespace separators in the tool name:
def file-ops::read {|path^string| ... } def file-ops::write {|path^string content^string| ... } def file-ops::list {|dir^string| ... } mcp/add :fs-tools $file-ops::read mcp/add :fs-tools $file-ops::write mcp/add :fs-tools $file-ops::list
The LLM sees tools named `file-ops.read`, `file-ops.write`,
`file-ops.list`.
## Best Practices
1. **Document every tool.** The LLM only knows what the description
tells it. A function without `@doc` shows up with no hint about
its purpose.
2. **Type your parameters.** Type hints become the JSON schema. An
untyped parameter shows up as `any`, which the LLM may fill with
garbage.
3. **Prefer small, composable tools.** Each tool should do one
thing. An LLM can chain small tools more reliably than reason
about a single monolithic one.
4. **Handle errors gracefully.** Return structured `%Err` maps
rather than throwing — thrown errors abort the call and confuse
the LLM.
5. **Return JSON-friendly data.** Maps, lists, and scalars
serialise cleanly. Tagged maps, sets, and closures don't.
## A Realistic Tool
def project-stats {|project-path^string| @doc { :description "Compute statistics for a Deft project: file count, total lines, dependencies." :params %{ :project-path "Absolute path to the project root" | } :returns "Map with :files :lines :deps keys" } set files [fs/glob "$project-path/**/*.dft"] set total 0 for f in $files { set text [fs/read $f] set lines $text(lines) set total ($total + $lines) | } set deps @{} for f in $files { set text [fs/read $f] for line [split $text "\n"] { if [starts-with? [trim $line] "import "] { set parts [split $line "\""] if ($parts(len) >= 2) { $deps << $parts(nth 1) } | | } | %{ :files $files(len) :lines $total :deps $deps(unique) | } } mcp/create :dev-tools mcp/add :dev-tools $project-stats mcp/serve :dev-tools
## Dispatch For Testing
`mcp/dispatch` is a one-shot entry point useful for tests and
scripting. Pass a JSON-RPC request string, get back a JSON-RPC
response string — without spinning up a server:
set req [json/stringify %{ :jsonrpc "2.0" :id 1 :method "tools/call" :params %{ :name "hello" :arguments %{ :name "Alice" } } }] set resp [mcp/dispatch :my-tools $req] echo $resp
Useful for unit-testing your tool surface in a `defsuite` without
needing a live LLM client.
## What's Already Shipped
The TUI's built-in chat app exposes a rich MCP tool server itself —
see `packages/stdext/src/mcp_tools.dft` for examples including:
- `help.lookup` — fetch docs by name
- `list-stdext` — enumerate the stdext package (legacy alias: `list-stdlib`)
- `grep-codebase` / `search-codebase` — search source files
- `git-log` / `git-diff` — git operations
- `find-files` — locate files by pattern
- `find-references` — symbol cross-reference
- `run-tests` — invoke the test runner
- `eval-dft` — evaluate a Deft snippet in a sandbox
- `web-fetch` — fetch a URL
- `write-patch` — apply code changes
These are good references for what a useful MCP tool looks like.
## Quick Reference
| Form | Purpose |
|---|---|
| `[mcp/create :name]` | Create a tool server |
| `[mcp/add :name $fn ?desc]` | Register a function |
| `[mcp/serve :name]` | Stdio server (blocks) |
| `[mcp/http-serve :name $port]` | HTTP server (blocks) |
| `[mcp/dispatch :name $json]` | One-shot dispatch |