# Tool calling, and what OpenWOP adds to it

> Source: https://openwop.dev/tool-calling/ · OpenWOP, the open-source protocol for multi-agent workflow orchestration.

OpenWOP is additive to tool calling. It defines no model-facing function-calling format and replaces neither MCP nor A2A; it specifies the things those protocols deliberately leave to the harness — who was allowed to call, what a failure looks like on the wire, and what happens the second time the same call arrives.

## What tool calling standardizes, and what it leaves open

In the agent world, "tool calling" is a **model-output convention**. The model emits a name plus JSON arguments, a harness dispatches it, and the result comes back as another turn. Function calling, tool use, and the Model Context Protocol (MCP) are variations on that shape.

They standardize how a tool is *described* — name, description, JSON Schema — and how a single invocation is *framed*. MCP goes further and makes the tool list a server you can connect to.

What none of them specify:

- Who was allowed to make the call.
- What happens if the same call arrives twice.
- How a failure is distinguished from a success that returned nothing.
- Whether replaying a conversation re-fires the side effect.

That is fine for a chat loop. It is not fine for an engine that resumes after a crash.

> **OpenWOP's position:** a tool call is a durable, authorized, idempotent effect on a run's event log — not a turn in a transcript.

## Four surfaces

### Description is a projection, not a registry

OpenWOP tools already lived behind five unrelated surfaces — node-pack typeIds, workflow-as-tool (`core.subWorkflow` and chains), MCP servers, connectors, and host-extension scopes — each discoverable only through its own mechanism. An agent handed a `toolAllowlist` of opaque `<scope>:<tool-id>` strings had no way to resolve them.

The `toolCatalog` capability unifies all five behind one `ToolDescriptor` ([`tool-descriptor.schema.json`](https://openwop.dev/spec/v2/tool-descriptor.schema.json)):

| Field | Field | Field |
| --- | --- | --- |
| `toolId` | `source` | `title` |
| `description` | `inputSchema` | `outputSchema` |
| `annotations` | `auth` | `egress` |
| `approval` | `replayPolicy` | `safetyTier` |
| `costHint` | `latencyHint` | |

`toolId`, `source`, and `safetyTier` are required; `source` is exactly the five origins above (`node-pack`, `workflow`, `mcp`, `connector`, `host-extension`).

`annotations` is the projection onto MCP `ToolAnnotations` ([RFC 0204](https://openwop.dev/rfcs/0204-host-mcp-client-returns-mcp-results.html)). When present it carries all four MCP hints, derived from the host's own `safetyTier`, `replayPolicy`, and `egress` rather than from MCP's defaults. The host assigns those three fields itself and MUST NOT copy them from an MCP server's `annotations`, which are untrusted; an MCP tool it has not classified is `safetyTier: "write"`.

The catalog is read-only, authorization-scoped, and **non-disclosing**: an unauthorized tool id returns **404, not 403**. The catalog will not confirm that a tool exists to someone who may not use it. Descriptors carry requirement *flags* such as `auth.credentialRef`, never credential material. Sub-keys: `sources`, `sessionLifecycle`, `compactView`.

### Invocation is a pair of durable events

Not a transcript entry. `agent.toolCalled` and `agent.toolReturned` are both in the v2 event codemap, alongside `tool.session.opened` and `tool.session.closed`.

### Permission is fail-closed

Under `toolHooks.perToolAuthorization`, the host checks the run principal's scopes **before** invoking — and if authorization cannot be evaluated, it does not invoke. It emits `agent.toolReturned { status: 'forbidden' }` and a 403. Sibling sub-keys: `prePostEvents`, `perToolRateLimit`.

### Failure has to be legible on the wire

The discriminators on `agent.toolReturned` (`error`, `outcome`, `status`) are optional in the schema, so a bare `{agentId, toolName, callId}` would be schema-valid on failure too. **A host MUST NOT report a failed tool that way:** a bare return means the tool succeeded.

## Idempotency: the part no function-calling spec addresses

OpenWOP gives the **effect** — not the call — an identity. This is written out in full normative prose in [`idempotency.md`](https://openwop.dev/spec/v2/core/idempotency.html).

| Rule | What the specification requires |
| --- | --- |
| Keying | Derived from the business operation, stable across every entry point, containing no `runId`, `nodeId`, or ordinal. |
| Attempts | The retry counter MUST NOT participate in the identity. Two retries of one logical invocation collide rather than diverge. |
| Claim | The persist that guards the effect MUST be an atomic claim — compare-and-set or insert-if-absent — that at most one executor can win. |
| Provider key | Where the provider accepts an idempotency key, the host MUST inject the effect identity or a documented deterministic derivative. |
| Retention | An effect record MUST be retained for at least 14 days. |

It is checkable rather than asserted. A host advertising `idempotency` MUST serve `GET /runs/{runId}/effects`, each record carrying `effectId`, `nodeId`, `attempt`, `keying`, and `state`.

### Why the atomic claim exists

A chat-loop mental model breaks under recovery. An orphan sweeper re-dispatches a run whose previous owner is stalled but still alive. Both executors re-execute the node from the start, so both mint the *same* effect identity. If the invocation log is a read-then-write, both miss and both fire.

A host had exactly that shape — `INSERT OR REPLACE`, which always wins and never reports a conflict — and measured **two charges where there should have been one**.

The rule is worded to hold *within a single-instance deployment*. This is not a distributed-systems edge case; it is reachable on one machine.

## Composing with MCP and A2A

OpenWOP does not compete on transport. REST and SSE are the wire, and a v2 host MUST NOT advertise a transport list — `supportedTransports` does not exist in the v2 capabilities schema, and a discovery document carrying it fails validation.

A2A and MCP are **facets, not forks**. A host that speaks either MUST advertise the corresponding facet with every required field, per [`interop.md`](https://openwop.dev/spec/v2/core/interop.html):

| Facet field | A2A | MCP |
| --- | --- | --- |
| Offered | `versions[]` as `major.minor` | `revisions[]` as dates |
| Default | `preferredVersion`, served when the peer names none | `preferredVersion` |
| Floor | `minimumVersion` | `minimumRevision` |
| Freshness | `refreshedAt` | `refreshedAt` |
| Profile ids | `a2a-<major.minor>` | `mcp-<date>` |

A negotiation that would land below the floor fails closed with `interop_version_unsupported`.

There is **no legacy escape hatch**: `a2a-0.3-legacy` and `mcp-2025-06-18-legacy` do not exist in v2, and the `profiles[]` item patterns admit no `-legacy` suffix. Version negotiation is a protocol, not a bag of compatibility shims.

Two more details worth keeping straight:

- **MCP round ceiling.** `mcp.mrtr.maxRounds` (an integer, 1–16) is an advertised ceiling on multi-round tool-result rounds. Exceeding it fails with `mcp_mrtr_rounds_exceeded`.
- **`mcp.serverMount.transports[]` is the MCP server's own transport enum** — explicitly *not* a host transport advertisement. The two get conflated easily.

The catalog composes rather than reinvents. `auth.scopes` declares what the per-call authorization enforces; `egress: "safe-fetch"` advertises that a tool routes through the host's SSRF-guarded fetch; `approval` surfaces the existing approval requirement; and `safetyTier: "exec"` is an honest carve-out that MUST declare `source: "host-extension"` and MUST NOT be dressed up as protocol-tier.

> The agent world standardized the envelope. OpenWOP specifies who may call, what a failure looks like, and what happens the second time the same call arrives.

## Where the normative text lives

- **Effects:** [`idempotency.md`](https://openwop.dev/spec/v2/core/idempotency.html) in the v2 core.
- **The catalog:** [`tool-catalog.md`](https://openwop.dev/spec/v2/core/tool-catalog.html) in the v2 core.
- **Authorization and invocation hooks:** [RFC 0064](https://openwop.dev/rfcs/0064-tool-invocation-hooks-and-authorization.html).
- **Error codes:** the [error vocabulary](https://openwop.dev/errors/).
