A capability is the smallest describable ability of a system: typed, permission-checked, auditable, and equally usable by humans and agents alike. MCP-first does not start with screens, but with these capabilities. Only when the capability set is complete do the web app, mobile app, CLI, and agent interfaces emerge as clients on top.
The seven building blocks
MCP-first models every capability with seven structural elements.
Resources
Data and contexts that can be read. Resources provide the agent with prepared context, not raw database tables, but filtered, purpose-bound views of the system state.
Tools
Actions the system can execute. Every tool has a typed input schema, an output schema, an error schema, and an audit schema. A tool is what a button in the web app merely represents.
Prompts / Workflows
Pre-built flows that guide an agent through complex, multi-step processes. Workflows define which tools should be called in which order with what context, and where human input is expected.
Policies
Rules, permissions, protection levels, and approval processes. Policies decide who may see, call, and autonomously execute a capability. They are not a layer added afterward, they are part of the capability definition.
Audit Events
Every action generates an audit entry: who, what, when, with which approval, with what result. Audit events are not an optional operational extension, but a mandatory component of every capability.
Human Confirmation Gates
Mechanisms by which the agent must actively ask the user for confirmation before an action is executed. Confirmation gates are declared in the capability definition, not ad hoc in the UI.
Risk Metadata
Every capability carries a risk level: low, medium, high, critical, or
forbidden_for_ai. These metadata control discovery filtering, confirmation requirements,
and step-up auth, and are available machine-readably in the tool contract.
What every entity should offer
Regardless of the domain, every entity needs a consistent base set of capabilities.
-
entity.listReturn filtered list -
entity.searchFull-text search with scoping -
entity.getSingle entity with context -
entity.createCreate new entity -
entity.updateChange fields, idempotent -
entity.archiveDeactivate instead of delete -
entity.auditRetrieve change history -
entity.permissionsQuery effective permissions -
entity.relatedLoad related entities -
entity.recommended_next_actionsAgent-ready action suggestions
Action layer first
Every function is first implemented as an action in the central action layer. Only then does the respective interface adapter emerge.
Action: create_project
Used by:
Web app
Mobile app
MCP Tool
Worker
CLI
This means there is no duplicate implementation: when a web app button
calls create_project and an agent does the same, both use the same code,
the same validations, the same audit events.
Build capabilities once. Expose them everywhere.
The MCP server is an adapter
The MCP server contains no business logic. It is a thin adapter between the agent client and the action layer.
Its responsibilities:
- Discovery, which tools and resources exist in this context?
- Schema Exposure, typed description of inputs, outputs, and errors
- Auth Context Mapping, map client token to user and tenant context
- Policy Checks, may this agent call this tool in this context?
- Audit Logging, write execution events for every tool call
Typed inputs and outputs
Every tool defines four schemas:
| Schema | Purpose |
|---|---|
| Input Schema | What parameters does the tool expect, what types, what required fields? |
| Output Schema | What does the tool return on success? |
| Error Schema | What error codes are possible, permission_denied, confirmation_required, policy_violation? |
| Audit Schema | What is written into the audit event? |
No loose JSON structures. Agents, as well as compilers and tests, rely on these schemas. Untyped outputs force guessing logic on the client side.
Idempotency
Many tools should be idempotent: delivering the same result for the same request, without generating unnecessary duplicates.
create_download_link(fileId, expiresAt)
If a valid link for fileId with identical expiresAt already exists,
the existing link is returned, no second one is created. This reduces
agent errors on retries and makes workflows more robust against network interruptions.