Skip to content

Commit d111f35

Browse files
dougborgclaude
andcommitted
fix(docs): repoint docs symlinks to StatusPro packages; rewrite stale ADR examples
Docs symlinks `docs/client` and `docs/mcp-server` pointed to deleted `../katana_*` directories, breaking every CI run since the fork. Repoint them to `../statuspro_public_api_client/docs` and `../statuspro_mcp_server/docs`. mkdocs.yml nav retuned to the actual StatusPro doc tree: - drops references to deleted Katana-era ADRs (ADR-007, ADR-010) and pages (architecture, implementation-plan) - adds StatusPro-era ADRs (0016, 0017, 0014) and the new examples + LOGGING pages - renames the top-level group from "Client" to "Python Client" Delete ADR-007 (Domain Helper Classes) — file leaked through the earlier sweep. StatusPro's thin helpers (client.orders, client.statuses) don't justify the helper-class architecture this ADR proposed for Katana's 100+ endpoints. ADR edits (rewriting Katana examples, not changing the decisions): - ADR-001: "248+ endpoints" → "every generated endpoint" - ADR-002: "76+ endpoints / 150+ models" → actual StatusPro counts (9 / 20) - ADR-005: get_all_sales_orders → list_orders in concurrency example - ADR-008: builder example uses OrderQuery + list_orders; remove dead link to BUILDER_PATTERN_ANALYSIS.md - ADR-011: domain grouping dropped inventory/sales_orders/etc, now errors/orders/statuses; conversion example uses OrderResponse - ADR-016: rewritten end-to-end — per-parameter Annotated[] as the primary pattern, Unpack() decorator as an option for bulk/complex bodies; all examples use StatusPro tools (update_order_status, bulk_update_order_status) - ADR-017: example tool is now update_order_status (not create_purchase_order); "15 foundation tools" → "9 tools (5 read-only, 4 mutations)" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 72f4232 commit d111f35

11 files changed

Lines changed: 238 additions & 515 deletions

docs/client

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
../katana_public_api_client/docs
1+
../statuspro_public_api_client/docs

docs/mcp-server

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
../katana_mcp_server/docs
1+
../statuspro_mcp_server/docs

mkdocs.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ markdown_extensions:
127127

128128
nav:
129129
- Home: index.md
130-
- Client:
130+
- Python Client:
131131
- User Guide: client/guide.md
132132
- Cookbook: client/cookbook.md
133133
- Testing: client/testing.md
@@ -140,20 +140,20 @@ nav:
140140
- ADR-004 Defer Observability to httpx: client/adr/0004-defer-observability-to-httpx.md
141141
- ADR-005 Sync and Async APIs: client/adr/0005-sync-async-apis.md
142142
- ADR-006 Response Unwrapping Utilities: client/adr/0006-response-unwrapping-utilities.md
143-
- ADR-007 Domain Helper Classes: client/adr/0007-domain-helper-classes.md
144143
- ADR-008 Avoid Builder Pattern (Proposed): client/adr/0008-avoid-builder-pattern.md
145144
- ADR-011 Pydantic Domain Models: client/adr/0011-pydantic-domain-models.md
146145
- ADR-012 Validation Tiers: client/adr/0012-validation-tiers-for-agent-workflows.md
147146
- MCP Server:
148147
- Overview: mcp-server/index.md
149-
- Architecture: mcp-server/architecture.md
148+
- Examples: mcp-server/examples.md
150149
- Development: mcp-server/development.md
151150
- Deployment: mcp-server/deployment.md
152151
- Docker: mcp-server/docker.md
153-
- Implementation Plan: mcp-server/implementation-plan.md
152+
- Logging: mcp-server/LOGGING.md
154153
- ADRs:
155154
- Overview: mcp-server/adr/README.md
156-
- ADR-010 StatusPro MCP Server: mcp-server/adr/0010-statuspro-mcp-server.md
155+
- ADR-0016 Tool Interface Pattern: mcp-server/adr/0016-tool-interface-pattern.md
156+
- ADR-0017 Automated Tool Documentation: mcp-server/adr/0017-automated-tool-documentation.md
157157
- API Reference: reference/
158158
- OpenAPI Documentation: openapi-docs.md
159159
- Development:
@@ -166,6 +166,7 @@ nav:
166166
- Overview: adr/README.md
167167
- ADR-009 Migrate to uv: adr/0009-migrate-from-poetry-to-uv.md
168168
- ADR-013 Module-Local Docs: adr/0013-module-local-documentation.md
169+
- ADR-014 GitHub Copilot Custom Agents: adr/0014-github-copilot-custom-agents.md
169170

170171
extra:
171172
version:

statuspro_mcp_server/docs/adr/0016-tool-interface-pattern.md

Lines changed: 121 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
Accepted
66

7-
Date: 2025-01-11
7+
Date: 2025-01-11 (updated 2026-04-17 for StatusPro fork — examples replaced,
8+
core decision unchanged)
89

910
## Context
1011

11-
MCP tools need consistent, type-safe interfaces for requests and responses. We needed to
12-
decide:
12+
MCP tools need consistent, type-safe interfaces for requests and responses.
13+
We needed to decide:
1314

1415
- How to structure tool parameters (flat vs nested)
1516
- How to handle validation
@@ -19,105 +20,125 @@ decide:
1920

2021
## Decision
2122

22-
We adopt the **Unpack Pattern with Pydantic Models** combined with **FastMCP
23-
Elicitation** for destructive operations.
23+
We adopt the **Pydantic parameter annotations** pattern combined with **FastMCP
24+
Elicitation** for destructive operations. StatusPro's tools are small enough
25+
that we use Pydantic `Field()` on each parameter directly rather than a
26+
nested request model + `Unpack()` decorator; the Katana parent project used
27+
the Unpack pattern for its larger request bodies and we kept the decorator
28+
infrastructure (`unpack.py`) as an option.
2429

25-
### Pattern Components
30+
### Pattern components
2631

27-
#### 1. Request Models
28-
29-
Pydantic models define tool parameters with full type safety and validation:
32+
#### 1. Per-parameter annotations (typical StatusPro tool)
3033

3134
```python
32-
class CreatePurchaseOrderRequest(BaseModel):
33-
"""Request to create a purchase order."""
34-
supplier_id: int = Field(..., description="Supplier ID")
35-
location_id: int = Field(..., description="Location ID where items will be received")
36-
order_number: str = Field(..., description="Purchase order number")
37-
items: list[PurchaseOrderItem] = Field(..., description="Line items", min_length=1)
38-
confirm: bool = Field(False, description="If false, returns preview. If true, creates order.")
35+
@mcp.tool(
36+
name="update_order_status",
37+
description="Change an order's status. Two-step confirm.",
38+
)
39+
async def update_order_status(
40+
context: Context,
41+
order_id: int,
42+
status_code: Annotated[
43+
str, Field(description="8-char status code, e.g. 'st000003'")
44+
],
45+
comment: Annotated[str | None, Field(description="Optional history comment")] = None,
46+
public: Annotated[bool, Field(description="Visible to the customer")] = False,
47+
email_customer: bool = True,
48+
email_additional: bool = True,
49+
confirm: Annotated[bool, Field(description="Must be true to apply the change")] = False,
50+
) -> dict[str, Any]:
51+
...
3952
```
4053

41-
#### 2. Unpack Decorator
54+
#### 2. Request model + Unpack decorator (for complex bodies)
4255

43-
Flattens nested models for FastMCP compatibility:
56+
When a request has many fields or nested structure, wrap it in a Pydantic
57+
model and use the `@unpack_pydantic_params` decorator (still available via
58+
`statuspro_mcp/unpack.py`):
4459

4560
```python
46-
@observe_tool
61+
class BulkStatusUpdateRequest(BaseModel):
62+
order_ids: list[int] = Field(..., min_length=1, max_length=50)
63+
status_code: str
64+
comment: str | None = None
65+
public: bool = False
66+
email_customer: bool = True
67+
confirm: bool = False
68+
4769
@unpack_pydantic_params
48-
async def create_purchase_order(
49-
request: Annotated[CreatePurchaseOrderRequest, Unpack()],
50-
context: Context
51-
) -> PurchaseOrderResponse:
52-
"""Create a new purchase order with user confirmation."""
70+
async def bulk_update_order_status(
71+
request: Annotated[BulkStatusUpdateRequest, Unpack()],
72+
context: Context,
73+
) -> dict[str, Any]:
5374
...
5475
```
5576

56-
#### 3. Response Models
77+
#### 3. Response shape
5778

58-
Structured responses with success/failure states:
79+
StatusPro tools return plain dicts. The mutation tools follow this shape:
5980

6081
```python
61-
class PurchaseOrderResponse(BaseModel):
62-
"""Response from creating a purchase order."""
63-
id: int | None = None
64-
order_number: str
65-
supplier_id: int
66-
status: str
67-
total_cost: float | None = None
68-
is_preview: bool
69-
message: str
70-
warnings: list[str] = []
71-
next_actions: list[str] = []
82+
{
83+
"confirmed": bool,
84+
"success": bool,
85+
"status_code": int, # HTTP status from the API
86+
# For bulk ops:
87+
"note": "Bulk updates are queued and processed asynchronously.",
88+
}
7289
```
7390

74-
#### 4. Elicitation Pattern (Safety-Critical Operations)
91+
Non-mutation tools return typed Pydantic responses (e.g. `list[OrderSummary]`,
92+
`list[StatusEntry]`).
7593

76-
For destructive operations, we use FastMCP's elicitation to request user confirmation:
94+
#### 4. Elicitation pattern (safety-critical operations)
95+
96+
For destructive operations, we use FastMCP's elicitation to request user
97+
confirmation:
7798

7899
```python
79-
# Preview mode (confirm=false) - show what would happen
80-
if not request.confirm:
81-
return preview_response()
100+
# Preview mode (confirm=false) show what would happen
101+
if not confirm:
102+
return {"preview": preview, "confirmed": False}
82103

83104
# Request user confirmation via elicitation
84-
elicit_result = await context.elicit(
85-
f"Create purchase order {order_number} with {item_count} items totaling ${total}?",
86-
ConfirmationSchema,
105+
result = await require_confirmation(
106+
context,
107+
f"Change order {order_id} status to {status_code}?",
87108
)
109+
if result is not ConfirmationResult.CONFIRMED:
110+
return {"preview": preview, "confirmed": False, "result": result.value}
88111

89-
# Handle user response
90-
if elicit_result.action != "accept":
91-
return cancelled_response()
92-
93-
if not elicit_result.data.confirm:
94-
return declined_response()
95-
96-
# User confirmed - proceed with operation
97-
result = await create_order()
98-
return success_response(result)
112+
# User confirmed — proceed with the API call
113+
response = await update_order_status_api.asyncio_detailed(...)
114+
return {"confirmed": True, "success": is_success(response), ...}
99115
```
100116

101-
#### 5. Shared Schemas
117+
#### 5. Shared schemas
102118

103-
Common schemas are extracted to `statuspro_mcp/tools/schemas.py` to avoid duplication:
119+
Common schemas live in `statuspro_mcp/tools/schemas.py` so every mutation
120+
tool reuses the same confirmation flow:
104121

105122
```python
106123
# statuspro_mcp/tools/schemas.py
107124
class ConfirmationSchema(BaseModel):
108125
"""Schema for user confirmation elicitation."""
109-
confirm: bool = Field(..., description="True to proceed, False to cancel")
126+
confirm: bool = Field(..., description="Confirm the action (true to proceed)")
127+
128+
129+
async def require_confirmation(context: Context, message: str) -> ConfirmationResult:
130+
...
110131
```
111132

112133
### Benefits
113134

114-
- **Type Safety**: Pydantic validates all inputs at runtime
115-
- **Documentation**: Model fields are self-documenting with descriptions
116-
- **IDE Support**: Autocomplete and type checking work perfectly
135+
- **Type safety**: Pydantic validates all inputs at runtime
136+
- **Documentation**: Field descriptions are self-documenting
137+
- **IDE support**: Autocomplete and type checking work perfectly
117138
- **Testability**: Easy to mock and test with Pydantic models
118-
- **Consistency**: All tools follow the same pattern
139+
- **Consistency**: All mutation tools follow the same two-step confirm pattern
119140
- **Safety**: Destructive operations require explicit user confirmation
120-
- **DRY**: Shared schemas eliminate duplication
141+
- **DRY**: Shared `require_confirmation` helper across every mutation tool
121142

122143
## Consequences
123144

@@ -126,83 +147,77 @@ class ConfirmationSchema(BaseModel):
126147
- Type-safe tool interfaces prevent runtime errors
127148
- Self-documenting parameters improve developer experience
128149
- Validation errors are clear and actionable
129-
- Easy to add new parameters (just update model)
130150
- Elicitation prevents accidental destructive operations
131-
- Shared schemas ensure consistency across tools
151+
- Shared helpers ensure consistency across tools
132152

133153
### Negative
134154

135-
- More boilerplate (request/response models for each tool)
136-
- Unpack decorator adds complexity
137-
- Learning curve for new contributors
138-
- Elicitation adds extra step for confirmed operations
155+
- Per-parameter `Annotated[...]` annotations are verbose for wide signatures
156+
- Unpack decorator adds complexity where it's used
157+
- Elicitation adds an extra round-trip for confirmed operations
139158

140159
### Neutral
141160

142-
- Models live in same file as tool implementation
143-
- Each tool has 2-3 model classes (Request, Response, nested types)
144-
- Elicitation pattern only used for destructive operations
161+
- Elicitation pattern only used for destructive operations (4 of 9 tools)
162+
- Preview-then-confirm means every mutation is at minimum a two-call flow
145163

146-
## Alternatives Considered
164+
## Alternatives considered
147165

148-
### Alternative 1: Flat Parameters
166+
### Alternative 1: Flat untyped parameters
149167

150168
```python
151-
async def create_purchase_order(
152-
supplier_id: int,
153-
location_id: int,
154-
order_number: str,
155-
items: list[dict], # ❌ Not type-safe
156-
context: Context
169+
async def update_order_status(
170+
order_id: int,
171+
status_code: str,
172+
comment: str | None, # ❌ No Field description
173+
...
174+
context: Context,
157175
) -> dict:
158176
...
159177
```
160178

161-
**Why rejected**: No validation, not type-safe, hard to document nested structures
179+
**Why rejected**: No validation, tool schemas lose field descriptions the
180+
model sees, harder to keep tools consistent.
162181

163-
### Alternative 2: Dictionary-Based
182+
### Alternative 2: Dictionary-based
164183

165184
```python
166-
async def create_purchase_order(
167-
params: dict, # ❌ No type safety
168-
context: Context
185+
async def update_order_status(
186+
params: dict, # ❌ No type safety
187+
context: Context,
169188
) -> dict:
170189
...
171190
```
172191

173-
**Why rejected**: No IDE support, no validation, no documentation
192+
**Why rejected**: No IDE support, no validation, no documentation.
174193

175-
### Alternative 3: Manual Confirmation via Response Field
194+
### Alternative 3: Manual confirmation via response field (no elicitation)
176195

177196
```python
178-
# Return a "pending" response, require second call to confirm
179-
async def create_purchase_order(...) -> dict:
197+
async def update_order_status(...) -> dict:
180198
if not confirmed:
181199
return {"status": "pending", "confirmation_required": True}
182-
# Otherwise create
200+
# Otherwise apply
183201
```
184202

185-
**Why rejected**: Two API calls required, harder to use, no built-in UI support
203+
**Why rejected**: Two round trips, harder to use, no built-in UI integration
204+
for preview/confirm in Claude Desktop.
205+
206+
## Implementation examples
207+
208+
Mutation tools using this pattern (all follow two-step confirm with elicitation):
186209

187-
## Implementation Examples
210+
- `update_order_status`
211+
- `add_order_comment`
212+
- `update_order_due_date`
213+
- `bulk_update_order_status`
188214

189-
Tools using this pattern:
215+
Read-only tools (no elicitation):
190216

191-
- `create_purchase_order` - Preview/confirm with elicitation
192-
- `receive_purchase_order` - Preview/confirm with elicitation
193-
- `create_manufacturing_order` - Preview/confirm with elicitation
194-
- `fulfill_order` - Preview/confirm with elicitation
195-
- `verify_order_document` - Read-only, no elicitation needed
196-
- `search_items` - Read-only, no elicitation needed
217+
- `list_orders`, `get_order`, `lookup_order`, `list_statuses`, `get_viable_statuses`
197218

198219
## References
199220

200221
- [ADR-0011: Pydantic Domain Models](../../statuspro_public_api_client/docs/adr/0011-pydantic-domain-models.md)
201222
- [ADR-0017: Automated Tool Documentation](0017-automated-tool-documentation.md)
202-
- [statuspro_mcp/unpack.py](../../src/statuspro_mcp/unpack.py) - Unpack decorator
203-
implementation
204-
- [statuspro_mcp/tools/schemas.py](../../src/statuspro_mcp/tools/schemas.py) - Shared
205-
confirmation schema
206-
- [FastMCP Documentation](https://github.com/jlowin/fastmcp) - Elicitation pattern
207-
- [PR #173](https://github.com/dougborg/statuspro-openapi-client/pull/173) - Elicitation
208-
implementation
223+
- [FastMCP Documentation](https://github.com/jlowin/fastmcp) — Elicitation pattern

0 commit comments

Comments
 (0)