We recently open sourced tapes and introduced cassettes,
a powerful way to extend the tapes capabilities across
a shared API namespace.
You can capture your AI data, write a custom cassette with bespoke capabilities, and serve
that functionality as part of the tapes API on v1/cassettes/.... We use open standards
and OpenAPI to power all of this making it an ideal way to integrate with your
tooling and your platform. In this blog post, let’s take
a deep look at how this all works, how you can build a cassette, and what capabilities
this can unlock for you!
Functionality
Let’s build a very simple cassette that returns the user prompt for a given session.
You can find this example in the tapes GitHub repo!
Every cassette must serve an API (which then gets re-served on tapes’s proxy API via /v1/cassettes/...).
We also need to serve an OpenAPI specification on our cassette API that describes what exactly that API is and how it functions:
this is the main contract with the tapes API server and allows for us to provide
important metadata downstream about our cassette microservice.
This is also the main mechanism for how clients (like tapesctl) discover bespoke
capabilities on cassettes for a given tapes server.
These mechanisms are the backbone of how functionality in tapes can be
extended to fit your needs.
First, let’s implement this in an API like so:
GET /api/prompt/{session_id}
where session_id is the identifier in tapes of a captured session.
The tapes session API is served at v1/sessions/ and sessions can be listed
with the tapesctl sessions list command.
The Python code for this is relatively simple. We can assume we have access to the tapes
server API from the cassette in order to use the v1/traces API:
response = client.get(
f"{tapes_base_url}/v1/traces",
params={"session_id": session_id},
)
for trace in response.json().get("items", []):
if prompt := trace.get("user_prompt"):
return {"session_id": session_id, "prompt": prompt}
This will query the data for that session ID, iterate the captured session turns, and
returns the first user_prompt it sees.
Next, we need to describe this API using OpenAPI in a root level /openapi endpoint:
GET /openapi
There is a wealth of tooling around OpenAPI and lots of options for you to choose from given your selected framework, language, and environment. In our example here, it’s easy enough to hard code the OpenAPI JSON but for a more robust solution, I recommend picking an HTTP framework and middleware that can dynamically generate the OpenAPI specification based on your code.
There are a few important bits in the cassette’s OpenAPI spec worth calling out: first,
the metadata and x-tapes-cassette extension:
{
"openapi": "3.0.3",
"info": {
"title": "Prompt Cassette",
"description": "Returns the first captured user prompt for a tapes session.",
"version": "0.0.1"
},
"x-tapes-cassette": {
"kind": "cassette/v1alpha1",
"cassette": {
"name": "prompt",
"version": "0.0.1",
"display_name": "Prompt",
"description": "Returns the first captured user prompt for a tapes session.",
"license": "Apache-2.0",
"homepage": "https://github.com/papercomputeco/prompt-cassette",
"image": "tapes/prompt-cassette:0.0.1",
"port": 9999
},
"depends": {
"core": "v1",
"views": []
},
"api": {
"health": "/ping",
"openapi": "/openapi",
"prefix_path": "api"
},
"config": [
{
"key": "tapes_base_url",
"type": "string",
"default": "http://127.0.0.1:8081",
"description": "Base URL of the tapes core API."
}
]
}
}
The x-tapes-cassette is the cassette’s manifest data that the tapes API server
will use when discovering and admitting the running cassette. It contains important information about
what version of tapes the cassette needs, what tables it reads, how it runs, how it’s been configured,
and what APIs it provides.
Importantly, it advertises the prefix path tapes will trim when proxying this API
as well as where the “healthy” endpoint is and where the OpenAPI spec is served from.
Next, in the actual API route definition, we describe the actual implementation:
"/api/prompt/{session_id}": {
"get": {
"operationId": "get",
"parameters": [
{
"in": "path",
"name": "session_id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {
"prompt": {
"type": "string"
},
"session_id": {
"type": "string"
}
},
"required": [
"prompt",
"session_id"
],
"type": "object"
}
}
},
"description": "The prompt"
}
}
}
}
This is a simple descriptor of how our /api/prompt/{session_id} capabilities
work and will be re-served on the tapes API.
Running it
Now that we have implemented the cassette’s capabilities, we can run the stack to see it in action.
First, we need to stand up a Postgres that the tapes server can talk to and manage.
We provide the necessary Postgres extensions and capabilities in a bundled image
you can run with Docker:
docker run --rm --name tapes-postgres \
-e POSTGRES_DB=tapes \
-e POSTGRES_USER=tapes \
-e POSTGRES_PASSWORD=tapes \
-p 5432:5432 \
public.ecr.aws/g4e5l3z3/papercomputeco/postgres:17.7-pgduckdb-1.1.1
We can then run our cassette via Python which serves its API on port 9999:
python main.py
Next, we can start tapes by running:
tapes serve \
--postgres postgres://tapes:tapes@localhost:5432/tapes?sslmode=disable \
--cassettes localhost:9999/openapi
The cassettes flag is what signals to tapes which OpenAPI spec to scrape
and serve on its reverse proxy. In the logs, we can see that the prompt cassette
is “admitted” via this log line:
time=2026-08-25T12:24:20.952Z level=INFO msg="admitted cassette OpenAPI source" source=http://prompt:9999/openapi cassette=prompt
End to end, we can now see that the cassette is being served on the tapes API
via v1/cassettes/prompt/... and is discoverable on the tapes OpenAPI spec on /openapi
and the admitted cassette manifest at /v1/cassettes.
This results in tapesctl automatically discovering and surfacing these capabilities:
tapesctl cassette prompt get 550e8400-e29b-41d4-a716-446655440000
{
"prompt": "Describe to me how tapes cassettes work?",
"session_id": "550e8400-e29b-41d4-a716-446655440000"
}
Running all these APIs and services is a bit cumbersome. Be sure to check out
the Docker Compose files in our examples
that demonstrate bringing up the whole
stack locally with a single docker compose up.
MCP
The tapes API server also includes an MCP server where cassette capabilities as tools
can be configured. All we need to do to add an MCP tool for our cassette is define
an x-tapes-mcp extension on a new POST route. This new route can have same capabilities
and internal implementation as the GET, but takes a session_id in an application/json body
as part of the request body:
"/api/prompt/get": {
"post": {
"operationId": "getPromptTool",
"summary": "Get a session's first user prompt",
"tags": [
"prompt"
],
"x-tapes-mcp": {
"name": "get_prompt",
"annotations": {
"readOnlyHint": true,
"idempotentHint": true,
"openWorldHint": false
}
},
// The API schema definition for POST /api/prompt/get
// which gets turned into a tool call args
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"session_id"
],
"properties": {
"session_id": {
"type": "string",
"format": "uuid"
}
}
}
}
}
}
// etc. etc. the response body definition
}
}
We can see this MCP tool materialize on the MCP server once we refresh the
servers and tapes re-admits the cassette:
{
"tools": [
{
"name": "prompt.get_prompt",
"title": "Get a session's first user prompt",
"description": "Get a session's first user prompt",
"inputSchema": {
"type": "object",
"properties": {
"session_id": {
"format": "uuid",
"type": "string"
}
},
"required": [
"session_id"
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false
},
"annotations": {
"readOnlyHint": true,
"idempotentHint": true,
"openWorldHint": false
}
}
]
}
An agent can then automatically discover, take advantage of, and utilize these capabilities
from the tapes MCP server.
Fin
Just a reminder that a cassette doesn’t run inside tapes and doesn’t need to share its implementation language or deployment model. As long as it satisfies the cassette contract, tapes can discover and expose it. That gives us room to move capabilities like search and skills out of the core without making them second-class features. This is why we’re very excited to see what people build with cassettes. Our team has already
built some very interesting things that extend, modularize, and enhance AI data
captured through tapes.
Read more about how cassettes work and how you can build your own on our docs.
The sky’s the limit with cassettes: if you can dream it up with your AI data, you can do it with cassettes.
Start with paper
Turn every session into knowledge at team scale.
