Skip to content

Commit fe49bd2

Browse files
Copilotdougborg
andauthored
feat(mcp): implement FastMCP elicitation pattern for destructive operations (#173)
* Initial plan * feat(mcp): add FastMCP elicitation pattern to destructive operations - Added ConfirmationSchema for user confirmation via elicit() - Implemented ctx.elicit() in create_purchase_order tool - Implemented ctx.elicit() in receive_purchase_order tool - Implemented ctx.elicit() in create_manufacturing_order tool - Implemented ctx.elicit() in fulfill_order tool (both manufacturing and sales) - Updated test fixtures to mock elicit() behavior - Fixed type narrowing for elicitation results Co-authored-by: dougborg <1261222+dougborg@users.noreply.github.com> * test(mcp): fix wrapper test to call implementation directly The wrapper function with @unpack_pydantic_params expects unpacked args from FastMCP, so tests should call the implementation function directly. Co-authored-by: dougborg <1261222+dougborg@users.noreply.github.com> * chore: configure yamllint with 120 char line length - Created .yamllint.yml config file - Set line-length max to 120 characters - Ignore .github/ directory (workflow files) - Ignore docs/katana-openapi.yaml (OpenAPI spec) - Updated pyproject.toml to match new line length limit - All yamllint checks now pass Co-authored-by: dougborg <1261222+dougborg@users.noreply.github.com> * refactor(mcp): extract ConfirmationSchema to shared module - Created katana_mcp/tools/schemas.py for shared Pydantic schemas - Moved ConfirmationSchema from 3 separate files to shared location - Updated imports in purchase_orders.py, manufacturing_orders.py, and orders.py - Fixes DRY principle violation identified in PR review This ensures consistency and makes future changes easier to manage. Co-authored-by: dougborg <1261222+dougborg@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: dougborg <1261222+dougborg@users.noreply.github.com>
1 parent c1c2a48 commit fe49bd2

17 files changed

Lines changed: 362 additions & 80 deletions

.github/FUNDING.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# These are supported funding model platforms
22

3-
github:
4-
- @dougborg
3+
github: [dougborg]
54
patreon: # Replace with a single Patreon username
65
open_collective: # Replace with a single Open Collective username
76
ko_fi: # Replace with a single Ko-fi username

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ body:
5858
id: terms
5959
attributes:
6060
label: Code of Conduct
61-
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/dougborg/katana-open-api-client/blob/main/CODE_OF_CONDUCT.md)
61+
description: >-
62+
By submitting this issue, you agree to follow our
63+
[Code of Conduct](https://github.com/dougborg/katana-open-api-client/blob/main/CODE_OF_CONDUCT.md)
6264
options:
6365
- label: I agree to follow this project's Code of Conduct
6466
required: true

.github/ISSUE_TEMPLATE/feature_request.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ body:
4343
id: terms
4444
attributes:
4545
label: Code of Conduct
46-
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/dougborg/katana-open-api-client/blob/main/CODE_OF_CONDUCT.md)
46+
description: >-
47+
By submitting this issue, you agree to follow our
48+
[Code of Conduct](https://github.com/dougborg/katana-open-api-client/blob/main/CODE_OF_CONDUCT.md)
4749
options:
4850
- label: I agree to follow this project's Code of Conduct
4951
required: true

.github/dependabot.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Dependabot configuration for automated dependency updates
2-
# See: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
2+
# See:
3+
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
34
#
45
# NOTE: Python dependency updates are currently disabled because this project uses uv
56
# for package management (see ADR-009), and Dependabot does not yet support the uv

.github/workflows/copilot-setup-steps.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ jobs:
2020
# Set the permissions to the lowest permissions possible needed for your steps.
2121
# Copilot will be given its own token for its operations.
2222
permissions:
23-
# If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete.
23+
# If you want to clone the repository as part of your setup steps, for example
24+
# to install dependencies, you'll need the `contents: read` permission. If you
25+
# don't clone the repository in your setup steps, Copilot will do this for you
26+
# automatically after the steps complete.
2427
contents: read
2528

2629
# You can define any steps you want, and they will run before the agent starts.

.github/workflows/release.yml

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,11 @@ jobs:
7373
- name: Check for client changes
7474
id: check
7575
run: |
76-
# Check if there are any commits with (client) scope or no scope since last client release
77-
if git log $(git describe --tags --abbrev=0 --match="client-v*" 2>/dev/null || echo "HEAD~10")..HEAD --pretty=format:"%s" | grep -qE '^(feat|fix|perf)(\(client\))?:'; then
76+
# Check if there are any commits with (client) scope or no scope since last
77+
# client release
78+
if git log $(git describe --tags --abbrev=0 --match="client-v*" 2>/dev/null || \
79+
echo "HEAD~10")..HEAD --pretty=format:"%s" | \
80+
grep -qE '^(feat|fix|perf)(\(client\))?:'; then
7881
echo "has_changes=true" >> $GITHUB_OUTPUT
7982
else
8083
echo "has_changes=false" >> $GITHUB_OUTPUT
@@ -89,11 +92,17 @@ jobs:
8992
verbosity: 2
9093

9194
- name: Build client package
92-
if: (steps.release.outcome == 'success' && steps.release.outputs.released == 'true') || inputs.force_publish_client == true
95+
if: >-
96+
(steps.release.outcome == 'success' &&
97+
steps.release.outputs.released == 'true') ||
98+
inputs.force_publish_client == true
9399
run: uv build
94100

95101
- name: Upload client build artifacts
96-
if: (steps.release.outcome == 'success' && steps.release.outputs.released == 'true') || inputs.force_publish_client == true
102+
if: >-
103+
(steps.release.outcome == 'success' &&
104+
steps.release.outputs.released == 'true') ||
105+
inputs.force_publish_client == true
97106
uses: actions/upload-artifact@v5
98107
with:
99108
name: client-dist
@@ -132,7 +141,9 @@ jobs:
132141
id: check
133142
run: |
134143
# Check if there are any commits with (mcp) scope since last mcp release
135-
if git log $(git describe --tags --abbrev=0 --match="mcp-v*" 2>/dev/null || echo "HEAD~10")..HEAD --pretty=format:"%s" | grep -qE '^(feat|fix|perf)\(mcp\):'; then
144+
if git log $(git describe --tags --abbrev=0 --match="mcp-v*" 2>/dev/null || \
145+
echo "HEAD~10")..HEAD --pretty=format:"%s" | \
146+
grep -qE '^(feat|fix|perf)\(mcp\):'; then
136147
echo "has_changes=true" >> $GITHUB_OUTPUT
137148
else
138149
echo "has_changes=false" >> $GITHUB_OUTPUT
@@ -148,11 +159,17 @@ jobs:
148159
directory: katana_mcp_server
149160

150161
- name: Build MCP package
151-
if: (steps.release.outcome == 'success' && steps.release.outputs.released == 'true') || inputs.force_publish_mcp == true
162+
if: >-
163+
(steps.release.outcome == 'success' &&
164+
steps.release.outputs.released == 'true') ||
165+
inputs.force_publish_mcp == true
152166
run: cd katana_mcp_server && uv build
153167

154168
- name: Upload MCP build artifacts
155-
if: (steps.release.outcome == 'success' && steps.release.outputs.released == 'true') || inputs.force_publish_mcp == true
169+
if: >-
170+
(steps.release.outcome == 'success' &&
171+
steps.release.outputs.released == 'true') ||
172+
inputs.force_publish_mcp == true
156173
uses: actions/upload-artifact@v5
157174
with:
158175
name: mcp-dist

.github/workflows/update-mcp-dependency.yml

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,22 @@ jobs:
2828
run: |
2929
# Get the most recent client-v* tag
3030
LATEST_CLIENT_TAG=$(git tag --list 'client-v*' --sort=-version:refname | head -n 1)
31-
31+
3232
if [ -z "$LATEST_CLIENT_TAG" ]; then
3333
echo "No client tags found"
3434
echo "has_release=false" >> $GITHUB_OUTPUT
3535
exit 0
3636
fi
37-
37+
3838
# Extract version from tag (client-v0.28.0 -> 0.28.0)
3939
CLIENT_VERSION="${LATEST_CLIENT_TAG#client-v}"
4040
echo "client_version=$CLIENT_VERSION" >> $GITHUB_OUTPUT
41-
41+
4242
# Check if this tag was created in the last workflow run
4343
# Get the timestamp of the tag
4444
TAG_DATE=$(git log -1 --format=%ct "$LATEST_CLIENT_TAG")
4545
WORKFLOW_DATE=$(date -d "${{ github.event.workflow_run.created_at }}" +%s)
46-
46+
4747
# If tag is newer than workflow start (created during the workflow)
4848
if [ "$TAG_DATE" -ge "$WORKFLOW_DATE" ]; then
4949
echo "New client release detected: $CLIENT_VERSION"
@@ -59,9 +59,9 @@ jobs:
5959
run: |
6060
CLIENT_VERSION="${{ steps.check-release.outputs.client_version }}"
6161
CURRENT_DEP=$(grep "katana-openapi-client>=" katana_mcp_server/pyproject.toml | sed 's/.*>=\([0-9.]*\).*/\1/')
62-
62+
6363
echo "current_version=$CURRENT_DEP" >> $GITHUB_OUTPUT
64-
64+
6565
if [ "$CURRENT_DEP" = "$CLIENT_VERSION" ]; then
6666
echo "MCP dependency already up to date"
6767
echo "needs_update=false" >> $GITHUB_OUTPUT
@@ -82,10 +82,12 @@ jobs:
8282
if: steps.check-current.outputs.needs_update == 'true'
8383
run: |
8484
CLIENT_VERSION="${{ steps.check-release.outputs.client_version }}"
85-
85+
8686
# Update the dependency in pyproject.toml
87-
sed -i "s/katana-openapi-client>=.*\",/katana-openapi-client>=$CLIENT_VERSION\",/" katana_mcp_server/pyproject.toml
88-
87+
sed -i \
88+
"s/katana-openapi-client>=.*\",/katana-openapi-client>=$CLIENT_VERSION\",/" \
89+
katana_mcp_server/pyproject.toml
90+
8991
# Verify the change
9092
echo "Updated dependency:"
9193
grep "katana-openapi-client>=" katana_mcp_server/pyproject.toml
@@ -105,22 +107,27 @@ jobs:
105107
title: "feat(mcp): update client dependency to v${{ steps.check-release.outputs.client_version }}"
106108
body: |
107109
## Automated Dependency Update
108-
110+
109111
This PR updates the MCP server's client dependency to match the latest client release.
110-
112+
111113
**Changes:**
112-
- Update `katana-openapi-client` dependency from `>=${{ steps.check-current.outputs.current_version }}` to `>=${{ steps.check-release.outputs.client_version }}`
114+
- Update `katana-openapi-client` dependency from
115+
`>=${{ steps.check-current.outputs.current_version }}` to
116+
`>=${{ steps.check-release.outputs.client_version }}`
113117
- Update `uv.lock` to reflect the new dependency
114-
118+
115119
**Release Information:**
116120
- Client version: v${{ steps.check-release.outputs.client_version }}
117121
- Tag: client-v${{ steps.check-release.outputs.client_version }}
118-
122+
119123
**Next Steps:**
120-
When this PR is merged, it will trigger a new MCP server release with the conventional commit message `feat(mcp):`, which will bump the MCP server's MINOR version.
121-
124+
When this PR is merged, it will trigger a new MCP server release with the
125+
conventional commit message `feat(mcp):`, which will bump the MCP server's
126+
MINOR version.
127+
122128
---
123-
*This PR was automatically created by the [Update MCP Client Dependency workflow](.github/workflows/update-mcp-dependency.yml).*
129+
*This PR was automatically created by the
130+
[Update MCP Client Dependency workflow](.github/workflows/update-mcp-dependency.yml).*
124131
branch: auto/update-mcp-dependency-v${{ steps.check-release.outputs.client_version }}
125132
delete-branch: true
126133
labels: |

.yamllint.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# yamllint configuration for katana-openapi-client
2+
# See https://yamllint.readthedocs.io/ for documentation
3+
4+
extends: default
5+
6+
rules:
7+
line-length:
8+
max: 120
9+
level: error
10+
# Ignore line length in OpenAPI spec files
11+
ignore: |
12+
/docs/katana-openapi.yaml
13+
14+
document-start:
15+
# Don't require --- at start of YAML files
16+
present: false
17+
18+
truthy:
19+
# Allow various boolean representations
20+
allowed-values: ['true', 'false', 'yes', 'no', 'True', 'False']
21+
22+
comments:
23+
min-spaces-from-content: 1
24+
25+
indentation:
26+
spaces: 2
27+
28+
brackets:
29+
min-spaces-inside: 0
30+
max-spaces-inside: 1
31+
32+
braces:
33+
min-spaces-inside: 0
34+
max-spaces-inside: 1
35+
36+
# Ignore these paths completely
37+
ignore: |
38+
.venv/
39+
node_modules/
40+
dist/
41+
build/
42+
*.egg-info/
43+
katana_public_api_client/generated/
44+
docs/original openapi files/

katana_mcp_server/src/katana_mcp/tools/foundation/manufacturing_orders.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
from katana_mcp.logging import observe_tool
1919
from katana_mcp.services import get_services
20+
from katana_mcp.tools.schemas import ConfirmationSchema
2021
from katana_mcp.unpack import Unpack, unpack_pydantic_params
2122
from katana_public_api_client.client_types import UNSET
2223
from katana_public_api_client.models import (
@@ -138,7 +139,47 @@ async def _create_manufacturing_order_impl(
138139
message=f"Preview: Manufacturing order for variant {request.variant_id}, quantity {request.planned_quantity}",
139140
)
140141

141-
# Confirm mode - create the manufacturing order via API
142+
# Confirm mode - use elicitation to get user confirmation before creating
143+
elicit_result = await context.elicit(
144+
f"Create manufacturing order for variant {request.variant_id} with quantity {request.planned_quantity}?",
145+
ConfirmationSchema,
146+
)
147+
148+
# Check if user accepted
149+
if elicit_result.action != "accept":
150+
logger.info(
151+
f"User did not accept creation of manufacturing order for variant {request.variant_id}"
152+
)
153+
return ManufacturingOrderResponse(
154+
variant_id=request.variant_id,
155+
planned_quantity=request.planned_quantity,
156+
location_id=request.location_id,
157+
order_created_date=request.order_created_date,
158+
production_deadline_date=request.production_deadline_date,
159+
additional_info=request.additional_info,
160+
is_preview=True,
161+
message="Manufacturing order creation cancelled by user",
162+
next_actions=["Review the order details and try again with confirm=true"],
163+
)
164+
165+
# Type narrowing: at this point we know action == "accept", so data exists
166+
if not elicit_result.data.confirm:
167+
logger.info(
168+
f"User declined to confirm creation of manufacturing order for variant {request.variant_id}"
169+
)
170+
return ManufacturingOrderResponse(
171+
variant_id=request.variant_id,
172+
planned_quantity=request.planned_quantity,
173+
location_id=request.location_id,
174+
order_created_date=request.order_created_date,
175+
production_deadline_date=request.production_deadline_date,
176+
additional_info=request.additional_info,
177+
is_preview=True,
178+
message="Manufacturing order creation declined by user",
179+
next_actions=["Review the order details and try again with confirm=true"],
180+
)
181+
182+
# User confirmed - create the manufacturing order via API
142183
try:
143184
services = get_services(context)
144185

0 commit comments

Comments
 (0)