> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentfront.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Meta-Tools

> search_skill / load_skill / run_workflow — the three tools the MCP client sees, and the prompts to give the LLM.

The plugin exposes exactly three tools through standard `tools/list`. Every per-operation OpenAPI tool is hidden behind these — `run_workflow` is the **only** path to actually invoke an upstream operation. It runs a short AgentScript program in an enclave sandbox, and each `callTool(actionId, input)` inside that script is the sole authorization checkpoint for that action.

## `search_skill`

Search the registry by free-form query.

```json title="input" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
{
  "query": "create an invoice",
  "limit": 20,
  "tags": ["billing"]
}
```

```json title="output" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
{
  "skills": [
    {
      "skillId": "invoices",
      "name": "Invoices",
      "description": "Issue, query, and refund invoices.",
      "score": 0.42,
      "bundleVersion": "1.0.0"
    }
  ]
}
```

The score comes from the SDK's `SkillRegistry.search()` (TF-IDF). `bundleVersion` is included when the skill was registered from a bundle — polling clients can use it to detect bundle swaps without depending on `notifications/skills/list_changed`.

## `load_skill`

Load the full markdown instructions and the executable actions for one skill.

```json title="input" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
{ "skillId": "invoices" }
```

```json title="output" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
{
  "skill": {
    "id": "invoices",
    "name": "Invoices",
    "description": "Issue, query, and refund invoices.",
    "instructions": "# Invoices skill\n\nThree operations: createInvoice, getInvoice, refundInvoice.",
    "bundleVersion": "1.0.0",
    "actions": [
      {
        "actionId": "createInvoice",
        "summary": "Create a new invoice",
        "inputJsonSchema": { "type": "object", "properties": { "customerId": {}, "amount": {} }, "required": ["customerId","amount"] },
        "outputJsonSchema": { "type": "object" }
      }
    ]
  },
  "isComplete": true
}
```

The LLM should call `load_skill` once per skill it intends to use, then keep the action schemas in context. Schemas inside `actions[]` are the same schemas the OpenAPI spec declared; the executor validates input against them on every `callTool` a `run_workflow` script issues. The `actionId` values listed here are exactly what you pass to `callTool(actionId, input)`.

Throws an MCP-protocol error when the `skillId` is unknown.

## `run_workflow`

Execute a task by running a short **AgentScript** program inside a secure **enclave** sandbox. This is the EXECUTE step: after `search_skill` and `load_skill`, the LLM writes a script that calls the loaded skill's actions to accomplish the user's task — chaining several calls in ONE round-trip.

```json title="input" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
{
  "script": "const inv = await callTool(\"createInvoice\", { customerId: \"cus_42\", amount: 1234 });\nreturn inv;"
}
```

```json title="output (success)" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
{
  "success": true,
  "value": { "id": "inv_1", "status": "open" },
  "stats": { "durationMs": 38, "toolCalls": 1, "steps": 12 }
}
```

```json title="output (failure)" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
{
  "success": false,
  "error": "authority denied: missing required role 'admin'",
  "stats": { "durationMs": 4, "toolCalls": 1, "steps": 3 }
}
```

### Inside the script

The script runs in the dependency-free `@enclave-vm` interpreter — **no host access** and **no network** except `callTool`:

* `await callTool(actionId, input)` invokes a loaded skill's action (the `actionId` is what `load_skill` lists under `actions[]`). It resolves the operation across all loaded skills, runs it through the full pipeline below, and returns the action's response `data` — or **throws** if the action is unknown / unauthorized / fails validation.
* `Math` and `JSON` are available.
* The script's final `return <value>` becomes the workflow's `value`.

```js title="example: chain two actions in one round-trip" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
const t = await callTool("getTodo", { id: 1 });
const u = await callTool("getUser", { id: t.userId });
return { todo: t.title, owner: u.name };
```

The sandbox caps a single workflow's step count, tool-call count (25), and wall time (8s).

### Per-`callTool` pipeline

Each `callTool(actionId, input)` runs the same authorized/validated path a single direct action call would:

1. **Resolve** `actionId` from the plugin-private `HiddenOpRegistry` across loaded skills. Unknown action **throws** `unknown action "..." — run search_skill then load_skill to discover available actions`.
2. **Pin** the operation descriptor at call entry — a hot bundle-swap mid-call doesn't change the descriptor the in-flight call uses.
3. **Authorize**: evaluate the action's `requiredAuthorities` (if any) against the caller's `authInfo` via `@frontmcp/auth`'s `AuthoritiesEngine`. Denial **throws** `authority denied: ...`.
4. **Validate input** strictly against the action's JSON Schema. A validation failure **throws** `input validation failed: ...`.
5. **Build** the outbound HTTP request through `@frontmcp/adapters/openapi`'s `buildRequest` — path interpolation, header injection defenses, body serialization, security context resolution all flow through the adapter.
6. **SSRF gate**: scheme allowlist (`https:` by default), hostname allowlist (built from the active bundle's `services[].baseUrl`), post-DNS IPv4/IPv6 blocklist (RFC 1918, link-local incl. cloud metadata, loopback, ULA), cloud-metadata hostname blocklist.
7. **Fetch** with timeout (`op.timeoutMs ?? defaultTimeoutMs`) and response-size cap (`op.maxResponseBytes ?? defaultMaxResponseBytes`).
8. **Parse** the response through `parseResponse`, validate the JSON body against the action's `outputSchema`, and return the response `data` into the script.

A `callTool` failure (auth, schema, network, SSRF) **throws inside the script**. If the script doesn't catch it, the workflow returns `{ success: false, error, stats }` — `run_workflow` itself never throws to the MCP client; every outcome is a structured envelope. (If the enclave isn't installed, `run_workflow` returns `{ success: false, error: "workflow execution unavailable: ..." }`.)

## Prompts to give the LLM

The exact prompt depends on your agent harness, but these phrasings work well in practice (cribbed from the tool descriptions the plugin ships):

* For `search_skill`: *"Use this tool first to discover what skills exist for the user's request."*
* For `load_skill`: *"Call this tool once per skill you intend to use. The instructions field is markdown — read it carefully before invoking any action."*
* For `run_workflow`: *"This is the only way to invoke upstream operations. Write a short AgentScript program and call `await callTool(actionId, input)` for each loaded action — you can chain several calls in one round-trip. End with `return <value>`. Each `callTool` auto-validates input and applies authority checks; an unauthorized or failing call throws inside your script. The result comes back as `{ success, value, error, stats }`."*

## Skills-only mode

If your FrontMCP server is configured with `skills_only` mode (set via `?mode=skills_only` on the MCP transport URL), the meta-tools stay visible — they're the only way to use skills, and the plugin treats them as exempt from the skills-only filter. See [Skills](/frontmcp/features/skills) for the skills-only contract.

## What about hidden tools?

By default (`exposeOperationsAsInternalTools: true`), per-operation tools are registered in `scope.tools` with **internal** visibility. This means they:

* Are still callable via the SDK / direct client and can participate in DI, hooks, and observability
* **Do NOT appear** in the public MCP `tools/list` response
* Are **NOT directly callable** from MCP clients via `tools/call`
* Always live in the plugin's `HiddenOpRegistry` so a `run_workflow` script's `callTool` can find and run them with the ABAC checkpoint

If you set `exposeOperationsAsInternalTools: false`, the per-operation tools live **only** in `HiddenOpRegistry` (not in `scope.tools`) and the only way to invoke them is a `callTool` inside a `run_workflow` script. Use this stricter mode when you need to guarantee that nothing — not even other plugins or in-process tooling — can bypass the ABAC checkpoint.
