OpenWOP openwop.dev

Tool calling additive to MCP and A2A

A tool call is an effect, not a turn.

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.

A 75-second narrated film, with English captions. A customer is charged twice though nobody made a mistake: an agent asked to take a payment goes quiet, a helper steps in, both see that it is not done yet, and both take the payment. The film then shows the rule that prevents it. Before acting, each must claim the job, and only one claim can win, so in the same race the card is charged once. Afterwards there is a record of what was done, kept for at least 14 days. A scripted illustration of the race this page describes.

What tool calling leaves open

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

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

Four surfaces

  1. 1Description is a projection, not a registryOpenWOP 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.
  2. 2Invocation is a pair of durable eventsNot a transcript entry.
  3. 3Permission is fail-closedUnder toolHooks.perToolAuthorization, the host checks the run principal's scopes before invoking — and if authorization cannot be evaluated, it does not invoke.
  4. 4Failure has to be legible on the wireThe 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.

Idempotency: the part no function-calling spec addresses

OpenWOP gives the effect — not the call — an identity. The rules in full.

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.

The full argument

The catalog fields, the events, the authorization hooks, the effect rules, and how it composes with MCP and A2A.

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):

FieldFieldField
toolIdsourcetitle
descriptioninputSchemaoutputSchema
annotationsauthegress
approvalreplayPolicysafetyTier
costHintlatencyHint

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). 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.

RuleWhat the specification requires
KeyingDerived from the business operation, stable across every entry point, containing no runId, nodeId, or ordinal.
AttemptsThe retry counter MUST NOT participate in the identity. Two retries of one logical invocation collide rather than diverge.
ClaimThe 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 keyWhere the provider accepts an idempotency key, the host MUST inject the effect identity or a documented deterministic derivative.
RetentionAn 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:

Facet fieldA2AMCP
Offeredversions[] as major.minorrevisions[] as dates
DefaultpreferredVersion, served when the peer names nonepreferredVersion
FloorminimumVersionminimumRevision
FreshnessrefreshedAtrefreshedAt
Profile idsa2a-<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.

Not a turn in a transcript.

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.