# mcp

> MCP 2025-06-18 Streamable HTTP bridge for the iii engine.

| field | value |
|-------|-------|
| version | 0.5.7 |
| type | binary |
| repo | https://github.com/iii-hq/workers |
| supported_targets | x86_64-apple-darwin, aarch64-apple-darwin, i686-pc-windows-msvc, x86_64-pc-windows-msvc, aarch64-pc-windows-msvc, x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-unknown-linux-musl, armv7-unknown-linux-gnueabihf |
| author | iii |

## installation

```sh
iii worker add mcp@0.5.7
```

## configuration

```yaml
- api_path: /mcp
  hidden_prefixes:
    - engine::
    - state::
    - stream::
    - iii.
    - iii::
    - mcp::
    - a2a::
    - skills::
    - prompts::
  require_expose: false
```

## dependencies

- `skills` @ `^0.2.1`

## readme

# mcp

Model Context Protocol (MCP) bridge for the [iii engine](https://iii.dev),
shipped as a single self-contained Rust binary worker. Registers an HTTP
endpoint (default `POST /mcp`) that speaks MCP 2025-06-18 JSON-RPC and maps
each method onto an `iii.trigger` call. [`skills`](https://workers.iii.dev/workers/skills) is listed as a
worker dependency in [`iii.worker.yaml`](iii.worker.yaml), so MCP clients see
registered skills as `iii://{id}` resources and slash-commands as MCP
prompts when `mcp` is installed.

## Install

```bash
iii worker add mcp
```

## Quickstart

Sanity-check the bridge with `curl` against MCP Inspector or any MCP client.
Substitute your engine's HTTP port (default `3111`).

Responses use the **MCP Streamable HTTP** transport: `POST /mcp` returns
`text/event-stream` with a single `event: message` SSE event whose `data:`
line is the JSON-RPC reply. Pure JSON-RPC notifications (no `id`) get HTTP
`202 Accepted` with no body.

```bash
curl -sNX POST http://127.0.0.1:3111/mcp \
  -H 'content-type: application/json' \
  -H 'accept: text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
  | sed -n 's/^data: //p' | jq '.result.protocolVersion'

curl -sNX POST http://127.0.0.1:3111/mcp \
  -H 'content-type: application/json' \
  -H 'accept: text/event-stream' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  | sed -n 's/^data: //p' | jq '.result.tools[].name'
```

`-N` disables curl's output buffering so the SSE frame appears as soon as the
bridge writes it.

## Configuration

```yaml
hidden_prefixes:
  - "engine::"
  - "state::"
  - "stream::"
  - "iii."
  - "iii::"
  - "mcp::"
  - "a2a::"
  - "skills::"
  - "prompts::"
require_expose: false
api_path: "/mcp"
```

`require_expose: true` advertises only functions whose `metadata.mcp.expose == true`.
`api_path` lets you bind a different path (e.g. `/mcp-custom`) for the bridge.

### Hidden namespaces

Function ids starting with any of the prefixes listed in `hidden_prefixes`
are excluded from `tools/list` and rejected at `tools/call`. The defaults
mirror the hard floor enforced by the `skills` worker: `engine::`, `state::`,
`stream::`, `iii.`, `iii::`, `mcp::`, `a2a::`, `skills::`, `prompts::`. The
bridge's own handler id (`mcp::handler`) is already covered by the `mcp::`
prefix, so it is auto-hidden.

### Tool naming

iii function ids contain `::`, which MCP tool names disallow. The bridge
transparently maps `myworker::echo` ↔ `myworker__echo` on the way out and
back. Pass tool names with `__` in `tools/call`; the bridge translates them
back to the iii function id.

If the config file is missing or malformed the worker logs a warning and
falls back to the defaults; boot is never blocked by a bad config path.

## MCP method coverage

The v0.1 surface implements the core methods needed for tool/resource/prompt
discovery and invocation:

| Method | Behaviour |
|---|---|
| `initialize` | Returns `{ protocolVersion: "2025-06-18", capabilities: { tools, resources, prompts }, serverInfo }` |
| `ping` | Returns `{}` |
| `notifications/initialized` | Acknowledged with no response |
| `tools/list` | Enumerates non-hidden iii functions |
| `tools/call` | Invokes the named iii function via `iii.trigger` |
| `resources/list` | Delegates to `skills::resources-list` |
| `resources/read` | Delegates to `skills::resources-read` |
| `resources/templates/list` | Delegates to `skills::resources-templates` |
| `prompts/list` | Delegates to `prompts::mcp-list` |
| `prompts/get` | Delegates to `prompts::mcp-get` |

Out of scope for this version: `resources/subscribe`, `resources/unsubscribe`,
`completion/complete`, `logging/setLevel`, MCP notification fan-out
(`tools/list_changed`, `resources/updated`, `notifications/message`,
`notifications/progress`), and the stdio transport.

## api reference

```json
{
  "functions": [
    {
      "description": "MCP 2025-06-18 Streamable HTTP bridge.",
      "metadata": {},
      "name": "mcp::handler",
      "request_schema": {
        "definitions": {
          "ChannelDirection": {
            "enum": [
              "read",
              "write"
            ],
            "type": "string"
          },
          "StreamChannelRef": {
            "properties": {
              "access_key": {
                "type": "string"
              },
              "channel_id": {
                "type": "string"
              },
              "direction": {
                "$ref": "#/definitions/ChannelDirection"
              }
            },
            "required": [
              "access_key",
              "channel_id",
              "direction"
            ],
            "type": "object"
          }
        },
        "description": "Subset of `iii_sdk::types::ApiRequest` plus the engine-issued response channel writer ref. We deserialize directly off `serde_json::Value` so we can capture the `response` field that the typed `ApiRequest<T>` strips.",
        "properties": {
          "body": {
            "default": null
          },
          "response": {
            "$ref": "#/definitions/StreamChannelRef"
          }
        },
        "required": [
          "response"
        ],
        "title": "McpRequest",
        "type": "object"
      },
      "response_schema": {
        "type": "null"
      }
    }
  ],
  "triggers": []
}
```
