skills
v0.2.2Agentic content registry worker. Hosts skills + prompts + the iii:// resource resolver that the mcp worker serves to harnesses.
- macOS: arm64 · x64
- Linux: arm64 · armv7 · x64
- Windows: arm64 · x64 · x86
full markdown
/workers/skills.md?version=0.2.2. paste it into an llm prompt or pipe it through curl from a worker.install
configuration
- scopes:
prompts: prompts
skills: skills
state_timeout_ms: 10000dependencies
readme
skills
Agentic content registry worker for the iii engine.
Persists two kinds of content for AI clients: skills (markdown
orientation docs about a worker's tools, served at iii://{id} plus
an auto-rendered iii://skills index) and prompts (parametric
slash-command templates, dispatched on demand to a registered handler
function). Workers register their content at boot via iii.trigger;
skills persists everything to iii-state and emits change
notifications on skills::on-change / prompts::on-change for any
other worker that wants to react.
| Surface | What clients see | When to use it |
|---|---|---|
| Skills | Markdown documents under iii://{id} plus an iii://skills index |
Orientation: "when and why to use my worker's tools" |
| Prompts | Slash-commands (e.g. /send-email) with declared arguments |
Parametric command templates the user invokes |
The rest of this README walks you through installing the worker and writing a worker that publishes skills and slash-command prompts.
Table of contents
- Install
- Quickstart: publish a skill and a slash-command
- Configuration
- Functions
- Custom trigger types
- Local development & testing
Install
iii worker add skillsiii worker add fetches the binary, writes a config block into
~/.iii/config.yaml, and the engine starts the worker on the next
iii start.
Quickstart: publish a skill and a slash-command
A worker can plug into this registry three ways. They compose: most workers ship one top-level skill, optional sub-skill markdown sections, and zero or more slash-commands.
The interface is two iii.trigger calls, plus optional in-process
handler functions for sub-skills and prompt rendering:
| Step | Function id | What it stores |
|---|---|---|
| Register a skill | skills::register |
Markdown body keyed by id |
| Register a prompt | prompts::register |
Slash-command name + args + handler function id |
| Subscribe to changes | skills::on-change / prompts::on-change |
Custom trigger types fired on every mutation |
Skills (markdown orientation docs)
A skill is a markdown document explaining when and why to use your worker's tools. Skills don't replicate the JSON schemas of your functions — clients get those from the engine's tool listing. Skills tell the LLM which tool for which job.
Register one with skills::register:
use iii_sdk::{register_worker, InitOptions, TriggerRequest};
use serde_json::json;
let iii = register_worker("ws://localhost:49134", InitOptions::default());
iii.trigger(TriggerRequest {
function_id: "skills::register".into(),
payload: json!({
"id": "myworker",
"skill": include_str!("../docs/skill.md"),
}),
action: None,
timeout_ms: Some(5_000),
}).await?;Validation rules (rejected at registration time):
id: 1+ path segments separated by/. Each segment uses lowercase ASCII letters, digits,-and_; max 64 chars per segment. Total id length capped at 1024 chars.- The literal
fnis reserved as the first segment of any id (used as the section-URI marker — see Section URIs).fnmay appear at deeper segments freely (docs/fn-referenceis fine). skill: non-empty; max 256 KiB.
Re-registering with the same id overwrites the body and refreshes the
registered_at timestamp. Workers MUST re-register on every boot
so any updated content shipped with the worker lands in the registry.
Doing this unconditionally also keeps you robust to state being wiped
during local development.
URI scheme
| URI | Returns |
|---|---|
iii://skills |
Auto-rendered markdown index of every registered skill (entry point). |
iii://{id} |
The body registered at that id (single segment). |
iii://{a}/{b}/.../{leaf} |
The body registered at the slashed path. Any depth. First segment must NOT equal fn. |
iii://fn/{a}/{b}/.../{leaf} |
Trigger function a::b::...::leaf with {} and serve its output. Each / after fn/ becomes ::. |
The first-segment fn literal flips the URI from "skill body lookup"
to "function trigger". Anything else as the first segment is treated
as a slashed path of skill ids and resolved against the skills
state scope as a single key. This keeps the scheme unambiguous at
arbitrary depth: iii://resend/email/send is always a stored
markdown body, while iii://fn/resend/email/send is always a call to
resend::email::send.
The auto-rendered iii://skills index uses the first H1 of each skill
as the link title and the first non-heading paragraph as the
description (truncated at 140 chars). Nested entries are indented by
2 * depth spaces so the index reads as a tree. Lead each registered
body with a # {title} and a short summary paragraph and the index
reads cleanly without further work.
Modeling a deep skill tree
Top-level skill bodies should stay small — a router that links to
deeper, more granular content. The agent only loads iii://yourworker
initially; everything else is fetched lazily through skill::fetch
(see below). Token cost grows with how deep the agent actually drills,
not with how big your worker's docs are.
Each level is an independent state row. Parents don't have to exist first (orphans are allowed but read awkwardly in the index). A re-register at the same id overwrites that row only — siblings and children are untouched.
Worked example: a small resend worker with two depths of nested
skills plus one section URI that points at a function.
use iii_sdk::{RegisterFunction, TriggerRequest};
use serde_json::{json, Value};
// 1. Top-level router (small body — links into children).
iii.trigger(TriggerRequest {
function_id: "skills::register".into(),
payload: json!({
"id": "resend",
"skill": "# resend\n\nEmail provider integration.\n\n\
- [`email`](iii://resend/email) — sending and tracking\n\
- [`contacts`](iii://resend/contacts) — audience management\n\
\n\
Live status check: \
[`health`](iii://fn/resend/health)\n",
}),
action: None,
timeout_ms: Some(5_000),
}).await?;
// 2. Mid-level group (also a router into its own children).
iii.trigger(TriggerRequest {
function_id: "skills::register".into(),
payload: json!({
"id": "resend/email",
"skill": "# resend/email\n\nEmail flows.\n\n\
- [`send`](iii://resend/email/send) — outbound\n\
- [`track`](iii://resend/email/track) — webhooks + status\n",
}),
action: None,
timeout_ms: Some(5_000),
}).await?;
// 3. Leaf body — the actual content the agent will read once it
// decides to drill in. Loaded on demand via skill::fetch.
iii.trigger(TriggerRequest {
function_id: "skills::register".into(),
payload: json!({
"id": "resend/email/send",
"skill": include_str!("../docs/resend-email-send.md"),
}),
action: None,
timeout_ms: Some(5_000),
}).await?;
// 4. A section URI: live function output instead of a static body.
// `iii://fn/resend/health` triggers `resend::health` with {} and
// returns its output. Useful for status, dynamic catalogues, etc.
iii.register_function(
RegisterFunction::new("resend::health", |_input: Value| {
Ok::<_, String>(json!({
"content": "# Resend health\n\nAPI: ok. Quota: 73% remaining."
}))
})
.description("Live operational health for the resend integration."),
);Once registered, an agent reading iii://resend sees the small
router. If it needs send semantics, it asks skill::fetch for
iii://resend/email/send (one round trip, ~200 lines of markdown
instead of the worker's full 2000-line documentation).
Section URIs (function-backed content)
Section URIs trigger an iii function with {} and serve its output
as resource content. The URI shape is iii://fn/{a}/{b}/... and the
mapping to a function id is mechanical: every / after fn/ becomes
::. This means agents and humans can construct a URI from any
function id by string-replace alone.
| URI | Function id triggered |
|---|---|
iii://fn/foo |
foo |
iii://fn/scope/foo |
scope::foo |
iii://fn/resend/email/send |
resend::email::send |
iii://fn/a/b/c/d |
a::b::c::d |
A function backing a section URI should return one of:
- a
String→ served astext/markdown. { "content": "..." }→ thecontentfield is served astext/markdown.- anything else → pretty-printed JSON, served as
application/json.
The recursion guard blocks the internal namespaces (engine::*,
state::*, mcp::*, skills::*, prompts::*, iii::*, iii.*,
a2a::*) at read time so a crafted iii://fn/state/set can't tunnel
into infra.
Function ids that contain . (e.g. the engine's iii.on_foo
lifecycle handlers) are NOT reachable through this shape. That's
intentional — those handlers are triggered on engine events, not by
agents.
Fetching skills on demand
Top-level skill bodies are designed to stay small (a router that
points at sub-skills) so they don't burn the LLM's context budget on
content the agent might never need. Consumers resolve those iii://
links lazily via the skill::fetch tool — a thin batched wrapper
over the same resolver that backs skills::resources-read.
| Field | Type | Description |
|---|---|---|
uri |
string | A single iii:// URI to read. |
uris |
string[] | Multiple iii:// URIs to read and concatenate. Wins when both are provided. |
Each URI is wrapped as # {uri}\n\n{body} and sections are joined
with \n\n---\n\n into one markdown document, so the agent can pull
several sub-skills in one round trip:
{ "uris": ["iii://resend/email/send", "iii://resend/email/track"] }This composes naturally with the multi-level body shape: the agent
loads the top-level router once, then batches fetches for the deeper
bodies it actually needs (and the iii://fn/... section URIs of the
functions it expects to call).
Two registrations, one handler:
skill::fetch— public alias on a non-hidden namespace. MCP clients see it as the toolskill__fetch, which is what the description tells agents to call when they encounteriii://links in skill instructions. This is the id agents should use.skills::fetch_skill— canonical id, colocated with the rest of the registry. Hidden from MCPtools/listbecause the bridge hard-floors everyskills::prefix; sibling workers can still call it directly viaiii.trigger.
Validation rules (rejected before any resource is touched):
- At least one of
uri/urismust be present and non-blank after trim. - Every URI must start with
iii://. Other schemes are rejected with a clear error so an agent can correct the call rather than silently fetching nothing.
Prompts (slash commands)
A prompt is a parametric template the user invokes from the
client (e.g. typing /send-email). The template is rendered by your
handler function with the user-supplied arguments.
Registration is two steps:
- Register a normal handler function. No special opt-in flag is
needed —
skillsdispatches the handler directly throughiii.triggerwhen the prompt is invoked. - Register the prompt itself, pointing at that function.
use iii_sdk::{RegisterFunction, TriggerRequest};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Deserialize, JsonSchema)]
struct SendEmailArgs {
to: String,
subject: String,
}
#[derive(Serialize, JsonSchema)]
struct PromptOutput {
content: String,
}
// Step 1: handler renders the prompt
iii.register_function(
RegisterFunction::new("myworker::send_email_prompt", |args: SendEmailArgs| {
Ok::<_, String>(PromptOutput {
content: format!(
"Compose an email to {} with the subject \"{}\". Be concise and friendly.",
args.to, args.subject
),
})
})
.description("Render the send-email slash-command body."),
);
// Step 2: register the slash-command
iii.trigger(TriggerRequest {
function_id: "prompts::register".into(),
payload: json!({
"name": "send-email",
"description": "Compose and send an email",
"arguments": [
{ "name": "to", "description": "Recipient address", "required": true },
{ "name": "subject", "description": "Subject line", "required": true }
],
"function_id": "myworker::send_email_prompt"
}),
action: None,
timeout_ms: Some(5_000),
}).await?;Validation rules (rejected at registration time):
name: lowercase ASCII letters, digits,-and_only; max 64 chars (no::, no/, no whitespace)description: non-empty after trimfunction_id: non-empty after trimarguments[].name: non-empty, no duplicates within the list
Argument schema gotcha
The arguments field is what the client sees in its argument-picker
UI. It does not auto-validate at runtime. The handler is
responsible for validating its own input — treat the schema as a
contract you uphold inside the function.
Output normalization
The handler can return any of:
String→ wrapped as a single user-text message{ "content": "..." }→ wrapped as a single user-text message{ "messages": [ ... ] }→ passed through unchanged (full control; use this for multi-turn templates or assistant-prefilled responses)- anything else → an error returned to the client
Lifecycle & boot-time handshake
Skills and prompts are stored in iii-state under scopes skills and
prompts (configurable). Both registries are durable and survive
restarts of either skills or your worker.
Workers MUST re-register on every boot. skills itself can be
absent or come up after your worker, so treat the registration as
best-effort with capped exponential backoff:
use std::sync::Arc;
use std::time::{Duration, Instant};
use iii_sdk::{TriggerRequest, III};
use serde_json::json;
fn register_with_iii_skills(iii: Arc<III>) {
tokio::spawn(async move {
let mut backoff = Duration::from_secs(5);
let started = Instant::now();
loop {
let result = iii.trigger(TriggerRequest {
function_id: "skills::register".into(),
payload: json!({
"id": "myworker",
"skill": include_str!("../docs/skill.md"),
}),
action: None,
timeout_ms: Some(5_000),
}).await;
if result.is_ok() {
tracing::info!("registered myworker skill");
return;
}
if started.elapsed() > Duration::from_secs(180) {
tracing::warn!("skills handshake gave up; install / start it and restart this worker");
return;
}
tokio::time::sleep(backoff).await;
backoff = (backoff * 2).min(Duration::from_secs(60));
}
});
}On graceful shutdown, optionally call skills::unregister and
prompts::unregister to clean up. Crashes leave entries in state
indefinitely; an operator can list them with skills::list /
prompts::list (which expose registered_at) and remove dead ones
manually.
Mutations of either scope fire the skills::on-change /
prompts::on-change custom trigger types automatically. Any worker
can subscribe and react to registrations without polling.
Subscribing to registry changes
Workers that need to react to registrations (a dashboard, a metrics sink, a sibling protocol bridge) subscribe to the custom trigger types:
use iii_sdk::RegisterTriggerInput;
use serde_json::json;
iii.register_trigger(RegisterTriggerInput {
trigger_type: "skills::on-change".into(),
function_id: "myworker::on_skill_change".into(),
config: json!({}),
metadata: None,
})?;Payload sent to each subscriber:
| Trigger type | Payload |
|---|---|
skills::on-change |
{ "op": "register" | "unregister", "id": " |
prompts::on-change |
{ "op": "register" | "unregister", "name": " |
Dispatches are fire-and-forget (Void), so the write path on
skills::register / prompts::register doesn't block on downstream
latency. Idempotent unregisters that found nothing to delete don't
fire.
Full multi-language example
Self-contained worker that registers one skill, one sub-skill function, and one slash-command prompt. Boots, registers everything, then sleeps until SIGINT.
Rust
use iii_sdk::{register_worker, InitOptions, RegisterFunction, TriggerRequest};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
#[derive(Default, Deserialize, JsonSchema)]
struct GreetArgs {
name: String,
}
#[derive(Serialize, JsonSchema)]
struct SkillContent {
content: String,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let iii = register_worker("ws://localhost:49134", InitOptions::default());
// 1. Sub-skill function: returns extended docs as markdown.
iii.register_function(
RegisterFunction::new("demo::guide", |_input: Value| {
Ok::<_, String>(SkillContent {
content: "# Demo guide\n\nThe long-form docs for the demo worker."
.into(),
})
})
.description("Detailed guide for the demo worker."),
);
// 2. Prompt handler: turns slash-command args into a templated message.
iii.register_function(
RegisterFunction::new("demo::greet_prompt", |input: GreetArgs| {
Ok::<_, String>(SkillContent {
content: format!("Greet {} warmly.", input.name),
})
})
.description("Render the greet prompt."),
);
// Wire the registries. Always skills::register / prompts::register
// at startup so the entries refresh after any worker upgrade.
iii.trigger(TriggerRequest {
function_id: "skills::register".into(),
payload: json!({
"id": "demo",
"skill": "# demo\n\n\
A demo worker.\n\n\
See [`guide`](iii://demo/demo::guide) for the long version.\n"
}),
action: None,
timeout_ms: Some(5_000),
}).await?;
iii.trigger(TriggerRequest {
function_id: "prompts::register".into(),
payload: json!({
"name": "greet",
"description": "Compose a greeting.",
"arguments": [
{ "name": "name", "description": "Who to greet", "required": true }
],
"function_id": "demo::greet_prompt"
}),
action: None,
timeout_ms: Some(5_000),
}).await?;
tokio::signal::ctrl_c().await?;
Ok(())
}Node
import { registerWorker } from 'iii-sdk'
const iii = registerWorker('ws://localhost:49134')
iii.registerFunction({ id: 'demo::guide' }, async () => ({
content: '# Demo guide\n\nThe long-form docs for the demo worker.',
}))
iii.registerFunction({ id: 'demo::greet_prompt' }, async ({ name = 'world' }) => ({
content: `Greet ${name} warmly.`,
}))
await iii.trigger({
function_id: 'skills::register',
payload: {
id: 'demo',
skill: '# demo\n\nA demo worker.\n\nSee [`guide`](iii://demo/demo::guide) for the long version.\n',
},
})
await iii.trigger({
function_id: 'prompts::register',
payload: {
name: 'greet',
description: 'Compose a greeting.',
arguments: [{ name: 'name', description: 'Who to greet', required: true }],
function_id: 'demo::greet_prompt',
},
})Python
from iii_sdk import register_worker
iii = register_worker('ws://localhost:49134')
@iii.register_function('demo::guide')
async def guide(_input):
return {'content': '# Demo guide\n\nThe long-form docs for the demo worker.'}
@iii.register_function('demo::greet_prompt')
async def greet_prompt(input):
name = input.get('name', 'world')
return {'content': f'Greet {name} warmly.'}
await iii.trigger(
function_id='skills::register',
payload={
'id': 'demo',
'skill': '# demo\n\nA demo worker.\n\nSee [`guide`](iii://demo/demo::guide) for the long version.\n',
},
)
await iii.trigger(
function_id='prompts::register',
payload={
'name': 'greet',
'description': 'Compose a greeting.',
'arguments': [{'name': 'name', 'description': 'Who to greet', 'required': True}],
'function_id': 'demo::greet_prompt',
},
)Configuration
# skills runtime config.
# State scopes used to persist the two registries. Changing these at
# runtime is supported but orphans prior entries; treat them as
# deployment-time constants in practice.
scopes:
skills: skills
prompts: prompts
# Default timeout for state::* and sub-skill function triggers (ms).
state_timeout_ms: 10000CLI flags:
--config <PATH> Path to config.yaml [default: ./config.yaml]
--url <URL> WebSocket URL of the iii engine [default: ws://127.0.0.1:49134]
--manifest Output the module manifest as JSON and exit
-h, --help Print helpIf 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.
Functions
Thirteen functions across the two registries plus one fetch alias.
The seven public CRUD entries (six registry CRUD + the public fetch
alias) are callable by any worker over iii.trigger; the alias is
also surfaced as an MCP tool. The remaining six entries are
internal-RPC reserved for protocol-bridge workers; they never appear
in tools/list because their ids start with the skills:: /
prompts:: hard-floor prefixes.
| Function ID | Description |
|---|---|
skills::register |
Store a markdown skill body keyed by id. |
skills::unregister |
Delete a skill by id. Idempotent. |
skills::list |
Metadata-only listing, sorted by id. |
skills::resources-list |
Internal: enumerate registered skills as resource entries. |
skills::resources-read |
Internal: resolve an iii:// URI to its content. |
skills::resources-templates |
Internal: declare the iii://{id} URI templates. |
skills::fetch_skill |
Internal: batched read across one or more iii:// URIs. Hidden from MCP because the id starts with skills::. |
skill::fetch |
Public alias of skills::fetch_skill on a non-hidden namespace. Surfaces as the MCP tool skill__fetch so agents can resolve iii:// links on demand. |
prompts::register |
Store a slash-command prompt definition. |
prompts::unregister |
Delete a prompt by name. Idempotent. |
prompts::list |
Metadata-only listing, sorted by name. |
prompts::mcp-list |
Internal: enumerate registered prompts with argument schemas. |
prompts::mcp-get |
Internal: dispatch a registered prompt handler with caller-supplied arguments. |
Custom trigger types
| Trigger type | Fires when | Payload to subscribers |
|---|---|---|
skills::on-change |
After every mutation of the skills registry | { "op": "register" | "unregister", "id": " |
prompts::on-change |
After every mutation of the prompts registry | { "op": "register" | "unregister", "name": " |
Local development & testing
Run from source
cargo run --release -- --url ws://127.0.0.1:49134 --config ./config.yamlTests
# Fast, offline — exercises the pure helpers (markdown / URI / validators)
# without needing an iii engine.
cargo test --test bdd -- --tags @pure
# Full suite — requires an iii engine on ws://127.0.0.1:49134
# (or III_ENGINE_WS_URL). Runs the full BDD scenario set covering
# every function and every validation rule.
cargo test
# One feature group at a time. Available tags:
# @pure @markdown
# @engine @skills_register @skills_resources @skills_fetch @skills_nested
# @prompts_register @prompts_get @notifications @mcp_bridge
cargo test --test bdd -- --tags @skills_nestedThe BDD harness lives under tests/. Feature files mirror the
modules in src/functions/. Step definitions under
tests/steps/ drive each feature through the same
iii.trigger path the production binary uses.
Migration (v0.1.x → v0.2.0)
The URI scheme changed in two breaking ways. Workers that registered
skills against 0.1.x need to update before upgrading.
1. Function-backed URIs use fn/ as a fixed prefix. The legacy
two-segment form iii://{anything}/{worker::function_id} is gone.
Rewrite each link by prefixing iii://fn/ and replacing every ::
with /:
| Old | New |
|---|---|
iii://anything/myworker::echo |
iii://fn/myworker/echo |
iii://brain/brain::summarize |
iii://fn/brain/summarize |
iii://x/state::get |
iii://fn/state/get (still blocked by the recursion guard) |
The legacy two-segment form now resolves to a state-backed sub-skill lookup, so it returns "Skill not found" instead of triggering the function. There is no silent fallback.
2. Multi-segment ids are now stored skills. iii://resend/email
and similar are state-backed bodies you can register. There is no
depth cap (per-segment cap stays at 64 chars; total id capped at
1024 chars). The fn literal is reserved as the first segment of
any registered id; deeper occurrences (docs/fn-reference) are fine.
Workers MUST update their bundled markdown bodies (replace any
iii://x/worker::fn in skill text with iii://fn/worker/fn) and
re-register at boot. The skills::on-change trigger fires on every
re-registration, so subscribers stay in sync automatically.
api reference (json)
{
"functions": [
{
"description": "Register a slash-command prompt; clients call prompts/get to render it.",
"metadata": {},
"name": "prompts::register",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"PromptArgument": {
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"name": {
"type": "string"
},
"required": {
"default": false,
"type": "boolean"
}
},
"required": [
"name"
],
"type": "object"
}
},
"properties": {
"arguments": {
"default": [],
"items": {
"$ref": "#/definitions/PromptArgument"
},
"type": "array"
},
"description": {
"description": "Free-text description shown in the client's prompt picker.",
"type": "string"
},
"function_id": {
"description": "Handler function called on prompts/get with the supplied arguments.",
"type": "string"
},
"name": {
"description": "Unique prompt name (lowercase ASCII, kebab/underscore, max 64 chars).",
"type": "string"
}
},
"required": [
"description",
"function_id",
"name"
],
"title": "RegisterPromptInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"name": {
"type": "string"
},
"registered_at": {
"type": "string"
}
},
"required": [
"name",
"registered_at"
],
"title": "RegisterPromptOutput",
"type": "object"
}
},
{
"description": "Register a markdown skill so it appears as iii://{id} in resources/list.",
"metadata": {},
"name": "skills::register",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"description": "Unique skill id (lowercase ASCII, kebab/underscore, max 64 chars).",
"type": "string"
},
"skill": {
"description": "Markdown body served at iii://{id}.",
"type": "string"
}
},
"required": [
"id",
"skill"
],
"title": "RegisterSkillInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string"
},
"registered_at": {
"type": "string"
}
},
"required": [
"id",
"registered_at"
],
"title": "RegisterSkillOutput",
"type": "object"
}
},
{
"description": "Internal: returns the MCP resources/list envelope with the iii://skills index + one iii://{id} entry per registered skill.",
"metadata": {},
"name": "skills::resources-list",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "EmptyInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
}
},
{
"description": "List registered prompts (name, function_id, arg count, registered_at).",
"metadata": {},
"name": "prompts::list",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ListPromptsInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"PromptEntry": {
"properties": {
"arguments": {
"format": "uint",
"minimum": 0,
"type": "integer"
},
"function_id": {
"type": "string"
},
"name": {
"type": "string"
},
"registered_at": {
"type": "string"
}
},
"required": [
"arguments",
"function_id",
"name",
"registered_at"
],
"type": "object"
}
},
"properties": {
"prompts": {
"items": {
"$ref": "#/definitions/PromptEntry"
},
"type": "array"
}
},
"required": [
"prompts"
],
"title": "ListPromptsOutput",
"type": "object"
}
},
{
"description": "Internal: resolves an iii:// URI and returns the MCP resources/read contents envelope.",
"metadata": {},
"name": "skills::resources-read",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"uri": {
"type": "string"
}
},
"required": [
"uri"
],
"title": "ReadResourceInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
}
},
{
"description": "Internal: dispatches a registered prompt handler and normalizes the result for MCP prompts/get.",
"metadata": {},
"name": "prompts::mcp-get",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"arguments": {
"default": null
},
"name": {
"type": "string"
}
},
"required": [
"name"
],
"title": "PromptGetInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
}
},
{
"description": "Remove a registered prompt by name.",
"metadata": {},
"name": "prompts::unregister",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"name": {
"type": "string"
}
},
"required": [
"name"
],
"title": "UnregisterPromptInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"name": {
"type": "string"
},
"removed": {
"type": "boolean"
}
},
"required": [
"name",
"removed"
],
"title": "UnregisterPromptOutput",
"type": "object"
}
},
{
"description": "Fetches the content of one or more skill resources identified by iii:// URIs. When you encounter iii:// links in skill instructions, use this tool to retrieve their contents (batch with `uris` when helpful).",
"metadata": {},
"name": "skill::fetch",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"uri": {
"default": null,
"description": "A single iii:// URI to read. Must start with \"iii://\".",
"type": [
"string",
"null"
]
},
"uris": {
"default": null,
"description": "Multiple iii:// URIs to read and concatenate into one response.",
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
}
},
"title": "FetchSkillInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "String",
"type": "string"
}
},
{
"description": "List registered skills (id, body length, registered_at) without bodies.",
"metadata": {},
"name": "skills::list",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ListSkillsInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"SkillEntry": {
"properties": {
"bytes": {
"format": "uint",
"minimum": 0,
"type": "integer"
},
"id": {
"type": "string"
},
"registered_at": {
"type": "string"
}
},
"required": [
"bytes",
"id",
"registered_at"
],
"type": "object"
}
},
"properties": {
"skills": {
"items": {
"$ref": "#/definitions/SkillEntry"
},
"type": "array"
}
},
"required": [
"skills"
],
"title": "ListSkillsOutput",
"type": "object"
}
},
{
"description": "Remove a registered skill by id.",
"metadata": {},
"name": "skills::unregister",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"title": "UnregisterSkillInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string"
},
"removed": {
"type": "boolean"
}
},
"required": [
"id",
"removed"
],
"title": "UnregisterSkillOutput",
"type": "object"
}
},
{
"description": "Fetches the content of one or more skill resources identified by iii:// URIs. When you encounter iii:// links in skill instructions, use this tool to retrieve their contents (batch with `uris` when helpful).",
"metadata": {},
"name": "skills::fetch_skill",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"uri": {
"default": null,
"description": "A single iii:// URI to read. Must start with \"iii://\".",
"type": [
"string",
"null"
]
},
"uris": {
"default": null,
"description": "Multiple iii:// URIs to read and concatenate into one response.",
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
}
},
"title": "FetchSkillInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "String",
"type": "string"
}
},
{
"description": "Internal: returns the MCP resources/templates/list envelope.",
"metadata": {},
"name": "skills::resources-templates",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "EmptyInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
}
},
{
"description": "Internal: returns the MCP prompts/list envelope (full arguments schema for each registered prompt).",
"metadata": {},
"name": "prompts::mcp-list",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "EmptyInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
}
}
],
"triggers": [
{
"description": "Fires after any mutation of the prompts registry (register / unregister).",
"invocation_schema": {},
"metadata": {},
"name": "prompts::on-change",
"return_schema": {}
},
{
"description": "Fires after any mutation of the skills registry (register / unregister).",
"invocation_schema": {},
"metadata": {},
"name": "skills::on-change",
"return_schema": {}
}
]
}