1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
|
# Story-Level Chatbot MCP Surface Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add `create_story`, `get_story`, `list_stories`, and `accept_story` to the chatbot-facing MCP server (`/chatbot/mcp`), so a chatbot can create and drive a Story the same way it already creates and drives a plain Task — closing the "MCP surface: story-level tools, additive to what exists" gap from the recursive-story-decomposition design spec.
**Architecture:** Both new tools and the existing REST endpoints (`internal/api/stories.go`) must share one service-layer implementation, mirroring how `internal/api/taskops.go`'s `acceptTask`/`rejectTask`/`cancelTask` already back both `internal/api/server.go`'s REST handlers and `internal/api/chatbotmcp.go`'s `accept_task`/`reject_task`/`cancel_task` tools — so the two surfaces cannot drift. `create_story`/`get_story`/`list_stories` are thin wrappers around the existing `storage.DB.CreateStory`/`GetStory`/`ListStories` (no new service-layer method needed, since there's no shared state-transition logic to protect — unlike accept, which does need one, exactly like `acceptTask`).
**Tech Stack:** Go, `github.com/modelcontextprotocol/go-sdk/mcp`, existing `internal/story`/`internal/storage` packages.
## Global Constraints
- Every new MCP tool must mirror an existing sibling tool's exact code shape in `internal/api/chatbotmcp.go` (input struct with `jsonschema` tags, `mcp.AddTool` registration, `mcpJSON`/`mcpText` for output) — this file has zero deviation from that shape today and this plan must not introduce any.
- `story.Story` has **no `repository_url` field** — only the loose-reference `ProjectID` (mirrors `tasks.project`, unenforced, no automatic resolution to a repo). The design spec's prose lists `create_story`'s inputs as "(name, spec, acceptance_criteria, repository_url → story_id)", but that field does not exist on the data model and nothing yet resolves a repo from a story (that requires the deferred root-task-spawning piece of the recursive-decomposition design). This plan's `create_story` accepts `project_id` instead — do not add a `repository_url` field to `story.Story` or to storage as part of this plan; that would be schema scope well beyond an additive MCP wrapper.
- Do not touch `internal/scheduler/story_orchestrator.go` or anything else in the recursive-decomposition design's larger, still-deferred pieces (per-node arbitrated review generalization, `builder` role system-prompt authoring). This plan is scoped to the MCP surface only.
---
## Task 1: Extract `acceptStory` service-layer helper
**Files:**
- Modify: `internal/api/stories.go` (add `errStoryNotFound`, add `acceptStory` method, refactor `handleAcceptStory` to call it)
- Modify: `internal/api/taskops.go` (map `errStoryNotFound` to 404 in `writeOpError`)
- Test: `internal/api/stories_test.go` (existing `TestServer_AcceptStory_*` tests must still pass unmodified — this task's own verification, no new tests needed)
**Interfaces:**
- Produces: `func (s *Server) acceptStory(id string, actor event.Actor) error` — Task 2 calls this directly from the new `accept_story` MCP tool.
- [ ] **Step 1: Add `errStoryNotFound` and the `acceptStory` helper**
In `internal/api/stories.go`, add `"errors"` to the import block (it currently imports `encoding/json`, `net/http`, `strconv`, `github.com/google/uuid`, `.../internal/event`, `.../internal/storage`, `.../internal/story`):
```go
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"github.com/google/uuid"
"github.com/thepeterstone/claudomator/internal/event"
"github.com/thepeterstone/claudomator/internal/storage"
"github.com/thepeterstone/claudomator/internal/story"
)
```
Directly after the imports, add:
```go
// errStoryNotFound is returned when a story ID does not resolve. REST maps
// it to 404 (via writeOpError, mirroring errTaskNotFound in taskops.go); MCP
// surfaces it as a tool error.
var errStoryNotFound = errors.New("story not found")
```
Directly before `handleAcceptStory` (which you'll replace in Step 2), add this new method:
```go
// acceptStory is the shared service-layer accept-gate for a story: both
// handleAcceptStory (REST) and the accept_story chatbot MCP tool call this,
// mirroring taskops.go's shared task-operation pattern (see acceptTask) so
// the two surfaces cannot drift.
func (s *Server) acceptStory(id string, actor event.Actor) error {
st, err := s.store.GetStory(id)
if err != nil {
return errStoryNotFound
}
if st.Status != "REVIEW_READY" {
return conflictf("story cannot be accepted from status %s", st.Status)
}
from := st.Status
st.Status = "DONE"
if err := s.store.UpdateStory(st); err != nil {
return err
}
payload, _ := json.Marshal(struct {
StoryID string `json:"story_id"`
From string `json:"from"`
To string `json:"to"`
}{StoryID: id, From: from, To: "DONE"})
if err := s.store.CreateEvent(&event.Event{
TaskID: id,
Kind: event.KindHumanAccepted,
Actor: actor,
Payload: payload,
}); err != nil {
s.logger.Error("failed to record human_accepted event", "storyID", id, "error", err)
}
return nil
}
```
- [ ] **Step 2: Refactor `handleAcceptStory` to call it**
Find the existing `handleAcceptStory` function (its whole body, including the doc comment above it):
```go
// handleAcceptStory is the human accept-gate for a story: it mirrors
// handleAcceptTask's pattern exactly (validate current state, transition,
// emit an event) but operates on story.Story, which (unlike task.Task) has
// no enforced state machine (story.UpdateStory is unvalidated pass-through —
// see internal/story's doc comments) — so the "only valid from REVIEW_READY"
// check is done here, not in the storage layer.
func (s *Server) handleAcceptStory(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
st, err := s.store.GetStory(id)
if err != nil {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "story not found"})
return
}
if st.Status != "REVIEW_READY" {
writeJSON(w, http.StatusConflict, map[string]string{"error": "story cannot be accepted from status " + st.Status})
return
}
from := st.Status
st.Status = "DONE"
if err := s.store.UpdateStory(st); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
payload, _ := json.Marshal(struct {
StoryID string `json:"story_id"`
From string `json:"from"`
To string `json:"to"`
}{StoryID: id, From: from, To: "DONE"})
if err := s.store.CreateEvent(&event.Event{
TaskID: id,
Kind: event.KindHumanAccepted,
Actor: event.ActorUser,
Payload: payload,
}); err != nil {
s.logger.Error("failed to record human_accepted event", "storyID", id, "error", err)
}
writeJSON(w, http.StatusOK, map[string]string{"message": "story accepted", "story_id": id})
}
```
Replace it with:
```go
// handleAcceptStory is the human accept-gate for a story: it mirrors
// handleAcceptTask's pattern exactly (validate current state, transition,
// emit an event), delegating to the shared acceptStory service-layer helper
// so the chatbot MCP accept_story tool cannot drift from this REST behavior.
func (s *Server) handleAcceptStory(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if err := s.acceptStory(id, event.ActorUser); err != nil {
writeOpError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]string{"message": "story accepted", "story_id": id})
}
```
- [ ] **Step 3: Map `errStoryNotFound` to 404 in `writeOpError`**
In `internal/api/taskops.go`, find `writeOpError`:
```go
// writeOpError maps a service-layer error to the appropriate REST status.
func writeOpError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, errTaskNotFound):
writeJSON(w, http.StatusNotFound, map[string]string{"error": "task not found"})
return
}
var ce *conflictError
if errors.As(err, &ce) {
writeJSON(w, http.StatusConflict, map[string]string{"error": ce.Error()})
return
}
var be *badRequestError
if errors.As(err, &be) {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": be.Error()})
return
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
```
Replace the `switch` block with:
```go
switch {
case errors.Is(err, errTaskNotFound):
writeJSON(w, http.StatusNotFound, map[string]string{"error": "task not found"})
return
case errors.Is(err, errStoryNotFound):
writeJSON(w, http.StatusNotFound, map[string]string{"error": "story not found"})
return
}
```
- [ ] **Step 4: Run the existing story tests to confirm the refactor is behavior-preserving**
Run: `go test ./internal/api/ -run TestServer_AcceptStory -v`
Expected: `TestServer_AcceptStory_SucceedsFromReviewReady`, `TestServer_AcceptStory_FailsFromOtherStatuses`, and `TestServer_AcceptStory_NotFound` all PASS unmodified — this task is a pure refactor, so these pre-existing tests are the proof it preserves REST behavior exactly. No new tests are added in this task.
- [ ] **Step 5: Run the full package suite, then the full repo suite**
Run: `go test ./internal/api/...`
Expected: PASS for everything.
Then run: `go test ./...` (the entire repo). A prior unrelated plan in this repo (`docs/superpowers/plans/2026-07-08-subtask-ordering-and-verdict-reporting.md`, Task 4) found that task-scoped tests passing is not sufficient — a separate package's code can fail to compile due to a change here. This task doesn't change any shared interface, but verify the full suite anyway rather than assuming. Paste the actual output.
- [ ] **Step 6: Commit**
```bash
git add internal/api/stories.go internal/api/taskops.go
git commit -m "refactor(api): extract acceptStory service-layer helper, mirroring acceptTask"
```
---
## Task 2: Add `create_story`/`get_story`/`list_stories`/`accept_story` chatbot MCP tools
**Files:**
- Modify: `internal/api/chatbotmcp.go` (new input structs + 4 new `mcp.AddTool` registrations)
- Test: `internal/api/chatbotmcp_test.go` (append)
**Interfaces:**
- Consumes: `s.acceptStory` (Task 1), `s.store.CreateStory`/`GetStory`/`ListStories` (existing, `internal/storage/story.go`), `storage.StoryFilter{Status, EpicID}` (existing).
- Produces: nothing new for later work — this is the last piece of the story-level MCP surface described in the design spec.
- [ ] **Step 1: Add the new input structs and imports**
In `internal/api/chatbotmcp.go`, the import block currently reads:
```go
import (
"context"
"crypto/subtle"
"encoding/json"
"net/http"
"strings"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/thepeterstone/claudomator/internal/event"
"github.com/thepeterstone/claudomator/internal/storage"
"github.com/thepeterstone/claudomator/internal/task"
)
```
Replace it with (adding `github.com/google/uuid` and `.../internal/story`):
```go
import (
"context"
"crypto/subtle"
"encoding/json"
"net/http"
"strings"
"time"
"github.com/google/uuid"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/thepeterstone/claudomator/internal/event"
"github.com/thepeterstone/claudomator/internal/storage"
"github.com/thepeterstone/claudomator/internal/story"
"github.com/thepeterstone/claudomator/internal/task"
)
```
Directly after the existing `rejectTaskMCPInput` struct (find it — ends with its closing `}`, directly before `type compactTask struct`), add these three new input structs:
```go
type createStoryMCPInput struct {
Name string `json:"name" jsonschema:"short human-readable story name"`
Spec string `json:"spec,omitempty" jsonschema:"prose description of the intent -- what a human is asking for and why"`
AcceptanceCriteria []string `json:"acceptance_criteria,omitempty" jsonschema:"optional list of concrete criteria the finished work must satisfy"`
EpicID string `json:"epic_id,omitempty" jsonschema:"optional epic ID to group this story under"`
ProjectID string `json:"project_id,omitempty" jsonschema:"optional loose-reference project ID; not yet used to automatically resolve a repository"`
Priority string `json:"priority,omitempty" jsonschema:"optional priority, e.g. high, normal, low"`
}
type listStoriesMCPInput struct {
Status string `json:"status,omitempty" jsonschema:"optional status filter, e.g. DISCOVERY, IN_PROGRESS, REVIEW_READY, DONE"`
EpicID string `json:"epic_id,omitempty" jsonschema:"optional epic ID filter"`
}
type storyIDInput struct {
StoryID string `json:"story_id" jsonschema:"the story ID"`
}
```
- [ ] **Step 2: Register the four new tools**
In the same file, find the `cancel_task` tool registration (the last `mcp.AddTool` call before `return srv`):
```go
mcp.AddTool(srv, &mcp.Tool{
Name: "cancel_task",
Description: "Cancel a RUNNING or QUEUED task.",
}, func(_ context.Context, _ *mcp.CallToolRequest, in taskIDInput) (*mcp.CallToolResult, any, error) {
msg, err := s.cancelTask(in.TaskID, event.ActorChatbot)
if err != nil {
return nil, nil, err
}
return mcpText(msg), nil, nil
})
return srv
```
Replace it with (adding the four new registrations directly after `cancel_task`, before `return srv`):
```go
mcp.AddTool(srv, &mcp.Tool{
Name: "cancel_task",
Description: "Cancel a RUNNING or QUEUED task.",
}, func(_ context.Context, _ *mcp.CallToolRequest, in taskIDInput) (*mcp.CallToolResult, any, error) {
msg, err := s.cancelTask(in.TaskID, event.ActorChatbot)
if err != nil {
return nil, nil, err
}
return mcpText(msg), nil, nil
})
mcp.AddTool(srv, &mcp.Tool{
Name: "create_story",
Description: "Create a story representing a piece of user intent -- a shippable slice of work realized by a tree of tasks. Use this instead of submit_task when the ask needs to be broken down rather than run as a single step. Returns the new story.",
}, func(_ context.Context, _ *mcp.CallToolRequest, in createStoryMCPInput) (*mcp.CallToolResult, any, error) {
if strings.TrimSpace(in.Name) == "" {
return nil, nil, badRequestf("name is required")
}
st := &story.Story{
ID: uuid.New().String(),
Name: in.Name,
Status: "DISCOVERY",
DiscoverySource: "agent",
Spec: in.Spec,
AcceptanceCriteria: in.AcceptanceCriteria,
EpicID: in.EpicID,
ProjectID: in.ProjectID,
Priority: in.Priority,
}
if st.AcceptanceCriteria == nil {
st.AcceptanceCriteria = []string{}
}
if err := s.store.CreateStory(st); err != nil {
return nil, nil, err
}
return mcpJSON(st)
})
mcp.AddTool(srv, &mcp.Tool{
Name: "get_story",
Description: "Get one story by ID.",
}, func(_ context.Context, _ *mcp.CallToolRequest, in storyIDInput) (*mcp.CallToolResult, any, error) {
st, err := s.store.GetStory(in.StoryID)
if err != nil {
return nil, nil, errStoryNotFound
}
return mcpJSON(st)
})
mcp.AddTool(srv, &mcp.Tool{
Name: "list_stories",
Description: "List stories, optionally filtered by status and/or epic_id.",
}, func(_ context.Context, _ *mcp.CallToolRequest, in listStoriesMCPInput) (*mcp.CallToolResult, any, error) {
stories, err := s.store.ListStories(storage.StoryFilter{Status: in.Status, EpicID: in.EpicID})
if err != nil {
return nil, nil, err
}
if stories == nil {
stories = []*story.Story{}
}
return mcpJSON(stories)
})
mcp.AddTool(srv, &mcp.Tool{
Name: "accept_story",
Description: "Accept a REVIEW_READY story, marking it DONE. This is the single human/chatbot touchpoint for a whole story's pipeline -- the underlying builder/evaluator/arbitration tasks are auto-accepted by the story orchestrator.",
}, func(_ context.Context, _ *mcp.CallToolRequest, in storyIDInput) (*mcp.CallToolResult, any, error) {
if err := s.acceptStory(in.StoryID, event.ActorChatbot); err != nil {
return nil, nil, err
}
return mcpText("Story accepted."), nil, nil
})
return srv
```
- [ ] **Step 3: Write the failing tests**
First, update the import block at the top of `internal/api/chatbotmcp_test.go`. It currently reads:
```go
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/thepeterstone/claudomator/internal/storage"
"github.com/thepeterstone/claudomator/internal/task"
)
```
Replace it with (adding `.../internal/event` and `.../internal/story`):
```go
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/thepeterstone/claudomator/internal/event"
"github.com/thepeterstone/claudomator/internal/storage"
"github.com/thepeterstone/claudomator/internal/story"
"github.com/thepeterstone/claudomator/internal/task"
)
```
Then append the following tests to the end of the file:
```go
func TestChatbotMCP_CreateStory_AndGetStory(t *testing.T) {
srv, _ := testServer(t)
srv.SetAPIToken("s3cret")
cs := chatbotClient(t, srv, "s3cret")
out := callText(t, cs, "create_story", map[string]any{
"name": "Add login page",
"spec": "Users should be able to log in with email + password",
"acceptance_criteria": []string{"User can log in", "Invalid credentials show an error"},
})
var created story.Story
if err := json.Unmarshal([]byte(out), &created); err != nil {
t.Fatalf("unmarshal create_story result %q: %v", out, err)
}
if created.ID == "" {
t.Fatal("create_story returned empty story ID")
}
if created.Status != "DISCOVERY" {
t.Errorf("Status: want DISCOVERY, got %q", created.Status)
}
out = callText(t, cs, "get_story", map[string]any{"story_id": created.ID})
var got story.Story
if err := json.Unmarshal([]byte(out), &got); err != nil {
t.Fatalf("unmarshal get_story result %q: %v", out, err)
}
if got.Name != "Add login page" {
t.Errorf("Name: want %q, got %q", "Add login page", got.Name)
}
if len(got.AcceptanceCriteria) != 2 {
t.Errorf("AcceptanceCriteria: want 2 items, got %+v", got.AcceptanceCriteria)
}
}
func TestChatbotMCP_CreateStory_MissingName_ReturnsToolError(t *testing.T) {
srv, _ := testServer(t)
srv.SetAPIToken("s3cret")
cs := chatbotClient(t, srv, "s3cret")
res, err := cs.CallTool(context.Background(), &mcp.CallToolParams{Name: "create_story", Arguments: map[string]any{}})
if err != nil {
t.Fatalf("CallTool transport error: %v", err)
}
if !res.IsError {
t.Fatal("expected create_story with no name to return a tool error")
}
}
func TestChatbotMCP_GetStory_NotFound_ReturnsToolError(t *testing.T) {
srv, _ := testServer(t)
srv.SetAPIToken("s3cret")
cs := chatbotClient(t, srv, "s3cret")
res, err := cs.CallTool(context.Background(), &mcp.CallToolParams{Name: "get_story", Arguments: map[string]any{"story_id": "nope"}})
if err != nil {
t.Fatalf("CallTool transport error: %v", err)
}
if !res.IsError {
t.Fatal("expected get_story with unknown ID to return a tool error")
}
}
func TestChatbotMCP_ListStories_FiltersByStatus(t *testing.T) {
srv, store := testServer(t)
srv.SetAPIToken("s3cret")
cs := chatbotClient(t, srv, "s3cret")
if err := store.CreateStory(&story.Story{ID: "s1", Name: "One", Status: "DISCOVERY", AcceptanceCriteria: []string{}}); err != nil {
t.Fatalf("seed story 1: %v", err)
}
if err := store.CreateStory(&story.Story{ID: "s2", Name: "Two", Status: "DONE", AcceptanceCriteria: []string{}}); err != nil {
t.Fatalf("seed story 2: %v", err)
}
out := callText(t, cs, "list_stories", map[string]any{"status": "DONE"})
var got []story.Story
if err := json.Unmarshal([]byte(out), &got); err != nil {
t.Fatalf("unmarshal list_stories result %q: %v", out, err)
}
if len(got) != 1 || got[0].ID != "s2" {
t.Errorf("expected only story s2, got %+v", got)
}
}
func TestChatbotMCP_AcceptStory_SucceedsFromReviewReady(t *testing.T) {
srv, store := testServer(t)
srv.SetAPIToken("s3cret")
cs := chatbotClient(t, srv, "s3cret")
if err := store.CreateStory(&story.Story{ID: "s1", Name: "One", Status: "REVIEW_READY", AcceptanceCriteria: []string{}}); err != nil {
t.Fatalf("seed story: %v", err)
}
callText(t, cs, "accept_story", map[string]any{"story_id": "s1"})
got, err := store.GetStory("s1")
if err != nil {
t.Fatalf("get story after accept: %v", err)
}
if got.Status != "DONE" {
t.Errorf("Status: want DONE, got %q", got.Status)
}
events, err := store.ListEvents("s1", 0)
if err != nil {
t.Fatalf("list events: %v", err)
}
found := false
for _, e := range events {
if e.Kind == event.KindHumanAccepted && e.Actor == event.ActorChatbot {
found = true
}
}
if !found {
t.Error("expected a KindHumanAccepted event with ActorChatbot")
}
}
func TestChatbotMCP_AcceptStory_FailsFromOtherStatus(t *testing.T) {
srv, store := testServer(t)
srv.SetAPIToken("s3cret")
cs := chatbotClient(t, srv, "s3cret")
if err := store.CreateStory(&story.Story{ID: "s1", Name: "One", Status: "IN_PROGRESS", AcceptanceCriteria: []string{}}); err != nil {
t.Fatalf("seed story: %v", err)
}
res, err := cs.CallTool(context.Background(), &mcp.CallToolParams{Name: "accept_story", Arguments: map[string]any{"story_id": "s1"}})
if err != nil {
t.Fatalf("CallTool transport error: %v", err)
}
if !res.IsError {
t.Fatal("expected accept_story from IN_PROGRESS to return a tool error")
}
}
```
- [ ] **Step 4: Run tests to verify they fail**
Run: `go test ./internal/api/ -run 'TestChatbotMCP_(CreateStory|GetStory|ListStories|AcceptStory)' -v`
Expected: compile failure — `create_story`/`get_story`/`list_stories`/`accept_story` tools don't exist yet. Paste the actual output.
- [ ] **Step 5: Run tests to verify they pass**
Run the same command as Step 4.
Expected: PASS, all 6 new tests. Paste the actual output.
- [ ] **Step 6: Run the full package suite, then the full repo suite**
Run: `go test ./internal/api/...`
Expected: PASS for everything, including the pre-existing `TestChatbotMCP_*` and `TestServer_*Story*` tests.
Then run: `go test ./...` (the entire repo). This must also pass — paste the actual output.
- [ ] **Step 7: Commit**
```bash
git add internal/api/chatbotmcp.go internal/api/chatbotmcp_test.go
git commit -m "feat(api): add create_story/get_story/list_stories/accept_story to chatbot MCP"
```
---
## Final Verification
- [ ] Run `go build ./...` — passes.
- [ ] Run `go test ./...` — passes, full repo.
- [ ] Run `grep -rn "create_story\|get_story\|list_stories\|accept_story\|acceptStory\|errStoryNotFound" internal/` to visually confirm every file in both tasks' File Maps was actually touched.
|