Tools
Portable MCP and OpenAPI Tool definitions.
A Tool is either an MCP server or an OpenAPI (Swagger) tool. Its address and behaviour are part of the Package. Whatever authenticates to it is not: that's a Slot the installer fills with their own account.
For a complete, valid project with one Tool of each kind, run npx @elizon/cli example tool. It
prints an MCP Tool connected with OAuth, an MCP Tool that takes an API key as a header, an OpenAPI
Tool, the credential Slot behind each one, and an Agent that binds their operations. It needs no
account and no network.
MCP
toolType: mcp
name: Practice Calendar
identifier: practice-calendar
url: https://calendar.example.com/mcp
authType: oauth_manual
credentialSlot: calendar-access
headers:
- name: X-Elizon-Package
valueType: fixed
value: triage
requestTimeoutMs: 30000
sseReadTimeoutMs: 60000| Field | Meaning |
|---|---|
name | Display name. |
identifier | The Tool's identifier, unique within the installing project. A clash is offered as reuse-or-rename. |
url | The MCP server's address, part of what the Package is. |
authType | dcr, oauth_manual or none. |
credentialSlot | The credential Slot that authenticates it for oauth_manual, otherwise null. |
headers[] | Extra headers: valueType: fixed with a value, or valueType: credential with a slot. |
requestTimeoutMs, sseReadTimeoutMs | Timeouts in milliseconds. |
An API key sent as a header
An API key isn't an OAuth flow, so use authType: none and send the key as a credential header.
Its value is resolved from the installer's Secrets vault at call time:
toolType: mcp
name: Spend Ledger
identifier: spend-ledger
url: https://ledger.example.com/mcp
authType: none
credentialSlot: null
headers:
- name: X-Ledger-Key
valueType: credential
slot: ledger-access
requestTimeoutMs: 15000
sseReadTimeoutMs: 30000A valueType: fixed header is carried into the .pkg exactly as written, so validate and build
warn with TOOL_FIXED_HEADER_SECRET when its value looks like a secret: it starts with an auth
scheme (Bearer …, Basic …), contains a known API-key prefix (sk-…, ghp_…, AKIA…), or is
set under a credential-bearing header name such as Authorization, Cookie or X-Api-Key. The
Package still builds; move the key to a credential header like the one above. A vault: or
{{secret.…}} value is refused instead, because a Package never carries a secret pointer.
OpenAPI (Swagger)
toolType: swagger
name: Orders API
spec: { … the full OpenAPI 3.x document … }
authType: header # none, header or query_param
credentialSlot: orders-api-key| Field | Meaning |
|---|---|
spec | The complete OpenAPI 3.x document. |
actions | Optional — leave it out. build derives the operations Agents can call from spec. |
authType | none, header or query_param. |
credentialSlot | The credential Slot holding the key, or null. |
Binding an Agent to an operation
Name the operation by its operationId from the spec:
tools:
- tool: tools.orders
toolType: swagger
actionId: getOrder # the spec's operationId
enabled: true
config: {}npx @elizon/cli build derives each operation's action exactly as Studio does when you import
the spec, and writes the action's actionId into the artifact. That actionId is the method in
lower case, _, then the path with every character other than a letter or a digit replaced by _
(the path keeps its case): getOrder on GET /orders/{id} becomes get__orders__id_, and on
GET /orders/{orderId} it becomes get__orders__orderId_. Binding by that actionId directly
works too, and is the only way to name an operation that has no operationId.
build refuses a binding that names neither an operationId nor an actionId of the Tool, and
lists the Tool's actionIds.
References in the spec
build resolves the spec's own references ($ref: '#/components/…') and carries the resolved spec
in the artifact, as Studio stores it. It never fetches anything, so a reference to another file or
URL is refused: bundle the spec into one self-contained document first.
Email addresses in the spec
Most third-party OpenAPI documents carry a contact address (info.contact.email). Inside the
embedded spec, validate and build report an address-shaped match as a
PORTABLE_MACHINE_CONTENT_EMAIL warning at its path, such as
tools.orders.spec.info.contact.email, instead of refusing it. A vendor's contact, or a match that
is not an address such as logo@2x.png, may stay; a person's address must go. A vault: /
{{secret.…}} reference in the spec is still refused. Everywhere else in the Tool file an address
is refused (see PORTABLE_PERSONAL_DATA_LEAK).
Listing actions yourself
You rarely need to. If you do, each action must be an operation of spec, in the shape Studio
derives:
actions:
- actionId: get__orders__id_ # GET /orders/{id}
name: getOrder
description: Fetch one order.
method: GET
path: /orders/{id}
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
requestBody: null| Field | Meaning |
|---|---|
actionId | What an Agent's tools[].actionId names. Unique within the Tool. |
name | What the model sees the action called. |
method | An HTTP method. With path, it must be an operation that spec actually has. |
path | The spec's path, starting with /. |
parameters | Optional. The operation's parameters (name, in, required), used to build the request. |
requestBody | Optional. { required, contentType } when the operation takes a body, otherwise null. |
A listed actions is carried as written: build checks it but does not derive anything.
Credentials
Declare the credential as a Slot and point the Tool at it:
slots:
- id: ledger-access
type: credential
scheme: api_key
required: true
boundTo: [tools.ledger]
description: An API key for your ledger with read access.At install the operator binds it to a Secret, and the platform writes the reference into the Tool.
A Tool whose credential was deferred is finished later with
packages bind or Bind in Studio.
OAuth Tools (dcr, oauth_manual) are connected in the Studio wizard, which also runs a
connectivity test for each Tool.
What validate and build refuse
validate checks the manifest, the references between objects and each Tool's credential
settings. build runs validate first and also parses every Tool file, so the rest of what is
wrong inside a Tool is reported by build. Run both after each change.
Each refusal below comes with the smallest change that triggers it, written against the project
npx @elizon/cli example tool prints. The title of each snippet names the file to change. Only the
keys shown change; the rest of the file stays as the example has it.
UNRESOLVED_LOCAL_REFERENCE
Refused by validate and build. An Agent names a Tool the manifest does not declare. The hint
lists the Tools that are declared.
tools:
- tool: tools.order
toolType: swagger
actionId: getOrder
enabled: true
config: {}PORTABLE_UNDECLARED_REFERENCE
Refused by validate and build. A Tool's credential is always a credential Slot that the
installer binds, at install or later. A Package never carries a key or a secret name. The
diagnostic's path names the Tool and the field, such as tools.orders.credentialSlot or
tools.ledger.headers[0].slot, and the message contains the one export gives for the same mistake.
A credentialSlot that names no declared Slot:
credentialSlot: no-such-slotA credentialSlot that names a Slot of another type (here the llm Slot):
credentialSlot: reasoningA credential header whose slot is not a declared credential Slot:
headers:
- name: X-Ledger-Key
valueType: credential
slot: nopecredentialSlot: null on a Tool whose authentication needs a credential: an oauth_manual MCP
Tool, or a header / query_param OpenAPI Tool. Without a Slot it would install with nothing for
the operator to supply:
credentialSlot: nullcredentialSlot: nullauthType: query_param
credentialSlot: nullPORTABLE_DEFINITION_INVALID for a secret reference
Refused by validate and build. A value in a Tool file is a vault reference: it starts with
vault:, or contains {{secret.…}}. A reference resolves against whichever Organization reads it,
so the install refuses the Package. Use a credential header and a credential Slot instead. The
path names the field, such as tools.ledger.headers[0].value, and every reference in the file is
reported:
headers:
- name: X-Ledger-Key
valueType: fixed
value: vault:ledger_keyheaders:
- name: X-Ledger-Key
valueType: fixed
value: '{{secret.LEDGER}}'PORTABLE_PERSONAL_DATA_LEAK
Refused by validate and build. A string in the Tool file holds an email address, such as a
fixed header's value or an action's description. The Tool file travels with the Package, so
remove the address (or, if it isn't one, such as logo@2x.png, rename it). The path names the
field, such as tools.ledger.headers[1].value, and every address in the file is reported. See
Carried text.
An address-shaped match inside the embedded spec is a warning, not a refusal; see
Email addresses in the spec.
headers:
- name: X-Ledger-Key
valueType: credential
slot: ledger-access
- name: X-Ledger-Contact
valueType: fixed
value: ops@acme.comPORTABLE_DEFINITION_INVALID
Refused by build. The Tool file is not a valid portable Tool. The diagnostic's path names the
field, such as tools.ledger.authType.
A value the field does not take. An MCP authType is dcr, oauth_manual or none:
authType: api_keyA field a Tool does not have. There is nowhere in a Tool file to put a credential, so a credential written into one is refused rather than carried:
authConfig:
token: replace-meAn identifier-shaped value (a UUID). An id belongs to one Organization and means nothing in another:
headers:
- name: X-Ledger-Account
valueType: fixed
value: 3f2b8c1e-4d5a-4b6c-8d7e-9f0a1b2c3d4eA header in the format Studio stores (expression). In a Package a header is fixed or
credential:
headers:
- name: X-Ledger-Key
valueType: expression
value: ledger-keyA spec that is not OpenAPI 3.x, when actions is left out (build can only derive actions from
3.x):
spec:
swagger: '2.0'
info:
title: Orders API
version: 1.0.0
paths: {}A spec with a $ref to another file or a URL. build never fetches anything:
spec:
openapi: 3.0.3
info:
title: Orders API
version: 1.0.0
paths:
/orders:
get:
operationId: listOrders
responses:
'200':
$ref: ./responses.yaml#/OrdersA listed action that is not an operation of the spec:
actions:
- actionId: delete__orders__orderId_
name: deleteOrder
method: DELETE
path: /orders/{orderId}PORTABLE_UNRESOLVED_BINDING
Refused by build. An Agent binds an OpenAPI Tool by a name that is neither an operationId in the
spec nor one of the Tool's actionIds. The message lists the Tool's actionIds.
tools:
- tool: tools.orders
toolType: swagger
actionId: cancelOrder
enabled: true
config: {}An MCP binding's actionId is the name of a tool the server lists, and build cannot check it: it
never connects to the server.
Refused when a Tool is exported from Studio or installed
These come from the same Tool code, at the two ends where a Tool meets a real Organization: exporting
a Tool from Studio into a Package, and installing a Package. Where a Tool file can make the same
mistake, validate and build refuse it first, as the sections above show.
| Code | Where | Smallest input |
|---|---|---|
PORTABLE_UNDECLARED_REFERENCE | Export | An oauth_manual MCP Tool, or a header / query_param OpenAPI Tool, with no credential Slot mapped to its authentication. |
PORTABLE_UNDECLARED_REFERENCE | Export | An Expression header (vault:ledger-key) with no credential Slot mapped to its secret. |
PORTABLE_UNDECLARED_REFERENCE | Export | A Slot mapped to the Tool that the Package does not declare, or declares with a type other than credential (an llm Slot, say). |
PORTABLE_UNDECLARED_REFERENCE | Export | A dcr MCP Tool with a Slot mapped to it. A dcr client registers itself when it connects, so it takes no credential from the operator. |
PORTABLE_ORG_IDENTIFIER_LEAK | Export | A fixed header whose value is a vault reference (vault:ledger-key) or an identifier-shaped value (a UUID). |
PORTABLE_UNRESOLVED_BINDING | Install | A credential Slot the Tool uses (its credentialSlot or a header's slot) that the operator neither binds to a secret nor defers. |
PORTABLE_DEFINITION_INVALID | Install | A Tool file with a vault reference: a value that starts with vault:, or a {{secret.…}} anywhere, such as the value of a fixed header. |
build does not yet check two things, so check them yourself before you build:
credentialSlotisnullfordcrandnone.buildaccepts a Slot there, and the installer is then asked for a credential nothing reads.validatedoes not check it because it is outside the scope of the credential checks above, and export already refuses it;vault:later in a value, such as a URL's query (?k=vault:x). No stage refuses it: notvalidate,build, export or install. Each treats only a value that starts withvault:as a vault reference, so never put a secret name anywhere in a Tool file.