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
129 changes: 129 additions & 0 deletions a2a/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,135 @@ skills and cannot be invoked via `message/send`:
--debug Verbose logging
```

## Local testing

A2A has no standard client inspector like MCP does. Use curl. Full smoke
path from a clean machine, assuming `iii-sdk` v0.11.3 engine installed:

### 1. Start the engine

Minimal `config.yaml`:

```yaml
workers:
- name: iii-worker-manager
- name: iii-http
config:
host: 127.0.0.1
port: 3111
- name: iii-state
```

```bash
iii --no-update-check
```

### 2. Register a test function tagged `a2a.expose: true`

```rust
use iii_sdk::{register_worker, InitOptions, RegisterFunctionMessage};
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let iii = register_worker("ws://127.0.0.1:49134", InitOptions::default());

iii.register_function_with(
RegisterFunctionMessage {
id: "pricing::quote".into(),
description: Some("Quote a price".into()),
metadata: Some(json!({ "a2a.expose": true })),
..Default::default()
},
|_input: Value| async move { Ok(json!({"price": 42})) },
);

tokio::signal::ctrl_c().await?;
Ok(())
}
```

### 3. Start iii-a2a

```bash
cargo run --release -p iii-a2a
# or: ./target/release/iii-a2a --base-url http://127.0.0.1:3111
```

Registers `GET /.well-known/agent-card.json` and `POST /a2a`.

### 4. Smoke path

```bash
# Agent card — exposed skills only, hard floor filters engine::/state::/etc.
curl -s http://127.0.0.1:3111/.well-known/agent-card.json \
| jq '{name, skills: [.skills[] | {id, description}]}'

# message/send with data part (direct invocation)
curl -sX POST http://127.0.0.1:3111/a2a \
-H 'content-type: application/json' \
-d '{
"jsonrpc":"2.0","id":"t1","method":"message/send",
"params":{"message":{
"messageId":"m1","role":"user",
"parts":[{"data":{"function_id":"pricing::quote","payload":{}}}]
}}
}' | jq '.result.task | {state: .status.state, artifact: .artifacts[0].parts[0].text}'

# message/send with text shorthand ("function_id <json>")
curl -sX POST http://127.0.0.1:3111/a2a \
-H 'content-type: application/json' \
-d '{
"jsonrpc":"2.0","id":"t2","method":"message/send",
"params":{"message":{
"messageId":"m2","role":"user",
"parts":[{"text":"pricing::quote {}"}]
}}
}' | jq '.result.task.status.state'

# tasks/get — retrieve the stored task
curl -sX POST http://127.0.0.1:3111/a2a \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":"t3","method":"tasks/get","params":{"id":"<paste task.id from t1>"}}' \
| jq '.result.task.status.state'
```

### 5. Verify each gate

```bash
# Hidden function (no a2a.expose) → state:"failed", distinct rejection
curl -sX POST http://127.0.0.1:3111/a2a \
-H 'content-type: application/json' \
-d '{
"jsonrpc":"2.0","id":"g1","method":"message/send",
"params":{"message":{
"messageId":"x","role":"user",
"parts":[{"data":{"function_id":"demo::hidden","payload":{}}}]
}}
}' | jq '.result.task.status.message.parts[0].text'

# Infra prefix → "in the iii-engine internal namespace" message
curl -sX POST http://127.0.0.1:3111/a2a \
-H 'content-type: application/json' \
-d '{
"jsonrpc":"2.0","id":"g2","method":"message/send",
"params":{"message":{
"messageId":"x","role":"user",
"parts":[{"data":{"function_id":"state::set","payload":{}}}]
}}
}' | jq '.result.task.status.message.parts[0].text'

# Tier filter: restart iii-a2a with --tier partner, function has a2a.tier="partner"
./target/release/iii-a2a --tier partner &
curl -s http://127.0.0.1:3111/.well-known/agent-card.json | jq '.skills[].id'
```

### 6. Validate with any A2A client

Any AI agent runtime that speaks A2A JSON-RPC 0.3 works. Point it at
`http://<your-host>:3111/.well-known/agent-card.json` (or the
`--base-url` you configured) and exercise `message/send`.

## Function resolution inside `message/send`

1. **Data part** with `{ "function_id": "foo::bar", "payload": {...} }` —
Expand Down
170 changes: 170 additions & 0 deletions mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,176 @@ still error on `iii_worker_*` and `iii_trigger_register*` because those
paths need the attached process). `--no-builtins` always wins and hides
them on both transports.

## Local testing

Full smoke path from a clean machine. Assumes `iii-sdk` v0.11.3 engine
installed (`which iii`).

### 1. Start the engine

Minimal `config.yaml`:

```yaml
workers:
- name: iii-worker-manager
- name: iii-http
config:
host: 127.0.0.1
port: 3111
- name: iii-state
```

Run it:

```bash
iii --no-update-check
```

Engine now listening on `ws://127.0.0.1:49134` (worker bus) and
`http://127.0.0.1:3111` (HTTP triggers).

### 2. Register test functions

Any worker that tags functions with `mcp.expose: true` works. Quick
standalone Rust worker — save as `testworker/src/main.rs` and
`cargo run --release`:

```rust
use iii_sdk::{register_worker, InitOptions, RegisterFunctionMessage};
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let iii = register_worker("ws://127.0.0.1:49134", InitOptions::default());

iii.register_function_with(
RegisterFunctionMessage {
id: "demo::hello".into(),
description: Some("Say hello".into()),
metadata: Some(json!({ "mcp.expose": true })),
..Default::default()
},
|_input: Value| async move { Ok(json!({"greeting": "hi"})) },
);

tokio::signal::ctrl_c().await?;
Ok(())
}
```

### 3a. Start iii-mcp over Streamable HTTP

```bash
cargo run --release -p iii-mcp -- --no-stdio
# or (release binary): ./target/release/iii-mcp --no-stdio
```

Registers `POST /mcp` on the engine's HTTP port.

### 3b. Sanity check with curl

```bash
# tools/list — should show demo__hello, NO builtins (HTTP default hides them),
# NO state::*/engine::*/iii.*/iii::* (hard floor), NO demo__hidden (no mcp.expose).
curl -sX POST http://127.0.0.1:3111/mcp \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools[].name'

# tools/call
curl -sX POST http://127.0.0.1:3111/mcp \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"demo__hello","arguments":{}}}' \
| jq '.result.content[0].text'
```

### 3c. MCP Inspector (GUI)

Launch the inspector UI:

```bash
DANGEROUSLY_OMIT_AUTH=true npx -y @modelcontextprotocol/inspector
# → MCP Inspector is up at http://127.0.0.1:6274
```

In the browser UI:

| Field | Value |
|---|---|
| Transport Type | Streamable HTTP |
| URL | `http://127.0.0.1:3111/mcp` |

Click **Connect** → green dot. Then try each tab:

- **Tools** → List Tools → should show the functions tagged
`mcp.expose: true`. Click one → fill the args form (text input, not a
dropdown — Inspector decorates it with a chevron but the MCP spec's
`PromptArgument`/tool input schema doesn't carry enum constraints,
so type the value) → Run.
- **Resources** → 4 URIs (`iii://functions`, `iii://workers`,
`iii://triggers`, `iii://context`). Click `iii://functions` → filtered
list (only exposed functions, no infra).
- **Prompts** → 4 canned prompts. Pick `register-function`, fill
`language=python`, `function_id=orders::place`, Get Prompt.
- **Ping** → `{}` response.

### 3d. Inspector CLI (non-interactive smoke)

```bash
DANGEROUSLY_OMIT_AUTH=true npx -y @modelcontextprotocol/inspector --cli \
--transport http http://127.0.0.1:3111/mcp --method tools/list

DANGEROUSLY_OMIT_AUTH=true npx -y @modelcontextprotocol/inspector --cli \
--transport http http://127.0.0.1:3111/mcp \
--method tools/call --tool-name demo__hello
```

### 3e. Stdio transport (Claude Desktop / Cursor path)

```bash
DANGEROUSLY_OMIT_AUTH=true npx -y @modelcontextprotocol/inspector --cli \
./target/release/iii-mcp \
--method tools/list
```

Claude Desktop config that talks to a running engine:

```json
{
"mcpServers": {
"iii": {
"command": "/absolute/path/to/iii-mcp",
"args": ["--engine-url", "ws://127.0.0.1:49134"]
}
}
}
```

Add `"--tier", "user"` and `"--no-builtins"` for a clean user-facing
surface. Add `"--expose-all"` for dev exploration (hard floor still
applies).

### 4. Verify each gate

From the curl / inspector shell above:

```bash
# Hidden function (no mcp.expose) → isError, "not exposed via mcp.expose metadata"
curl -sX POST http://127.0.0.1:3111/mcp -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"demo__hidden","arguments":{}}}' \
| jq '.result.content[0].text'

# Infra prefix → isError, "in the iii-engine internal namespace"
curl -sX POST http://127.0.0.1:3111/mcp -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"state__set","arguments":{}}}' \
| jq '.result.content[0].text'

# Builtin with --no-builtins → "disabled on this server"
# (restart iii-mcp with --no-builtins first)
curl -sX POST http://127.0.0.1:3111/mcp -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"iii_trigger_void","arguments":{"function_id":"demo::hello","payload":{}}}}' \
| jq '.result.content[0].text'
```

## Invocation path

`tools/call` with name `foo__bar` →
Expand Down
Loading