|
| 1 | +# Design: `plan_feedback` MCP Tool |
| 2 | + |
| 3 | +**Date:** 2026-03-28 |
| 4 | +**Proposal:** [127-mcp-feedback.md](../../proposals/127-mcp-feedback.md) |
| 5 | + |
| 6 | +## Summary |
| 7 | + |
| 8 | +Add a `plan_feedback` MCP tool that allows LLM consumers to submit structured feedback about the PlanExe MCP interface, plan quality, and workflow experiences. Feedback is stored in PostgreSQL for later analysis. The tool is non-blocking and fire-and-forget: it always returns success to the caller even if storage fails internally. |
| 9 | + |
| 10 | +## Parameters |
| 11 | + |
| 12 | +| Parameter | Type | Required | Description | |
| 13 | +|-----------|------|----------|-------------| |
| 14 | +| `category` | enum | yes | One of: `sse_issue`, `status_staleness`, `queue_delay`, `file_visibility`, `plan_quality`, `tool_description`, `workflow`, `performance`, `error_handling`, `suggestion`, `compliment`, `other` | |
| 15 | +| `message` | string | yes | Free-text feedback (concise, actionable) | |
| 16 | +| `plan_id` | string\|null | no | UUID to attach feedback to a specific plan | |
| 17 | +| `rating` | integer 1-5\|null | no | Satisfaction score | |
| 18 | +| `severity` | enum\|null | no | `low`, `medium`, or `high` (for issue reports) | |
| 19 | + |
| 20 | +## Response |
| 21 | + |
| 22 | +### Success (always returned to caller) |
| 23 | + |
| 24 | +```json |
| 25 | +{ |
| 26 | + "feedback_id": "uuid", |
| 27 | + "received_at": "2026-03-28T14:30:00Z", |
| 28 | + "message": "Feedback received. Thank you." |
| 29 | +} |
| 30 | +``` |
| 31 | + |
| 32 | +### Validation Errors |
| 33 | + |
| 34 | +```json |
| 35 | +{ |
| 36 | + "error": { |
| 37 | + "code": "INVALID_FEEDBACK", |
| 38 | + "message": "Human-readable validation error" |
| 39 | + } |
| 40 | +} |
| 41 | +``` |
| 42 | + |
| 43 | +```json |
| 44 | +{ |
| 45 | + "error": { |
| 46 | + "code": "PLAN_NOT_FOUND", |
| 47 | + "message": "Plan not found: <plan_id>" |
| 48 | + } |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +## Database Table: `feedback_item` |
| 53 | + |
| 54 | +| Column | Type | Notes | |
| 55 | +|--------|------|-------| |
| 56 | +| `id` | VARCHAR(36) | PK, UUID generated server-side | |
| 57 | +| `received_at` | TIMESTAMP | UTC, indexed, default now() | |
| 58 | +| `category` | VARCHAR(32) | One of 12 enum values | |
| 59 | +| `message` | TEXT | Free-text feedback | |
| 60 | +| `plan_id` | VARCHAR(36) | Nullable, references task_item(id) | |
| 61 | +| `rating` | INTEGER | Nullable, 1-5 | |
| 62 | +| `severity` | VARCHAR(8) | Nullable: low/medium/high | |
| 63 | +| `user_id` | VARCHAR(36) | Nullable, resolved from auth context | |
| 64 | +| `plan_progress_pct` | FLOAT | Nullable, snapshot at feedback time | |
| 65 | +| `plan_state` | VARCHAR(16) | Nullable, snapshot at feedback time | |
| 66 | +| `plan_current_step` | VARCHAR(128) | Nullable, snapshot at feedback time | |
| 67 | + |
| 68 | +No foreign key constraint on `plan_id` to keep writes simple and avoid blocking on plan table locks. |
| 69 | + |
| 70 | +## Files to Create/Modify |
| 71 | + |
| 72 | +### New File |
| 73 | +- `database_api/model_feedback.py` — SQLAlchemy model `FeedbackItem` |
| 74 | + |
| 75 | +### Modified Files |
| 76 | +- `mcp_cloud/tool_models.py` — Add `PlanFeedbackInput`, `PlanFeedbackOutput` Pydantic models |
| 77 | +- `mcp_cloud/schemas.py` — Add schema constants and `ToolDefinition` entry |
| 78 | +- `mcp_cloud/handlers.py` — Add `handle_plan_feedback()`, register in `TOOL_HANDLERS` |
| 79 | +- `mcp_cloud/db_setup.py` — Import model, add `PlanFeedbackRequest`, update `PLANEXE_SERVER_INSTRUCTIONS` |
| 80 | +- `mcp_cloud/db_queries.py` — Add `_create_feedback_sync()` and `_get_plan_snapshot_for_feedback_sync()` |
| 81 | + |
| 82 | +## Handler Logic |
| 83 | + |
| 84 | +1. Parse and validate input via `PlanFeedbackRequest` (Pydantic BaseModel) |
| 85 | +2. If `plan_id` provided: |
| 86 | + - Look up plan via `_get_plan_snapshot_for_feedback_sync()` |
| 87 | + - If not found, return `PLAN_NOT_FOUND` error (this is the only error visible to caller) |
| 88 | + - If found, capture snapshot: `progress_percentage`, `state`, `current_step` |
| 89 | +3. Generate `feedback_id` (UUID4) and `received_at` (UTC now) |
| 90 | +4. Write to DB in try/except: |
| 91 | + - On success: return `{feedback_id, received_at, message}` |
| 92 | + - On failure: **log the error**, still return success response (fire-and-forget) |
| 93 | +5. Response is always `isError=False` except for `PLAN_NOT_FOUND` and `INVALID_FEEDBACK` |
| 94 | + |
| 95 | +## Tool Annotations |
| 96 | + |
| 97 | +```python |
| 98 | +annotations={ |
| 99 | + "readOnlyHint": False, # writes to DB |
| 100 | + "destructiveHint": False, # no destructive side effects |
| 101 | + "idempotentHint": True, # duplicate submissions are safe |
| 102 | + "openWorldHint": False, # no external calls |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | +## Server Instructions Update |
| 107 | + |
| 108 | +Add to `PLANEXE_SERVER_INSTRUCTIONS`: |
| 109 | +> "Use plan_feedback to report issues or share observations about plan quality, workflow friction, or the MCP interface. Feedback is fire-and-forget and never blocks the workflow." |
| 110 | +
|
| 111 | +## Behavioral Guarantees |
| 112 | + |
| 113 | +- Non-blocking: handler returns in <1 second |
| 114 | +- Fire-and-forget: never gates workflow |
| 115 | +- No rate limiting on LLM consumers |
| 116 | +- Always returns success to caller (except validation/plan-not-found errors) |
| 117 | +- Internal storage failures are logged but not surfaced to caller |
| 118 | + |
| 119 | +## Testing |
| 120 | + |
| 121 | +Follow existing test patterns (e.g., `test_plan_create_tool.py`): |
| 122 | +- Valid feedback with all fields |
| 123 | +- Valid feedback with only required fields |
| 124 | +- Invalid category returns INVALID_FEEDBACK |
| 125 | +- Invalid plan_id returns PLAN_NOT_FOUND |
| 126 | +- Rating out of range returns INVALID_FEEDBACK |
| 127 | +- Invalid severity returns INVALID_FEEDBACK |
| 128 | +- DB write failure still returns success (fire-and-forget) |
0 commit comments