| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| Invoke | /mcp-toolkit:scenario |
| Category | cocode-flutter |
| Complexity | high |
| MCP Servers | mcp_flutter, marionette, maestro, dart |
/mcp-toolkit scenario โ ์๋๋ฆฌ์ค ํ ๋ฒ์ ์๋ ์คํ#
ํ๋ง๋๋ก#
์ฌ๋ฌ ๋จ๊ณ์ ์ฑ ํ ์คํธ๋ฅผ ๊ฐ๋ณธ(YAML ํ์ผ) ํ๋์ ์ ์ด๋๋ฉด, ์์๋๋ก ์๋์ผ๋ก ์คํํด ์ฃผ๋ ๋๊ตฌ์ ๋๋ค. ์ฐ๊ทน ๋๋ณธ์ฒ๋ผ "์ค๋น โ ๋ณธ ์ฐ๊ธฐ โ ์ ๋ฆฌ"๋ฅผ ๋ฏธ๋ฆฌ ์ ์ด ๋๋ฉด, ๋ฐฐ์ฐ(์ฑ) ๋์ ์๋์ผ๋ก ๋ฌด๋๋ฅผ ์งํํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋กํด ์ค๋๋ค.
๋๊ฐยท์ธ์ ์ฐ๋์#
- ์ฑ์ ๋ณต์กํ ํ๋ฆ์ ์๋์ผ๋ก ๊ฒ์ฆํ๊ณ ์ถ์ ๊ฐ๋ฐ์/QA ๋ด๋น์
- ์: "๋ก๊ทธ์ธ โ ๋ฐ์ดํฐ ์ค๋น โ ํ๋ฉด์์ ๋น๊ฒจ์ ์๋ก๊ณ ์นจ โ ๊ฒฐ๊ณผ๊ฐ ๋ง๋์ง ํ์ธ โ ํ๋ฉด ์บก์ฒ"์ฒ๋ผ ์ฌ๋ฌ ๋์์ด ์์ธ ์๋๋ฆฌ์ค๋ฅผ ๋ฐ๋ณต ์คํํด์ผ ํ ๋
- ํนํ ์ฑ ๋ด๋ถ ์ํ ํ์ธ + ํ๋ฉด ํฐ์น + ํ๋ฉด ์ด๋ฏธ์ง ๋น๊ต๊ฐ ํ๊บผ๋ฒ์ ํ์ํ ๊ฒฝ์ฐ(cocode์ ๋ํ์ ์ธ ์ฌ์ฉ ์ฌ๋ก)์ ์ ํฉํฉ๋๋ค.
๐ ์ด๋ด ๋ ์ฐ์ง ๋ง์ธ์: ๋จ์ํ ํ๋ฉด์ ํฐ์นํ๊ณ ์คํฌ๋ฆฐ์ท๋ง ์ฐ๋ ์ ๋๋ผ๋ฉด, ๋ ๊ฐ๋ฒผ์ด cc-flutter-marionette:sweep๋
cc-flutter-pixel-loop:loop๋ฅผ ์ฐ๋ ๊ฒ ๋ซ์ต๋๋ค.
๋ฌด์์ ํด์ฃผ๋์#
๊ฐ๋ณธ๋๋ก ์คํํ ๋ค, ๊ฒฐ๊ณผ๋ฅผ ํด๋ ํ๋์ ์ ๋ฆฌํด ์ค๋๋ค. ํด๋ ์์๋:
scenario.yamlโ ์ค์ ๋ก ์คํํ ๊ฐ๋ณธ ์ฌ๋ณธsteps/โ ๊ฐ ๋จ๊ณ๋ณ๋ก ๋ฌด์์ ํ๊ณ ์ด๋ค ๊ฐ์ด ๋์์๋์ง(01.json๋ฑ)screens/โ ์๋๋ฆฌ์ค ์ค ์ฐ์ ํ๋ฉด ์บก์ฒ ์ด๋ฏธ์งbloc-dumps/โ ์ฑ ๋ด๋ถ ์ํ ๊ธฐ๋กteardown.jsonโ ๋ง๋ฌด๋ฆฌ ์ ๋ฆฌ ๋จ๊ณ ๊ธฐ๋กsummary.mdโ ์ฑ๊ณต/์คํจ ์์ฝ(๊ฑธ๋ฆฐ ์๊ฐ ํฌํจ)
์ด๋ป๊ฒ ์ฐ๋์#
# ๊ฐ๋ณธ ํ์ผ์ ์ง์ ํด ์คํ
/mcp-toolkit scenario --file test/scenarios/feed_pull_to_refresh.yaml
# ๊ฒฐ๊ณผ ์ ์ฅ ํด๋๋ฅผ ์ง์ ์ง์ (์๋ตํ๋ฉด ์๋ ํด๋์ ์ ์ฅ)
/mcp-toolkit scenario --file ... --output .claude/docs/mcp-toolkit/my-run/
# ์ค์ ์คํ ์์ด ๊ฐ๋ณธ ๋ฌธ๋ฒ์ด ๋ง๋์ง๋ง ๊ฒ์ฌ (์ํ์ฐ์ต)
/mcp-toolkit scenario --file ... --dry-run
--file(ํ์): ์คํํ ๊ฐ๋ณธ YAML ํ์ผ ๊ฒฝ๋ก-
--output(์ ํ): ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅํ ํด๋. ์ ์ ์ผ๋ฉด.claude/docs/mcp-toolkit/<์๋๋ฆฌ์ค์ด๋ฆ>-<์๊ฐ>/์ ์๋ ์ ์ฅ --dry-run(์ ํ): ์ง์ง๋ก ๋๋ฆฌ์ง ์๊ณ ๊ฐ๋ณธ์ด ์ฌ๋ฐ๋ฅธ์ง๋ง ํ์ธ
์์์ ๋ฌด์จ ์ผ์ด ๋ฒ์ด์ง๋์#
์ด ๋๊ตฌ๋ ํ ๋ฒ ์ญ ์คํํ๊ณ ๋๋๋ ์ ํด์ง ์์์ ์๋ ์ฌ์์ ๋๋ค(๊ฐ์ ๊ฑธ ๋ฐ๋ณตํ๋ ๋ฃจํ๊ฐ ์๋๋๋ค).
- ์ค๋น(setup) โ ๋ก๊ทธ์ธ, ๋ฐ์ดํฐ ์ด๊ธฐํ, ํ ์คํธ์ฉ ๋ฐ์ดํฐ ์ฌ๊ธฐ, ํ๋ฉด ์ด๋ ๋ฑ ์ฌ์ ์ค๋น๋ฅผ ํ ๋ฒ ํฉ๋๋ค.
-
๋ณธ ํ๋ฆ(steps) โ ๊ฐ๋ณธ์ ์ ํ ๋์์ ์ฐจ๋ก๋ก ์คํํฉ๋๋ค. ์ฑ ๋ด๋ถ ์ํ๋ฅผ ํ์ธํ๊ฑฐ๋(
expect๋ก "์ด๋ฐ ๊ฒฐ๊ณผ๊ฐ ๋์์ผ ํ๋ค" ๊ฒ์ฆ), ํ๋ฉด์ ํฐ์น/์คํฌ๋กคํ๊ฑฐ๋, ํ๋ฉด์ ์บก์ฒํฉ๋๋ค. - ์ ๋ฆฌ(teardown) โ ๋ฐ์ดํฐ ์ด๊ธฐํ ๋ฑ ๋ท์ ๋ฆฌ๋ฅผ ํฉ๋๋ค. ์ค๊ฐ์ ์คํจํ๋๋ผ๋ ์ด ์ ๋ฆฌ ๋จ๊ณ๋ ํญ์ ์คํ๋ฉ๋๋ค.
- ์ฌํ ํ์ธ(asserts) โ ๋ง์ง๋ง์ผ๋ก ์ต์ข ์ํ๊ฐ ๊ธฐ๋๋๋ก์ธ์ง ํ ๋ฒ ๋ ์ ๊ฒํฉ๋๋ค.
์คํจ๋ ์ด๋ฐ ๊ฒฝ์ฐ์ ๊ธฐ๋ก๋ฉ๋๋ค: ๋๊ตฌ ํธ์ถ ์์ฒด๊ฐ ์ค๋ฅ์ผ ๋, expect๋ก ๊ธฐ๋ํ ๊ฐ๊ณผ ์ค์ ๊ฐ ๋ค๋ฅผ ๋, ์ธ๋ถ ๋ช
๋ น์ด ์คํจ๋ฅผ ๋๋ ค์ค ๋.
โ๏ธ ์์ธ ์ต์ ยท์คํ ๋ช ์ธ (๊ฐ๋ฐ์ / AI ์์ด์ ํธ์ฉ)
Parameters#
| Parameter | Required | Description |
|---|---|---|
--file | โ | Path to a scenario YAML |
--output | โ | Output dir (default .claude/docs/mcp-toolkit/<scenario-name>-<ts>/) |
--dry-run | โ | Validate the YAML without executing |
YAML Schema#
name: < scenario-id > # required, snake_case
description: < free text >
# Optional preconditions โ run once
setup:
- tool: < name > # dynamic tool from mcp_flutter
args: { ... }
- external: < plugin:command >
args: { ... }
# Main flow
steps:
- tool: < name >
args: { ... }
expect: { ... } # optional assertion on the tool ' s return
- external: < plugin:command >
args: { ... }
# Teardown โ runs even on failure
teardown:
- tool: < name >
args: { ... }
# Post-conditions
asserts:
- tool: bloc.dump_state
args: { type: FeedBloc }
expect: { status: loaded, posts: { length: 10 } }tool: vs external:#
tool:โ dynamic tool frommcp_flutter(the in-app tools you registered).external:โ call into another plugin's command. Currently supported:cc-flutter-marionette:tap,enter_text,scroll_to,take_screenshots,get_interactive_elements,hot_reloadcc-flutter-pixel-loop:capture,comparecc-jaspr-web:audit,match,regressionโ when the scenario covers a Web/Jaspr surface
Example Scenario#
# test/scenarios/feed_pull_to_refresh.yaml
name: feed_pull_to_refresh
description: Verify pull-to-refresh on Feed correctly hits the server and updates state.
setup:
- tool: auth.impersonate
args: { persona: " alice " }
- tool: db.reset
- tool: db.seed_fixture
args: { name: " feed_with_3 " }
- tool: nav.go_named
args: { name: " feed " }
steps:
# Confirm initial state has 3 posts
- tool: bloc.dump_state
args: { type: " FeedBloc " }
expect: { status: " loaded " , posts: { length: 3 } }
- external: cc-flutter-pixel-loop:capture
args: { route: " /feed " , label: " before-refresh " }
# Seed more posts on the backend
- tool: db.seed_fixture
args: { name: " feed_with_10 " }
# User pulls to refresh
- external: cc-flutter-marionette:scroll_to
args: { key: " feed_top " }
- external: cc-flutter-marionette:swipe # if pull gesture supported
args: { direction: " down " , from_key: " feed_top " }
- tool: bloc.dump_state
args: { type: " FeedBloc " }
expect: { status: " loaded " , posts: { length: 10 } }
- external: cc-flutter-pixel-loop:capture
args: { route: " /feed " , label: " after-refresh " }
teardown:
- tool: db.reset
- tool: auth.impersonate
args: { persona: " default " }
asserts:
# Final state was already asserted in steps; nothing extra here.Convergence#
The scenario runner does not loop. It is a deterministic playback. Failures collected:
- Tool error (non-2xx style response).
expect:mismatch.external:command returned a failure summary.
On any failure, teardown still runs.
Output#
.claude/docs/mcp-toolkit/ < scenario-name > - < ts > /
โโโ scenario.yaml # copy of the run scenario
โโโ steps/
โ โโโ 01.json # each step ' s args + return
โ โโโ ...
โโโ screens/ # any pixel-loop captures forwarded here
โโโ bloc-dumps/
โโโ teardown.json
โโโ summary.md # pass/fail summary with timingsWhen to use vs when to avoid#
- Use: complex flows that mix domain state + UI taps + visual diffs (the cocode bread-and-butter case).
- Avoid for: pure tap-and-screenshot flows โ those belong in
cc-flutter-marionette:sweeporcc-flutter-pixel-loop:loop.