Example 3: let an AI agent operate the library over MCP
Connect an MCP client to the hosted endpoints, let the agent register a member, lend a book and extend the loan, put a limited volunteer profile next to it, and trace every change in the audit trail and record history.
The lending library from example 1, with the automation from example 2, is now handed to an AI agent. The agent works through the Model Context Protocol: it calls the same API, under the same permissions and validation rules, as a person would — and it leaves the same trail.
Environment. Every call on this page was made against the hosted
examplesdemo tenant (app.kelta.io/examples) with thekeltaCLI 1.0.1427 (gitb3a8869) on 2026-10-05; the output blocks are what came back, with JSON pretty-printed and wide tables trimmed where noted. The platform does not expose a server build endpoint; the cluster’s download service reported 1.0.1436 as the newest published release. No real token appears on this page. IDs will differ in your tenant.
1. Connect the client#
Setup — creating a token, choosing a client, verifying — is covered in Connecting Claude Code, Claude Desktop, Cursor and others. For this tenant the two hosted endpoints are:
| Toolset | URL |
|---|---|
| admin (build and change metadata) | https://api.kelta.io/examples/mcp/admin |
| user (work with records) | https://api.kelta.io/examples/mcp/user |
Put the token in an environment variable rather than in the configuration file:
export KELTA_API_KEY=klt_... # a personal access token — see Personal access tokens
Claude Code
claude mcp add --transport http kelta-user \
https://api.kelta.io/examples/mcp/user \
--header "Authorization: Bearer $KELTA_API_KEY"
JSON-configured clients (.mcp.json and similar; clients that expand environment variables substitute
${KELTA_API_KEY}, otherwise paste the token):
{
"mcpServers": {
"kelta-admin": {
"type": "http",
"url": "https://api.kelta.io/examples/mcp/admin",
"headers": { "Authorization": "Bearer ${KELTA_API_KEY}" }
},
"kelta-user": {
"type": "http",
"url": "https://api.kelta.io/examples/mcp/user",
"headers": { "Authorization": "Bearer ${KELTA_API_KEY}" }
}
}
}
The CLI prints the same shape for the profile you are logged in with — tokens left as a placeholder:
kelta mcp install generic --direct --toolset admin
{
"mcpServers": {
"kelta-default-admin": {
"type": "http",
"url": "https://api.kelta.io/examples/mcp/admin",
"headers": {
"Authorization": "Bearer <YOUR_PAT>"
}
}
}
}
# Replace <YOUR_PAT>; tokens come from: kelta token create --name mcp
Without a token the endpoint refuses to talk at all:
curl -sS -o /dev/null -w '%{http_code}\n' -X POST https://api.kelta.io/examples/mcp/admin \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
401
The agent’s session below was driven through kelta mcp serve, the CLI’s stdio bridge
(Local MCP bridge), which forwards every record and metadata tool to the hosted endpoints
above. Each step shows the tool call the agent made and the tool result it got back.
2. The agent does the desk work#
Check the connection.
{ "name": "ping", "arguments": {} }
pong (user)
Register a new member.
{ "name": "create_record", "arguments": { "collection": "members",
"attributes": { "full_name": "Grace Hopper", "email": "grace@example.org" } } }
{
"data": {
"attributes": {
"createdAt": "2026-10-05T01:02:26.576891108Z",
"full_name": "Grace Hopper",
"updatedBy": "13de168a-ab36-4ec0-a811-647e447f1827",
"recordTypeId": null,
"createdBy": "13de168a-ab36-4ec0-a811-647e447f1827",
"email": "grace@example.org",
"updatedAt": "2026-10-05T01:02:26.576891108Z"
},
"id": "4c846c6f-910a-4ef0-8e37-ae323157711a",
"type": "members"
}
}
Lend her Kindred. Before this call the administrator turned on record versioning for loans
(kelta collections update loans --data '{"trackHistory":true}'), so every change from here on is kept as a
version — see Field history and record versions.
{ "name": "create_record", "arguments": { "collection": "loans",
"attributes": { "book": "3232b8a4-efd0-4dc6-a9cb-cf0caffa5e8a", "member": "4c846c6f-910a-4ef0-8e37-ae323157711a",
"loaned_on": "2026-10-05", "due_on": "2026-10-19" } } }
{
"data": {
"relationships": {
"book": {
"data": {
"type": "books",
"id": "3232b8a4-efd0-4dc6-a9cb-cf0caffa5e8a"
}
},
"member": {
"data": {
"type": "members",
"id": "4c846c6f-910a-4ef0-8e37-ae323157711a"
}
}
},
"attributes": {
"createdAt": "2026-10-05T01:03:38.165774889Z",
"updatedBy": "13de168a-ab36-4ec0-a811-647e447f1827",
"recordTypeId": null,
"createdBy": "13de168a-ab36-4ec0-a811-647e447f1827",
"book": "3232b8a4-efd0-4dc6-a9cb-cf0caffa5e8a",
"member": "4c846c6f-910a-4ef0-8e37-ae323157711a",
"returned_on": null,
"due_on": "2026-10-19",
"loaned_on": "2026-10-05",
"updatedAt": "2026-10-05T01:03:38.165774889Z"
},
"id": "a959d627-094b-4c11-a316-70edd965a7ab",
"type": "loans"
}
}
The Sync book status flow from example 2 picked the new loan up and marked the book ON_LOAN — the agent did not
have to know the flow exists.
Extend the loan by a week. update_record is a PATCH: only the attributes sent change.
{ "name": "update_record", "arguments": { "collection": "loans", "id": "a959d627-094b-4c11-a316-70edd965a7ab",
"attributes": { "due_on": "2026-10-26" } } }
{
"data": {
"relationships": {
"book": {
"data": {
"type": "books",
"id": "3232b8a4-efd0-4dc6-a9cb-cf0caffa5e8a"
}
},
"member": {
"data": {
"type": "members",
"id": "4c846c6f-910a-4ef0-8e37-ae323157711a"
}
}
},
"attributes": {
"createdAt": "2026-10-05T01:03:38.165775Z",
"updatedBy": "13de168a-ab36-4ec0-a811-647e447f1827",
"recordTypeId": null,
"createdBy": "13de168a-ab36-4ec0-a811-647e447f1827",
"book": "3232b8a4-efd0-4dc6-a9cb-cf0caffa5e8a",
"member": "4c846c6f-910a-4ef0-8e37-ae323157711a",
"returned_on": null,
"due_on": "2026-10-26T00:00:00.000Z",
"updatedAt": "2026-10-05T01:03:50.862959Z",
"loaned_on": "2026-10-05T00:00:00.000Z"
},
"id": "a959d627-094b-4c11-a316-70edd965a7ab",
"type": "loans"
}
}
Validation rules apply to agents exactly as to people. Note the
known issue in example 1: a PATCH that
sends only due_on is not checked against loaned_on, so an agent that moves a due date should send both.
3. Draw the boundary: a volunteer profile#
An agent acts with the permissions of the user whose token it holds. The token used above belongs to an administrator, which is right for building the app and wrong for running the desk. The desk role should be able to record loans and nothing else. Create a profile for it, then a user — run as an administrator, with the CLI:
kelta records create profiles --data '{"name":"Library Volunteer","description":"Can record loans; cannot change the catalogue"}'
{
"id": "94688ab9-c366-4600-b8fe-c6d7f8be1b07",
"isSystem": false,
"createdAt": "2026-10-05T01:00:16.877308806Z",
"updatedBy": "13de168a-ab36-4ec0-a811-647e447f1827",
"createdBy": "13de168a-ab36-4ec0-a811-647e447f1827",
"name": "Library Volunteer",
"tenantId": "5770f937-2fd9-4c7c-ac79-f3a447a82c34",
"description": "Can record loans; cannot change the catalogue",
"updatedAt": "2026-10-05T01:00:16.877308806Z"
}
Grant API_ACCESS (so the token works at all), read-only access to books and members, and create/read/edit on
loans (Profiles and permissions):
P=94688ab9-c366-4600-b8fe-c6d7f8be1b07
kelta records create profile-system-permissions --quiet \
--data '{"profileId":"'$P'","permissionName":"API_ACCESS","granted":true}'
kelta records create profile-object-permissions --quiet \
--data '{"profileId":"'$P'","collectionId":"2cf0aab2-2311-4746-b890-dfedf5984418","canCreate":false,"canRead":true,"canEdit":false,"canDelete":false,"canViewAll":false,"canModifyAll":false}'
kelta records create profile-object-permissions --quiet \
--data '{"profileId":"'$P'","collectionId":"842948da-0838-4633-907a-118376d136c9","canCreate":false,"canRead":true,"canEdit":false,"canDelete":false,"canViewAll":false,"canModifyAll":false}'
kelta records create profile-object-permissions --quiet \
--data '{"profileId":"'$P'","collectionId":"52da9a1c-dc69-49a1-ada5-a30829e370af","canCreate":true,"canRead":true,"canEdit":true,"canDelete":false,"canViewAll":false,"canModifyAll":false}'
dc8967cf-d60e-4f5e-acd2-8235b98444b1
e9ba23e7-4ebd-440e-8d8b-0f13eacad0b3
c5791289-b7aa-4c57-9112-c4c2bab8c792
ae1cd3d9-46e5-459d-9a50-8285a941116c
(The collection ids are books, members and loans from example 1.) Then the user:
kelta records create users --data '{"email":"volunteer@example.org","firstName":"Sam","lastName":"Volunteer","profileId":"94688ab9-c366-4600-b8fe-c6d7f8be1b07","status":"ACTIVE","userType":"INTERNAL"}'
{
"id": "849f1ddb-8e28-44f6-921d-b06bf5e0ef55",
"lastName": "Volunteer",
"settings": {},
"updatedBy": "13de168a-ab36-4ec0-a811-647e447f1827",
"timezone": "UTC",
"managerId": null,
"locale": "en_US",
"loginCount": 0,
"createdAt": "2026-10-05T01:00:25.752640571Z",
"firstName": "Sam",
"lastLoginAt": null,
"createdBy": "13de168a-ab36-4ec0-a811-647e447f1827",
"profileId": "94688ab9-c366-4600-b8fe-c6d7f8be1b07",
"tenantId": "5770f937-2fd9-4c7c-ac79-f3a447a82c34",
"mfaEnabled": false,
"userType": "INTERNAL",
"email": "volunteer@example.org",
"updatedAt": "2026-10-05T01:00:25.752640571Z",
"status": "ACTIVE",
"username": null
}
An agent configured with a token belonging to Sam can register loans and read the catalogue. If it tries to change
a book — update_record on books, or kelta records update books <id> … — the write is checked against the
Library Volunteer profile, which has canEdit: false on books, and refused before it reaches the data
(how access is evaluated); the refusal comes back
in the error envelope, and an MCP client sees it as a tool error.
Not captured here. The walkthrough’s automation is deliberately not allowed to mint or hold a token for any user other than the tenant administrator, so this page does not show Sam’s refused write or its audit entry. To see it yourself, sign in as a limited user, create a token under Profile → API tokens, point the client configuration above at it and ask the agent to mark a book
LOST.
4. Follow the trail#
Everything above left a record.
Setup audit trail — the profile, its permissions and the user (kelta audit setup shows the same entries with
their before/after values; this query trims them to the essentials):
kelta records list setup-audit-entries \
--filter timestamp.gte=2026-10-05T01:00:00Z --filter timestamp.lt=2026-10-05T01:00:30Z \
--sort timestamp --fields timestamp,action,section,entityType,entityName,userId --output table
timestamp action section entityType entityName userId
--------------------------- ------- -------- ------------------ --------------------- ------------------------------------
2026-10-05T01:00:16.888321Z CREATED Profiles profile Library Volunteer 13de168a-ab36-4ec0-a811-647e447f1827
2026-10-05T01:00:24.635039Z CREATED Profiles system-permissions 13de168a-ab36-4ec0-a811-647e447f1827
2026-10-05T01:00:24.927883Z CREATED Profiles object-permissions 13de168a-ab36-4ec0-a811-647e447f1827
2026-10-05T01:00:25.211287Z CREATED Profiles object-permissions 13de168a-ab36-4ec0-a811-647e447f1827
2026-10-05T01:00:25.482670Z CREATED Profiles object-permissions 13de168a-ab36-4ec0-a811-647e447f1827
2026-10-05T01:00:25.771011Z CREATED Users user volunteer@example.org 13de168a-ab36-4ec0-a811-647e447f1827
(Table trimmed: the id, createdAt, updatedBy, createdBy and updatedAt columns are removed.)
Record history — the agent’s create and update of the loan:
kelta records list record-versions --filter recordId=a959d627-094b-4c11-a316-70edd965a7ab \
--sort versionNumber --output table
versionNumber changeType changedFields changeSource changedBy changedAt
------------- ---------- -------------------------------------- ------------ ------------------------------------ ---------------------------
1 CREATED ["book","member","loaned_on","due_on"] UI 13de168a-ab36-4ec0-a811-647e447f1827 2026-10-05T01:03:38.186491Z
2 UPDATED ["due_on"] UI 13de168a-ab36-4ec0-a811-647e447f1827 2026-10-05T01:03:50.872474Z
(Table trimmed to six of its columns; the full rows also carry a snapshot of the record after each change.)
changedBy is the token’s owner. changeSource reads UI on this release even though both writes came through
MCP, so do not rely on it to tell agent writes from human ones; use a dedicated user — and token — per agent
instead.
Flow runs — the loan the agent created started Sync book status; it is the last row of
kelta records list flow-executions --filter flowId=ba4ffdbc-55df-445b-98c3-3e2858f97fb0 --sort startedAt …
(same columns as in example 2):
7e32f5ac-7c25-4fea-9adb-b663d4719d76 a959d627-094b-4c11-a316-70edd965a7ab MarkOnLoan 2026-10-05T01:03:38.255974Z COMPLETED
Where next#
- MCP tools — every tool in the admin and user toolsets
- Personal access tokens — expiry, revocation, minting on a service user’s behalf
- Audit logs — the security audit log, setup audit trail and login history