Runtime Tools
Overview
Section titled “Overview”The upjack library provides 6 entity management operations. When using create_server() / createServer(), these are automatically registered as MCP tools per entity type (e.g., create_contact, get_contact, list_contacts). When using UpjackApp directly, they are available as methods.
By default, all operations are listed in tools/list. To control which tools are discoverable by the LLM, specify a tools array on the entity definition in the manifest. Hidden tools remain callable via tools/call. See the Manifest Reference for configuration.
The operations work on the entity definitions and schemas declared in the app’s upjack manifest. They are the primary interface through which agents (and custom code) interact with entity data.
Implementation Notes
Section titled “Implementation Notes”The current implementation uses in-memory file scanning for search (not a database index). Filters use flat key matching with comparison operators (not JSONPath). Singleton checking is not yet implemented. Immutable field protection is implemented: update operations silently strip id, type, version, created_at, and created_by from incoming data.
Tool Summary
Section titled “Tool Summary”| Tool | Purpose | Returns |
|---|---|---|
create_{entity} | Create a new entity | Created entity dict/object |
get_{entity} | Get a single entity by ID | Entity dict/object |
update_{entity} | Update an existing entity | Updated entity dict/object |
list_{plural} | Lightweight entity listing | Array of entity dicts/objects |
search_{plural} | Text and structured search | Array of matching entity dicts/objects |
delete_{entity} | Soft or hard delete an entity | Deleted entity dict/object |
MCP Tool Naming
Section titled “MCP Tool Naming”When using create_server() / createServer(), tools are named per entity type:
| Entity Type | Plural | Tools Generated |
|---|---|---|
contact | contacts | create_contact, get_contact, update_contact, list_contacts, search_contacts, delete_contact |
deal | deals | create_deal, get_deal, update_deal, list_deals, search_deals, delete_deal |
create_entity
Section titled “create_entity”Create a new entity of a given type.
app.create_entity(entity_type: str, data: dict, created_by: str = "agent") -> dictapp.createEntity(entityType: string, data: Record<string, unknown>, createdBy?: string): EntityRecordMCP Tool
Section titled “MCP Tool”create_{entity}(data: dict) -> dictParameters
Section titled “Parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
entity_type / entityType | string | Yes | — | Entity type name (e.g., "contact"). |
data | object | Yes | — | Domain-specific fields for the entity. |
created_by / createdBy | string | No | "agent" | Origin of the entity (user, agent, system, ingestion, schedule). |
Behavior
Section titled “Behavior”- Validate entity type: Confirm entity type exists in the app’s entity definitions.
- Generate ID: Create a type-prefixed ULID using the entity’s
prefix(e.g.,ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X). - Inject base fields: Set
id,type,version(default 1),created_at(now),updated_at(now),created_by,status("active"). - Extract special fields: Pull
tags,relationships, andsourcefromdatainto base-level fields if present. - Merge data: Combine base fields with the remaining
datafields. - Validate against schema: If a schema is loaded for this entity type, validate the complete record. On failure, no entity is created.
- Write file: Write the entity JSON to
{namespace}/data/{plural}/{id}.jsonwith 2-space indentation. - Return: The complete entity (base fields + app data merged flat).
Example
Section titled “Example”from upjack import UpjackApp
app = UpjackApp.from_manifest("manifest.json")
contact = app.create_entity("contact", { "name": "Alice Chen", "email": "alice@example.com", "company_name": "TechCorp", "title": "VP Engineering", "stage": "new",})import { UpjackApp } from "upjack";
const app = UpjackApp.fromManifest("manifest.json");
const contact = app.createEntity("contact", { name: "Alice Chen", email: "alice@example.com", company_name: "TechCorp", title: "VP Engineering", stage: "new",});Returns:
{ "id": "ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X", "type": "contact", "version": 1, "created_at": "2026-02-15T10:30:00Z", "updated_at": "2026-02-15T10:30:00Z", "created_by": "agent", "status": "active", "tags": [], "relationships": [], "name": "Alice Chen", "email": "alice@example.com", "company_name": "TechCorp", "title": "VP Engineering", "stage": "new"}Errors
Section titled “Errors”| Condition | Exception | Message |
|---|---|---|
Unknown entity_type | ValueError | Unknown entity type 'foo'. Known types: ['contact', 'deal'] |
| Schema validation failure | jsonschema.ValidationError | Describes the specific validation error |
| Condition | Error | Message |
|---|---|---|
Unknown entityType | Error | Unknown entity type 'foo'. Known types: contact, deal |
| Schema validation failure | Error | Describes the specific validation error (from AJV) |
update_entity
Section titled “update_entity”Update an existing entity by ID.
app.update_entity(entity_type: str, entity_id: str, data: dict, merge: bool = True) -> dictapp.updateEntity(entityType: string, entityId: string, data: Record<string, unknown>, merge?: boolean): EntityRecordMCP Tool
Section titled “MCP Tool”update_{entity}(entity_id: str, data: dict) -> dictParameters
Section titled “Parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
entity_type / entityType | string | Yes | — | Entity type name. |
entity_id / entityId | string | Yes | — | Entity ID (e.g., "ct_01HZ...5X"). |
data | object | Yes | — | Fields to update. |
merge | boolean | No | true | Whether to merge with existing data or replace. |
Behavior
Section titled “Behavior”- Load entity: Read the entity JSON file from its storage path. Error if not found.
- Strip immutable fields: Silently remove
id,type,version,created_at, andcreated_byfrom incomingdata. These cannot be changed after creation. - Merge or replace: If
merge: true(default), shallow-mergedatainto the existing entity. Existing fields not present indataare preserved. Ifmerge: false, replace all fields except immutable base fields. - Auto-update timestamp: Set
updated_atto the current timestamp. - Validate against schema: If a schema is loaded, validate the merged entity. On failure, no changes are written.
- Write file: Overwrite the entity JSON file.
- Return: The complete updated entity.
Example
Section titled “Example”updated = app.update_entity("contact", "ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X", { "stage": "qualified", "score": 85,})const updated = app.updateEntity("contact", "ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X", { stage: "qualified", score: 85,});Returns:
{ "id": "ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X", "type": "contact", "version": 1, "created_at": "2026-02-15T10:30:00Z", "updated_at": "2026-02-15T14:22:00Z", "created_by": "agent", "status": "active", "name": "Alice Chen", "email": "alice@example.com", "company_name": "TechCorp", "title": "VP Engineering", "stage": "qualified", "score": 85}entity_search
Section titled “entity_search”Text and structured search across entities of a given type.
app.search_entities( entity_type: str, query: str | None = None, filter: dict | None = None, sort: str = "-updated_at", limit: int = 20,) -> list[dict]app.searchEntities(entityType: string, options?: { query?: string; filter?: Record<string, unknown>; sort?: string; limit?: number;}): EntityRecord[]MCP Tool
Section titled “MCP Tool”search_{plural}(query: str | None, filter: dict | None, sort: str, limit: int) -> list[dict]Parameters
Section titled “Parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
entity_type / entityType | string | Yes | — | Entity type to search. |
query | string | No | — | Case-insensitive substring match across all string-valued fields. |
filter | object | No | — | Structured filter with comparison operators. |
sort | string | No | "-updated_at" | Sort field. Prefix with - for descending. |
limit | integer | No | 20 | Maximum results to return. |
Behavior
Section titled “Behavior”- Load all entities: Read every JSON file in
{namespace}/data/{plural}/. Corrupt files are silently skipped. - Exclude deleted by default: Unless the filter explicitly includes a
statuskey, entities withstatus: "deleted"are excluded. - Apply text query: If
queryis provided, match case-insensitively against all string-valued fields in the entity. An entity matches if any string field contains the query as a substring. - Apply structured filters: If
filteris provided, evaluate each key-value pair against the entity. All conditions must match (AND logic). - Sort: Apply the sort expression. Default is
-updated_at(most recently updated first). - Limit: Return at most
limitresults. - Return: Array of complete entity dicts/objects matching the query.
Filter Syntax
Section titled “Filter Syntax”Filters use flat field names (not JSONPath) with optional comparison operators:
# Direct equalityapp.search_entities("contact", filter={"stage": "qualified"})
# Comparison operatorsapp.search_entities("contact", filter={"score": {"$gte": 70}})
# Combined filters (AND logic)app.search_entities("contact", filter={ "stage": {"$in": ["qualified", "contacted"]}, "score": {"$gte": 60},})// Direct equalityapp.searchEntities("contact", { filter: { stage: "qualified" } });
// Comparison operatorsapp.searchEntities("contact", { filter: { score: { $gte: 70 } } });
// Combined filters (AND logic)app.searchEntities("contact", { filter: { stage: { $in: ["qualified", "contacted"] }, score: { $gte: 60 }, },});Supported operators:
| Operator | Meaning | Example |
|---|---|---|
| (direct value) | Equality | {"stage": "qualified"} |
$gt | Greater than | {"score": {"$gt": 50}} |
$gte | Greater than or equal | {"score": {"$gte": 70}} |
$lt | Less than | {"score": {"$lt": 50}} |
$lte | Less than or equal | {"score": {"$lte": 30}} |
$ne | Not equal | {"stage": {"$ne": "lost"}} |
$in | Value in array | {"stage": {"$in": ["qualified", "contacted"]}} |
$contains | Array field contains value | {"tags": {"$contains": "saas"}} |
$exists | Field exists (true) or is absent (false) | {"score": {"$exists": true}} |
Example
Section titled “Example”results = app.search_entities( "contact", query="TechCorp", filter={"score": {"$gte": 60}}, sort="-score", limit=10,)const results = app.searchEntities("contact", { query: "TechCorp", filter: { score: { $gte: 60 } }, sort: "-score", limit: 10,});Returns:
[ { "id": "ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X", "type": "contact", "version": 1, "created_at": "2026-02-15T10:30:00Z", "updated_at": "2026-02-15T14:22:00Z", "status": "active", "name": "Alice Chen", "email": "alice@example.com", "company_name": "TechCorp", "title": "VP Engineering", "stage": "qualified", "score": 85 }]Performance Note
Section titled “Performance Note”The current implementation loads all entity files into memory for each search operation. This works well for typical app sizes (hundreds to low thousands of entities). For larger datasets, a future version may introduce indexing.
entity_list
Section titled “entity_list”Lightweight entity listing. Simpler than entity_search, filtering by status only.
app.list_entities(entity_type: str, status: str = "active", limit: int = 50) -> list[dict]app.listEntities(entityType: string, status?: string, limit?: number): EntityRecord[]MCP Tool
Section titled “MCP Tool”list_{plural}(status: str, limit: int) -> list[dict]Parameters
Section titled “Parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
entity_type / entityType | string | Yes | — | Entity type to list. |
status | string | No | "active" | Filter by status. |
limit | integer | No | 50 | Maximum results to return. |
Example
Section titled “Example”contacts = app.list_entities("contact", status="active", limit=5)const contacts = app.listEntities("contact", "active", 5);Difference from entity_search
Section titled “Difference from entity_search”| Aspect | entity_search | entity_list |
|---|---|---|
| Text search | Yes (substring match) | No |
| Structured filters | Yes (comparison operators) | Status only |
| Sort | Any field | By updated_at (most recent first) |
| Default limit | 20 | 50 |
| Returns | Full entity dicts/objects | Full entity dicts/objects |
| Use case | Finding specific entities | Browsing/overview |
entity_delete
Section titled “entity_delete”Delete an entity by ID. Soft delete (status change) by default; hard delete (file removal) with explicit flag.
app.delete_entity(entity_type: str, entity_id: str, hard: bool = False) -> dictapp.deleteEntity(entityType: string, entityId: string, hard?: boolean): EntityRecordMCP Tool
Section titled “MCP Tool”delete_{entity}(entity_id: str, hard: bool = False) -> dictParameters
Section titled “Parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
entity_type / entityType | string | Yes | — | Entity type name. |
entity_id / entityId | string | Yes | — | Entity ID to delete. |
hard | boolean | No | false | If true, permanently remove the file. If false, set status to "deleted". |
Example
Section titled “Example”# Soft delete (default)result = app.delete_entity("contact", "ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X")assert result["status"] == "deleted"
# Hard delete (permanent)result = app.delete_entity("contact", "ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X", hard=True)# File is removed from disk// Soft delete (default)const result = app.deleteEntity("contact", "ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X");console.log(result.status); // "deleted"
// Hard delete (permanent)const removed = app.deleteEntity("contact", "ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X", true);// File is removed from diskCommon Errors
Section titled “Common Errors”| Exception | When |
|---|---|
ValueError | Unknown entity type, invalid ID prefix |
FileNotFoundError | Entity ID does not exist on disk |
jsonschema.ValidationError | Data fails JSON Schema validation |
json.JSONDecodeError | Entity file contains corrupt JSON (raised by get_entity; list and search skip corrupt files silently) |
| Error | When |
|---|---|
Error (“Unknown entity type…”) | Unknown entity type, invalid ID prefix |
Error (“Entity not found…”) | Entity ID does not exist on disk |
Error (“Validation failed…”) | Data fails JSON Schema validation (AJV) |
Concurrency and Consistency
Section titled “Concurrency and Consistency”Entity operations work on individual JSON files in the workspace. The consistency model is:
- Single-writer: The agent processes one tool call at a time. Concurrent writes to the same entity are not expected.
- Read-your-writes: After an operation returns, subsequent reads will see the updated state.
- No transactions: Multi-entity operations are not atomic. If a skill needs to create a contact and an activity, they are two separate writes. This is acceptable because the agent is the sole writer.
Future Enhancements
Section titled “Future Enhancements”The following features are specified but not yet implemented:
- Singleton constraint: Preventing creation of duplicate singleton entities.
- Indexed search: SQLite FTS or similar for faster search on large datasets.
- Structured error objects: MCP-level error responses with
code/message/detailsformat. - Entity summaries: Returning lightweight projections from
entity_listinstead of full objects.