Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@
"lhs": "${hostSystemName}",
"rhs": "Linux"
}
},
{
"name": "osx-arm64",
"displayName": "macOS arm64",
"description": "macOS arm64 development configure path using Ninja Multi-Config and the default vcpkg triplet.",
"generator": "Ninja Multi-Config",
"binaryDir": "${sourceDir}/build/osx-arm64",
"cacheVariables": {
"VCPKG_TARGET_TRIPLET": "arm64-osx"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
}
}
],
"buildPresets": [
Expand All @@ -56,6 +71,20 @@
"description": "Build the Windows x64 development tree with the Release configuration.",
"configurePreset": "windows-x64",
"configuration": "Release"
},
{
"name": "osx-debug",
"displayName": "macOS Debug",
"description": "Build the macOS arm64 development tree with the Debug configuration.",
"configurePreset": "osx-arm64",
"configuration": "Debug"
},
{
"name": "osx-release",
"displayName": "macOS Release",
"description": "Build the macOS arm64 development tree with the Release configuration.",
"configurePreset": "osx-arm64",
"configuration": "Release"
}
]
}
}
127 changes: 127 additions & 0 deletions docs/lua.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,130 @@ local result = mcp.call_tool("docs", "lookup", { query = "install" })
```

The MCP bridge is intentionally thin. Config loading, transports, protocol negotiation, tool listing, and tool calls stay in native C++.

## Authoring MCP Servers with Lua

Yaaf scripts can host MCP servers, allowing local tools and prompts to be consumed by any MCP client (Claude, VS Code, etc.). This is the reverse of the default MCP client mode: instead of yaaf consuming remote servers, MCP clients consume yaaf.

### Entry Point

Host an MCP server directly:

```bash
yaaf run ./examples/mcp_host_example.lua
```

The script blocks until the MCP client disconnects. JSON-RPC messages flow over stdin/stdout.

### Authoring Workflow

A typical MCP host script follows this pattern:

```lua
-- 1. Load required modules
local tool = require("tool")
local mcp = require("mcp")

-- 2. Register custom tools
tool.register({
spec = {
name = "calculate",
description = "Simple calculator",
parameters = {
type = "object",
properties = {
expression = {
type = "string",
description = "Math expression to evaluate"
}
},
required = { "expression" }
}
},
execute = function(args)
-- Tool execution logic
local result = load("return " .. args.expression)()
return {
tool_name = "calculate",
content = tostring(result),
success = true,
metadata = {}
}
end
})

-- 3. Register prompts (optional)
mcp.register_prompt({
name = "system_role",
description = "System role prompt for the assistant",
arguments = {
{ name = "style", description = "Response style: formal or casual" }
},
handler = function(args)
local style = args.style or "formal"
return {
messages = {
{
role = "user",
content = "You are a helpful assistant. Use a " .. style .. " tone."
}
}
}
end
})

-- 4. Start the server
mcp.host_stdio({
tools = { "calculate", "echo" },
prompts = { "system_role" }
})
```

### Available Tools

Hosted tools can come from three sources:

1. **Built-in tools:** The `echo` tool shipped with yaaf
2. **Custom tools:** Registered via `tool.register()` in the same script
3. **Remote MCP tools:** Tools from configured MCP servers, available via `mcp.servers()`

Use `tool.names()` to list all available tools:

```lua
local tool = require("tool")

local available = tool.names()
-- Example output: { "echo", "reverse", "server1.tool1", "server1.tool2" }
```

### Prompt Specification

Prompts are script-local and must be registered before calling `mcp.host_stdio()`. Each prompt:

- Has a unique `name` and optional `description`
- Optionally accepts templating arguments (e.g., tone, style, detail level)
- Returns a table with a `messages` array
- Each message has `role` (`"user"` or `"assistant"`) and `content` (string)

Prompts allow clients to request system instructions or conversation starters alongside tools.

### Selective Exposure

Use the `{tools?, prompts?}` parameters to expose a subset of registered items:

```lua
-- Expose only "reverse" and "echo" tools, hide remote MCP tools
mcp.host_stdio({
tools = { "reverse", "echo" },
prompts = { "system_role", "greeting" }
})
```

If omitted, all tools and prompts are exposed.

### Use Cases

- **Wrap local scripts:** Expose shell commands, local APIs, or file operations as MCP tools
- **Composite servers:** Use remote MCP tools and augment them with custom logic
- **Prompt libraries:** Provide system instructions and conversation starters
- **Local tool testing:** Develop and test tools in isolation before shipping
2 changes: 2 additions & 0 deletions docs/mcp.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# MCP Tools

This page covers **consuming remote MCP servers** (client mode). To **host local tools as an MCP server** (host mode), see [Authoring MCP Servers with Lua](lua.md#authoring-mcp-servers-with-lua).

Yaaf loads MCP tools from a config file. The path is resolved in this order:

1. `--mcp <path>` passed to `ask`, `chat`, `agent`, or `run` (or as a global option before the subcommand).
Expand Down
144 changes: 144 additions & 0 deletions docs/modules/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,147 @@ end
```

Use `mcp.diagnostics()` when you want a structured active connectivity check without invoking a tool. Use the higher-level [tool](tool.md) registry when you want MCP tools to appear beside local and script-registered tools.

## Hosting MCP Servers

Lua scripts can host MCP servers to expose tools and prompts to MCP clients (Claude, etc.) over stdio transport.

### `mcp.register_prompt(descriptor)`

Register a prompt for use when hosting an MCP server.

**Parameters:**

- `descriptor` (table): Prompt descriptor with the following structure:
- `name` (string): Unique prompt identifier
- `description` (string, optional): Human-readable description of the prompt
- `arguments` (table, optional): Array of argument descriptors `{ {name, description?, required?}, ... }`
- `handler` (function): Handler called when client requests the prompt. Signature: `function(arguments_table) -> {messages = {{role, content}, ...}}`

**Returns:** `true` on success

**Throws:** Lua error on invalid descriptor (missing name, missing handler, etc.)

**Message format:**

Each message table has:
- `role` (string): `"user"` or `"assistant"`
- `content` (string): Message text

**Example:**

```lua
local mcp = require("mcp")

mcp.register_prompt({
name = "system_role",
description = "System role for a helpful assistant",
arguments = {
{ name = "tone", description = "Assistant tone: formal or casual", required = false },
},
handler = function(args)
local tone = args.tone or "formal"
local instruction = "You are a helpful assistant. Keep a " .. tone .. " tone."
return {
messages = {
{ role = "user", content = instruction }
}
}
end
})
```

### `mcp.host_stdio(options)`

Start an MCP server listening on stdin/stdout.

**Parameters:**

- `options` (table, optional):
- `tools` (table, optional): Array of tool names to expose. If omitted, all available tools are exposed.
- `prompts` (table, optional): Array of prompt names to expose. If omitted, all registered prompts are exposed.

**Returns:** `boolean` (`true` on clean exit)

**Throws:** Lua error on fatal error (e.g., schema registry not available, JSON-RPC parse failure)

**Behavior:**

- Blocks until the client disconnects or stdin reaches EOF
- Handles all JSON-RPC protocol messages from the client
- Responds to `tools/list`, `tools/call`, `prompts/list`, and `prompts/get` requests
- Responds to `initialize` with supported protocol version

**Example:**

```lua
local tool = require("tool")
local mcp = require("mcp")

-- Register a custom tool
tool.register({
spec = {
name = "reverse",
description = "Reverses a string",
parameters = {
type = "object",
properties = {
text = { type = "string", description = "Text to reverse" }
},
required = { "text" }
}
},
execute = function(args)
local text = args.text or ""
local reversed = string.reverse(text)
return {
tool_name = "reverse",
content = reversed,
success = true,
metadata = { input_length = #text }
}
end
})

-- Register a prompt
mcp.register_prompt({
name = "greeting",
description = "Greeting prompt",
handler = function(args)
return {
messages = {
{ role = "user", content = "Hello! How can I help?" }
}
}
end
})

-- Start the server, exposing reverse, echo (built-in), and greeting
mcp.host_stdio({
tools = { "reverse", "echo" },
prompts = { "greeting" }
})
```

### Integration with yaaf's Tool Ecosystem

Hosted tools can be:

- **Built-in tools:** The `echo` tool that ships with yaaf
- **Custom tools:** Registered in the script via `tool.register()`
- **Remote MCP tools:** Tools from configured MCP servers, accessible via `mcp.servers()` and `mcp.list_tools()`

Use `tool.names()` and `tool.specs()` to discover available tools before calling `mcp.host_stdio()`:

```lua
local tool = require("tool")
local mcp = require("mcp")

local available = tool.names()
print("Available tools: " .. table.concat(available, ", "))

-- Expose a selected subset
mcp.host_stdio({
tools = { "echo", "reverse" }
})
```
12 changes: 11 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ Options:

### `run`

`run` executes a standalone Lua file through the native script runtime.
`run` executes a standalone Lua file through the native script runtime. Scripts can consume MCP servers (via `mcp` module APIs) or host them (via `mcp.host_stdio()`).

```powershell
yaaf run ./examples/example.lua one two three
Expand All @@ -272,6 +272,16 @@ Options:
| `<file.lua>` | Path to the standalone Lua script to execute. Required. |
| `[args...]` | Positional arguments exposed to the script as `yaaf.args`. |

**Hosting an MCP server:**

A script can call `mcp.host_stdio()` to start an MCP server that listens on stdin/stdout. This allows MCP clients (Claude, VS Code, etc.) to connect and use the script's registered tools and prompts.

```powershell
yaaf run ./examples/mcp_host_example.lua
```

See [Authoring MCP Servers with Lua](../lua.md#authoring-mcp-servers-with-lua) for details on implementing hosted tools and prompts.

## Common Workflows

Basic ask:
Expand Down
Loading
Loading