The Model Context Protocol (MCP) has made it far simpler to expose backend capabilities to autonomous
agents. Its spread across distributed environments, though, introduces a potential architectural
bottleneck.
In an ecosystem of many MCP servers, each exposing tens — or even hundreds — of tools, the agent
runtime has to manage discovery of and access to those distributed capabilities. When the full
catalog is loaded into the LLM's context, a tool/context bloat appears: the model must process an
ever-growing amount of metadata just to decide which tool best resolves each request — driving up
token consumption, latency and inference cost, and compounding the complexity of tool routing and
orchestration.
The fix is to consolidate discovery and decouple it from execution: a centralized capability
catalog that gives the agent a unified, semantically consistent view of the available tools,
abstracting away their physical implementation and the concrete MCP server that ultimately runs the
operation. Separating a tool's semantic interface from its execution mechanism is the foundational
principle for regaining control over the scalability, governance and evolution of the agent ecosystem.
In answer to these scalability, governance and control challenges, the OpenTool Specification
(OTS) is a declarative, language-agnostic contract that acts as the single source of truth for
agent-facing capabilities. It standardizes which tools, resources and prompts exist, defines the
schemas they accept and return, and establishes the topology that maps those interfaces to their
execution mechanisms — MCP servers included. In short, OTS separates three concerns that usually stay
coupled: what capability is offered, how it is discovered, and where/how it is executed.
OTS describes, in a natural and technology-agnostic language, the set of tools and capabilities that
an organisation exposes to agents. It is a structured specification that can be read by both humans
and machines to understand what a service does, without requiring access to the source code, documentation,
or runtime traffic.
A document declares three kinds of thing, and the same fields describe all three:
|
|
| tools |
what the model decides to call |
| resources |
what the application attaches to the model's context, by address |
| prompts |
templates the user picks, filled with arguments |
The point of writing one is that the description is operational, not documentary. The same file
says what the agent sees and how to reach the real service — so one document is the contract, the
translation, the validation, the test fixture and the governance rule. An Example:
ots: "1.0.0"
info:
title: "BotiBank Core Tools"
version: "1.2.0"
servers: # where the tools are served from
- protocol: streamable-http
url: "https://localhost:8243/botibankmcp/1.0/mcp"
security: # what every tool demands, unless it says otherwise
- AgentOAuthClientCredentials: []
tools:
consultar_cuentas:
name: "consultar_cuentas" # must equal the key, or the document is refused
summary: "Consultar cuentas de un cliente"
description: "Lista las cuentas bancarias de un cliente dado su ID."
aiHints:
whenToUse: "Úsalo para verificar el saldo de las cuentas de un usuario."
backend: # the agent's vocabulary is not your backend's
tool: "get_cuentas"
arguments:
clienteId: "cliente_id" # their name ← yours
cache: { ttlMs: 300000 }
idempotent: true # may be retried. Absent means no
inputSchema:
type: "object"
required: [ "cliente_id" ]
additionalProperties: false
properties:
cliente_id: { type: "string", description: "ID del cliente (UUID)" }
outputSchema: # a list is declared wrapped, never a bare array
type: "object"
properties:
cuentas:
type: "array"
items:
type: "object"
properties:
cuentaId: { type: "string", x-ots-mask: "last4" } # •••-122 before the agent sees it
saldo: { type: "number" }
examples: # it answers before its backend exists
default:
input: { cliente_id: "71992c72-cc1c-4c5a-8b50-9ee4fb6c214d" }
value:
- { cuentaId: "CTA-122", saldo: 1800.50, tipo: "Corriente" }
cliente_inexistente:
input: { cliente_id: "00000000-0000-0000-0000-000000000000" }
error: { code: -32602, message: "Cliente no encontrado" }
server: # this tool alone is answered by a query
- protocol: database
url: "mariadb://127.0.0.1:3306/botibank?getCuentasByCliente"
transferir_dinero:
name: "transferir_dinero"
summary: "Transferencia entre cuentas"
backend:
tool: "post_cuentas_by_cuentaId_transferir"
arguments:
cuentaId: "cuenta_origen"
"requestBody.monto": "monto" # dotted paths reach into the body
idempotent: false # never retried: it moves money
security: # more than the agent's own credential
- UserDelegationOAuth: [ "bank:transfer:execute" ]
errors: # what a status means, in words an agent can act on
"402":
rpcCode: -32000
description: "Saldo insuficiente."
aiAction: "Dile que la cuenta origen no tiene fondos."
resources:
politica_privacidad:
title: "Política de privacidad"
uri: "ots://botibank/politica-privacidad"
mimeType: "text/markdown"
cache: { ttlMs: 3600000 }
example: "# Política de privacidad v2.1"
movimientos_cuenta:
title: "Movimientos"
uriTemplate: "ots://botibank/cuentas/{cuentaId}/movimientos" # parameterised
mimeType: "application/json"
example:
- { date: "2026-09-01", amount: -50.00, concept: "Supermercado" }
prompts:
resumen_mensual:
title: "Resumen mensual"
arguments:
- { name: "cuenta_id", description: "La cuenta a resumir", required: true }
example:
messages:
- role: "user"
content: { type: "text", text: "Analiza la cuenta {cuenta_id}…" }
components:
schemas:
Cuenta:
type: "object"
properties:
cuentaId: { type: "string" }
tipo: { type: "string", enum: [ "Corriente", "Ahorro" ] }
securitySchemes:
AgentOAuthClientCredentials:
type: oauth2
flows:
clientCredentials:
tokenUrl: "https://localhost:8243/oauth2/token"
scopes: { "mcp:invoke": "Llamada básica a herramientas" }
UserDelegationOAuth:
type: oauth2
flows:
authorizationCode:
authorizationUrl: "https://localhost:8243/oauth2/authorize"
tokenUrl: "https://localhost:8243/oauth2/token"
scopes: { "bank:transfer:execute": "Aprobación de movimientos" }
What this OTS Visual Designer extension does with it
Visual Map. An interactive visual representation of your entire OTS ecosystem, allowing you
to easily explore tools, resources, prompts, and the backend servers that implement them. You can
quickly modify existing components, add new ones, and understand the relationships between
them.
Embedding OTS Gateway. Built-in gateway functionality that automatically exposes the OTS specification,
allowing agents and consumers to discover and interact with available capabilities through a standardised interface. Every request can be visualised in real time through an interactive execution map, highlighting the exact path taken across tools, resources, prompts, and backend services. This provides instant observability and simplifies debugging, monitoring, and development.
To enable the embedded gateway, configure the ots.gatewayPath setting to point to the appropriate gateway binary for your machine architecture. This can be configured through the extension settings or added to .vscode/settings.json as "ots.gatewayPath": "/Users/pablosa/src/ots/otsgw", allowing each project to use the gateway binary it requires. Before configuring the path, download the appropriate binary distribution for your operating system and architecture from Open Tool Specification GitHub Repository.
Debugger. An integrated debugging experience that provides end-to-end visibility into execution flows,
requests, responses, and backend interactions, making it easy to understand, troubleshoot, and optimise agent
workflows. Developers can also create and manage test cases for each component, allowing capabilities
to be validated independently and ensuring expected behaviour as specifications evolve.
To go back to the text at any moment: Open as text, top left. Both views edit the same file and
stay in sync.
License
Copyright (c) 2026, Pablo Sagarna. (https://github.com/psagarna) All Rights Reserved.
Licensed under the Apache License, Version 2.0. See LICENCE for details.
Built by Pablo Sagarna