Skip to content

Commit 1bba883

Browse files
committed
Domain-friendly tool interfaces with typed responses
- CRUD tools use entity-specific parameter names (contact_id, deal_id) instead of generic entity_id - list/search/relationship tools return {entities, count, ...} envelope instead of bare arrays - All entity-derived tools declare outputSchema and return structuredContent per MCP spec 2025-06-18 - Add build_entity_output_schema() and build_list_output_schema() to upjack.schema Release 0.3.0
1 parent 3b6bf0a commit 1bba883

12 files changed

Lines changed: 384 additions & 125 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
44

55
This project follows [Keep a Changelog](https://keepachangelog.com/).
66

7+
## [0.3.0] - 2026-03-27
8+
9+
### Changed
10+
- **Breaking:** CRUD tools use entity-specific parameter names (`contact_id`, `deal_id`) instead of generic `entity_id`
11+
- **Breaking:** `list_*` and `search_*` tools return `{entities: [...], count, ...}` envelope instead of bare arrays
12+
- **Breaking:** `query_*_by_relationship` and `get_related_*` tools return the same envelope format
13+
- All entity-derived tools declare `outputSchema` and return `structuredContent` (MCP spec 2025-06-18)
14+
- Added `build_entity_output_schema()` and `build_list_output_schema()` helpers to `upjack.schema`
15+
- Internal: `_wrap_list()` helper for consistent response envelope construction
16+
717
## [0.2.0] - 2026-03-26
818

919
### Added

lib/python/e2e/test_crm_mcpb.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ async def test_seed_and_list(bundle_path):
107107
assert seed_data["errors"] == []
108108

109109
list_result = await session.call_tool("list_contacts", {})
110-
contacts = json.loads(list_result.content[0].text)
111-
assert len(contacts) == 2
110+
result_data = json.loads(list_result.content[0].text)
111+
assert result_data["count"] == 2
112112

113113

114114
@pytest.mark.asyncio
@@ -134,15 +134,15 @@ async def test_full_crud_cycle(bundle_path):
134134
assert contact_id.startswith("ct_")
135135

136136
# Get
137-
get_result = await session.call_tool("get_contact", {"entity_id": contact_id})
137+
get_result = await session.call_tool("get_contact", {"contact_id": contact_id})
138138
fetched = json.loads(get_result.content[0].text)
139139
assert fetched["first_name"] == "Bob"
140140
assert fetched["email"] == "bob@test.com"
141141

142142
# Update
143143
update_result = await session.call_tool(
144144
"update_contact",
145-
{"entity_id": contact_id, "data": {"lead_score": 85}},
145+
{"contact_id": contact_id, "data": {"lead_score": 85}},
146146
)
147147
updated = json.loads(update_result.content[0].text)
148148
assert updated["lead_score"] == 85
@@ -153,19 +153,16 @@ async def test_full_crud_cycle(bundle_path):
153153
"search_contacts",
154154
{"query": "Bob"},
155155
)
156-
results = json.loads(search_result.content[0].text)
157-
assert len(results) == 1
158-
assert results[0]["lead_score"] == 85
156+
search_data = json.loads(search_result.content[0].text)
157+
assert search_data["count"] == 1
158+
assert search_data["entities"][0]["lead_score"] == 85
159159

160160
# Delete
161-
delete_result = await session.call_tool("delete_contact", {"entity_id": contact_id})
161+
delete_result = await session.call_tool("delete_contact", {"contact_id": contact_id})
162162
deleted = json.loads(delete_result.content[0].text)
163163
assert deleted["status"] == "deleted"
164164

165165
# Verify gone from list
166166
list_result = await session.call_tool("list_contacts", {})
167-
if list_result.content:
168-
active = json.loads(list_result.content[0].text)
169-
else:
170-
active = []
171-
assert all(c["id"] != contact_id for c in active)
167+
list_data = json.loads(list_result.content[0].text)
168+
assert all(c["id"] != contact_id for c in list_data["entities"])

lib/python/e2e/test_research_mcpb.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,5 @@ async def test_seed_data(bundle_path):
8585
assert seed_data["errors"] == []
8686

8787
list_result = await session.call_tool("list_topics", {})
88-
topics = json.loads(list_result.content[0].text)
89-
assert len(topics) == 2
88+
result_data = json.loads(list_result.content[0].text)
89+
assert result_data["count"] == 2

lib/python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "upjack"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
description = "Schema-driven entity management for AI-native applications"
55
readme = "README.md"
66
license = {text = "Apache-2.0"}

lib/python/src/upjack/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""NimbleBrain Upjack — schema-driven entity management for AI-native applications."""
22

3-
__version__ = "0.2.0"
3+
__version__ = "0.3.0"
44

55
from upjack.activity import ACTIVITY_ENTITY_DEF, get_activity_schema
66
from upjack.app import UpjackApp

lib/python/src/upjack/schema.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,38 @@ def validate_schema_change(
212212
)
213213

214214
return diagnostics
215+
216+
217+
def build_entity_output_schema(schema: dict[str, Any]) -> dict[str, Any]:
218+
"""Build an output schema for a single-entity tool response.
219+
220+
Returns the full entity schema (including base fields) with JSON Schema
221+
meta keywords stripped, suitable for use as a tool's ``outputSchema``.
222+
"""
223+
result = copy.deepcopy(schema)
224+
result.pop("$schema", None)
225+
result.pop("$id", None)
226+
return result
227+
228+
229+
def build_list_output_schema(entity_schema: dict[str, Any]) -> dict[str, Any]:
230+
"""Build an output schema for a list/search response envelope.
231+
232+
Returns an object schema with ``entities`` (array of entity schemas)
233+
and ``count`` (integer).
234+
"""
235+
item_schema = build_entity_output_schema(entity_schema)
236+
return {
237+
"type": "object",
238+
"properties": {
239+
"entities": {
240+
"type": "array",
241+
"items": item_schema,
242+
},
243+
"count": {
244+
"type": "integer",
245+
"description": "Number of entities returned",
246+
},
247+
},
248+
"required": ["entities", "count"],
249+
}

0 commit comments

Comments
 (0)