@usercase/mcp 0.2.8-next.2 → 0.2.8-next.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -109,7 +109,7 @@ Claude suggests the moment, the question and its answers, and saves it only afte
109
109
  /mcp__usercase__new-research
110
110
  ```
111
111
 
112
- Claude asks whether you already know what you want to learn. If you don't, it reads your project's findings and where visitors left, and suggests two or three researches, saying which evidence comes from real visitors and which from virtual users. With no data yet, it offers to send a few virtual users first. It then reads your code to write the tasks and their success conditions (the page that means done, or the exact button), and saves the research only after you approve it — as a draft in your Inbox, where you create it. Afterwards, `/mcp__usercase__path <research id>` records each task's expected path.
112
+ Claude asks whether you already know what you want to learn. If you don't, it reads your project's findings and where visitors left, and suggests two or three researches, saying which evidence comes from real visitors and which from virtual users. With no data yet, it offers to send a few virtual users first. It then reads your code to write the tasks, their success conditions (the page that means done, or the exact button) and the route each task is meant to take, and shows you all of it at once. On your approval it walks each route in a browser, checks it, and saves the research with the routes — as a draft in your Inbox, where you create it. The routes move into the research when you create it. A draft you discard, or don't create within 30 days, loses its recorded paths and their screenshots.
113
113
 
114
114
  ## What the visitors will and won't do
115
115
 
@@ -56,14 +56,42 @@ One research is **one flow a participant can walk in one sitting**: 1–4 tasks,
56
56
 
57
57
  **Invite**: the page glob where visitors should be invited (where the flow begins, or the moment the evidence points to), and one short line asking them to take part.
58
58
 
59
+ **Wording people read** (the start screen, the invite line):
60
+ - Say plainly what they will do and roughly how long it takes.
61
+ - The participant is the subject. Never ask for help, never make the team or the site the subject, never thank in advance.
62
+ - Do not mention the problem you suspect or what others said.
63
+
59
64
  **Language**: English, unless the user asks for another language.
60
65
 
61
- ## 6. Show the user, then save
66
+ ## 6. Plan each task's route
67
+
68
+ The results tell who went straight from who took a detour only when a task has an expected path: the route the site was built for. Work it out now, from the code, for every task with a page or click to reach (tasks done by staying on a page have none):
69
+ - **where it starts**: the page a participant is on when the task begins;
70
+ - **the clicks a participant must make**, in order — the main route the UI offers, not a shortcut only you know;
71
+ - **where it ends**: the page or the click that counts as done. It must agree with the task's success condition.
72
+ - what the route needs: a signed-in demo account, or a write the safety guard does not send (the last click is then recorded without pressing it).
73
+
74
+ ## 7. Show the user everything at once
75
+
76
+ Show the whole research compactly: the purpose and its evidence, each task with how it counts as done (in plain words, no XPath) and its route in one line, the questions, and the invite. Say that on their approval you will walk each route in a browser and save the draft with the routes. Change whatever they ask for.
77
+
78
+ ## 8. On approval: record the routes, then save
79
+
80
+ Do not ask again between these steps. For each task with a route, in order, use the task id `plan-<n>` (n = the task's position in the research, from 1):
81
+
82
+ {{path_walk}}
83
+
84
+ ### Save each route
85
+
86
+ When `path_check` passes and the route ends where the plan said, call `path_save` (`page` or `click` when the route's end is the task's success condition, otherwise `keep`). It is kept until the draft is saved. Stop and ask the user only when a route cannot be walked as planned — the site differs from the code, a check keeps failing, or the route needs a write or a sign-in you do not have; leave that task without a route if they say so.
87
+
88
+ ### Save the draft
62
89
 
63
- Show the whole research compactly: the purpose and its evidence, each task with how it counts as done (in plain words: "done when they reach the order page"), the questions, and the invite. Change whatever they ask for.
90
+ Then call `research_save` with `basis` (`real` if the evidence came from real visitors, `virtual` if only from virtual users, `none` otherwise) and the ids of the findings it answers. It uploads the routes you saved with the draft. Tell the user:
91
+ - the Inbox link, and that it stays a draft until they create it there, where they also set when the invite appears;
92
+ - which tasks have a route and the screenshots `path_check` listed for them, and any task left without one and why;
93
+ - a draft that is discarded, or not created within 30 days, loses its routes.
64
94
 
65
- On their approval, call `research_save` with `basis` (`real` if the evidence came from real visitors, `virtual` if only from virtual users, `none` otherwise) and the ids of the findings it answers. Give them the Inbox link it returns and tell them:
66
- - it stays a draft until they create it in the Inbox, where they also set when the invite appears;
67
- - after it is created, `{{path_cmd}}` with the research id records each task's expected path, so the results can tell who went straight from who took a detour.
95
+ To redo a route later: `path_start` with `draft:<draft>:<index>` while the draft exists, or `{{path_cmd}}` with the research id once it is created.
68
96
 
69
97
  Do not change any code.
package/mcp/path.js CHANGED
@@ -124,9 +124,24 @@ class PathRecorder {
124
124
  this.hub = hub;
125
125
  /** taskId → { research, task } — path_tasks 로 받아 둔다 */
126
126
  this.known = new Map();
127
+ /** 아직 저장 안 한 리서치의 과제 경로(plan-<번호>) — 번호 → { body, steps, how }. research_save 가 가져간다 */
128
+ this.planned = new Map();
127
129
  this.cur = null;
128
130
  }
129
131
 
132
+ /** 녹화해 둔 계획 과제 경로를 넘기고 비운다 */
133
+ takePlanned() {
134
+ const out = new Map(this.planned);
135
+ this.planned.clear();
136
+ return out;
137
+ }
138
+
139
+ /** 상대 주소를 풀 사이트 — 연결된 프로젝트의 주소. 못 읽으면 빈 값(전체 주소를 받는다) */
140
+ async projectSite() {
141
+ if (this.site === undefined) this.site = await report.getInstall().then((m) => m.website || "").catch(() => "");
142
+ return this.site;
143
+ }
144
+
130
145
  /** 리서치의 과제들 */
131
146
  async tasks(researchId) {
132
147
  const r = await report.getResearchTasks(researchId);
@@ -150,6 +165,12 @@ class PathRecorder {
150
165
  .join("\n");
151
166
  }
152
167
 
168
+ /** 방금 저장한 리서치 초안의 과제들(new-research) — draft:<초안>:<번호> 로 path_start 에 쓴다 */
169
+ addDraft(draftId, { title, website, tasks }) {
170
+ const research = { title, status: "draft", website };
171
+ for (const t of tasks) this.known.set(`draft:${draftId}:${t.index}`, { research, task: { taskId: `draft:${draftId}:${t.index}`, title: t.title } });
172
+ }
173
+
153
174
  async open(s, { device, account }) {
154
175
  let storageState;
155
176
  if (account) {
@@ -163,15 +184,24 @@ class PathRecorder {
163
184
 
164
185
  /** 새로 기록을 시작한다. 같은 과제를 다시 하면 처음부터 */
165
186
  async start({ task, url, device = "desktop", account = null }) {
166
- const k = this.known.get(String(task));
167
- if (!k) throw new Error(`Task ${task} is not known yet. Call path_tasks with the research id first.`);
168
- const full = /^https?:\/\//i.test(url) ? url : new URL(url, k.research.website || undefined).toString();
187
+ // 아직 저장 안 한 리서치의 과제(new-research, plan-<번호>) — 녹화해 두었다가 research_save 가 초안과 함께 올린다
188
+ const plan = String(task).match(/^plan-(\d+)$/i);
189
+ const k = plan ? { research: { website: await this.projectSite() }, task: { title: `Planned task ${plan[1]}` } } : this.known.get(String(task));
190
+ if (!k)
191
+ throw new Error(
192
+ String(task).startsWith("draft:")
193
+ ? `Draft task ${task} is not known in this session. Draft tasks are known right after research_save; once the research is created, use path_tasks with its id instead.`
194
+ : `Task ${task} is not known yet. Call path_tasks with the research id first.`,
195
+ );
196
+ if (!/^https?:\/\//i.test(url) && !k.research.website) throw new Error("Give the full URL where the route starts — the project has no site address to resolve a path against.");
197
+ const full = /^https?:\/\//i.test(url) ? url : new URL(url, k.research.website).toString();
169
198
  await this.stop();
170
199
  const s = this.hub.site(full);
171
200
  const ctx = await this.open(s, { device, account });
172
201
  const page = await ctx.newPage();
173
202
  ws.ensure();
174
- const dir = path.join(ws.DIR, "paths", String(task));
203
+ // 폴더 이름에 : 를 못 쓰는 OS 가 있다(Windows) — draft:<초안>:<번호> 같은 id 를 바꿔 쓴다
204
+ const dir = path.join(ws.DIR, "paths", String(task).replace(/[^\w.-]+/g, "_"));
175
205
  fs.rmSync(dir, { recursive: true, force: true });
176
206
  fs.mkdirSync(dir, { recursive: true });
177
207
  this.cur = { taskId: String(task), title: k.task.title, s, ctx, page, net: watchNet(page), dir, startUrl: full, device, account, steps: [], actions: [], view: null, checked: null };
@@ -407,13 +437,26 @@ class PathRecorder {
407
437
  steps: steps.map((s) => ({ type: s.type, url: s.url, xpath: s.xpath || "", classes: s.classes || [], ids: s.ids || [], fingerprint: s.fingerprint || null, image: fs.readFileSync(s.image).toString("base64") })),
408
438
  };
409
439
  if (success === "page") body.success = { condition: "url_entry", targetUrl: last.url };
410
- else if (success === "click") body.success = { condition: "element_click", xpath: last.xpath, xpathUrl: last.url, classes: last.classes, ids: last.ids };
411
- const r = await report.savePath(c.taskId, body);
440
+ else if (success === "click") body.success = { condition: "element_click", xpath: last.xpath, xpathUrl: last.url, classes: last.classes, ids: last.ids, label: last.label || "" };
441
+ const cutLine = cut.length
442
+ ? `Left out the ${cut.length} step(s) after the success point (${cut.map((s) => (s.type === "URL" ? `page ${s.url}` : `click "${s.label || s.tag}"`)).join(", ")}) — the task ends at the success point, so no participant could ever reach them. Tell the user.`
443
+ : "";
444
+ // 아직 저장 안 한 리서치의 과제 — 올리지 않고 들고 있다가 research_save 가 초안과 함께 올린다
445
+ const plan = c.taskId.match(/^plan-(\d+)$/i);
446
+ if (plan) {
447
+ const n = Number(plan[1]);
448
+ const how = success === "page" ? `done on reaching ${last.url}` : success === "click" ? `done on clicking "${last.label || last.tag}" on ${last.url}` : "success condition as written";
449
+ this.planned.set(n, { body, steps: steps.length, how });
450
+ await this.stop();
451
+ return [`Kept the checked path for planned task ${n}: ${steps.length} steps, ${how}. research_save uploads it with the draft.`, cutLine].filter(Boolean).join("\n");
452
+ }
453
+ // 초안 과제(draft:<초안>:<번호>)는 초안에 둔다 — 리서치로 만들 때 옮겨진다
454
+ const draft = c.taskId.match(/^draft:([^:]+):(\d+)$/);
455
+ const r = draft ? await report.saveDraftPath(draft[1], Number(draft[2]), body) : await report.savePath(c.taskId, body);
456
+ const where = draft ? "The path is kept in the draft and moves into the research when it is created." : "The dashboard's task results now separate going straight from detours.";
412
457
  const done = [
413
- `Saved the expected path for task ${c.taskId} ("${c.title}"): ${r.steps} steps${r.success ? `, success condition set to ${r.success === "url_entry" ? `reaching ${last.url}` : `clicking "${last.label || last.tag}"`}` : ""}. The dashboard's task results now separate going straight from detours.`,
414
- cut.length
415
- ? `Left out the ${cut.length} step(s) after the success point (${cut.map((s) => (s.type === "URL" ? `page ${s.url}` : `click "${s.label || s.tag}"`)).join(", ")}) — the task ends at the success point, so no participant could ever reach them. Tell the user.`
416
- : "",
458
+ `Saved the expected path for task ${c.taskId} ("${c.title}"): ${r.steps} steps${r.success ? `, success condition set to ${r.success === "url_entry" ? `reaching ${last.url}` : `clicking "${last.label || last.tag}"`}` : ""}. ${where}`,
459
+ cutLine,
417
460
  ]
418
461
  .filter(Boolean)
419
462
  .join("\n");
package/mcp/report.js CHANGED
@@ -279,6 +279,8 @@ const getResearchTasks = (researchId) => keyed("GET", `/agent/researches/${encod
279
279
 
280
280
  /** 과제의 정상 경로를 통째로 바꾼다. 화면을 올리느라 오래 걸릴 수 있다 */
281
281
  const savePath = (taskId, body) => keyed("PUT", `/agent/tasks/${encodeURIComponent(taskId)}/path`, body, 90000);
282
+ /** 아직 리서치가 안 된 초안 과제의 경로(new-research) — 초안에 둔다 */
283
+ const saveDraftPath = (draftId, index, body) => keyed("PUT", `/agent/researches/drafts/${encodeURIComponent(draftId)}/tasks/${index}/path`, body, 90000);
282
284
 
283
285
  /** 질문 만들기(question) — 최근 방문을 읽어야 해서 조금 걸린다 */
284
286
  const getQuestionContext = () => keyed("GET", "/agent/questions/context", null, 60000);
@@ -305,6 +307,7 @@ module.exports = {
305
307
  sendDiagnostics,
306
308
  getResearchTasks,
307
309
  savePath,
310
+ saveDraftPath,
308
311
  getQuestionContext,
309
312
  checkQuestion,
310
313
  saveQuestion,
package/mcp/server.js CHANGED
@@ -566,7 +566,7 @@ server.registerTool(
566
566
  description:
567
567
  "Open a fresh browser on the page where the task's intended route begins and start recording its expected path. The page itself is step 1. Starting again discards the recording in progress.",
568
568
  inputSchema: {
569
- task: z.string().describe("Task id from path_tasks"),
569
+ task: z.string().describe("Task id from path_tasks · plan-<n> for task n of a research not saved yet (new-research) · draft:<draft>:<index> for a saved draft"),
570
570
  url: z.string().describe("Full URL where the route starts — the local dev server is fine; only paths are saved"),
571
571
  device: z.enum(["desktop", "mobile"]).optional(),
572
572
  account: z.string().optional().describe("Demo account saved by login (a1 …) if the route needs a signed-in user"),
@@ -685,7 +685,7 @@ server.registerTool(
685
685
  "research_save",
686
686
  {
687
687
  description:
688
- "Save the research as a DRAFT in the dashboard Inbox, after the user approved it. It becomes a research only when someone creates it there. Paths are page paths starting with / (no host), with :name for the parts that change.",
688
+ "Save the research as a DRAFT in the dashboard Inbox, after the user approved it. It becomes a research only when someone creates it there. Paths are page paths starting with / (no host), with :name for the parts that change. Expected paths recorded and saved beforehand as plan-<n> (n = the task's position in tasks, from 1) are uploaded with it.",
689
689
  inputSchema: {
690
690
  title: z.string().describe("Short, for the team"),
691
691
  purpose: z.string().describe("What the team wants to learn and why — the evidence it came from"),
@@ -713,10 +713,33 @@ server.registerTool(
713
713
  },
714
714
  wrap(async (args) => {
715
715
  const r = await report.saveResearchDraft(args);
716
+ const tasks = r.tasks || [];
717
+ if (tasks.length) recorder.addDraft(r.draftId, { title: args.title, website: r.website || "", tasks });
718
+ // 저장 전에 녹화해 둔 경로(plan-<번호>, 번호는 tasks 순서 1부터)를 초안의 그 과제에 올린다
719
+ const planned = recorder.takePlanned();
720
+ const lines = [];
721
+ for (const [n, p] of [...planned.entries()].sort((a, b) => a[0] - b[0])) {
722
+ const t = tasks[n - 1];
723
+ if (!t) {
724
+ lines.push(`- planned task ${n}: not uploaded — the research has ${tasks.length} task(s)`);
725
+ continue;
726
+ }
727
+ try {
728
+ await report.saveDraftPath(r.draftId, t.index, p.body);
729
+ lines.push(`- task ${n} "${t.title}": ${p.steps} steps, ${p.how}`);
730
+ } catch (e) {
731
+ lines.push(`- task ${n} "${t.title}": not uploaded (${e.message}) — record it again with path_start draft:${r.draftId}:${t.index}`);
732
+ }
733
+ }
734
+ const missing = tasks.filter((t, i) => !planned.has(i + 1) && t.condition !== "time_on_page");
716
735
  return text(
717
736
  [
718
737
  `Saved as a draft${r.findings ? `, linked to ${r.findings} finding${r.findings > 1 ? "s" : ""}` : ""}.`,
719
738
  `Open it in the Inbox to review and create it: ${report.dashboardUrl(r.inboxPath)}`,
739
+ ...(lines.length ? ["", "Expected paths saved with it:", ...lines] : []),
740
+ ...(missing.length
741
+ ? ["", "Tasks without an expected path:", ...missing.map((t) => `- ${t.title} (record later with path_start draft:${r.draftId}:${t.index})`)]
742
+ : []),
720
743
  ].join("\n"),
721
744
  );
722
745
  }),
@@ -843,6 +866,19 @@ const agentDoc = (name, vars = {}) => {
843
866
  return body;
844
867
  };
845
868
 
869
+ /** path.md 의 "## 3. Walk it" ~ "## 4. Check it" 끝까지 — 제목은 한 단계 낮추고 번호를 뗀다 */
870
+ const pathWalkRules = () => {
871
+ const d = doc("path.md");
872
+ const from = d.indexOf("## 3. Walk it");
873
+ const to = d.indexOf("## 5.");
874
+ if (from < 0 || to < 0) throw new Error("agents/path.md lost its Walk it / Check it sections");
875
+ return d
876
+ .slice(from, to)
877
+ .replace(/^## \d+\. /gm, "### ")
878
+ .replace(/\n*Do not change any code\.\n*/g, "\n\n")
879
+ .trim();
880
+ };
881
+
846
882
  /**
847
883
  * 명령 지시문. 따로 쓰면 MCP 프롬프트(/mcp__usercase__research), 플러그인이면 instructions 도구(/usercase:research 스킬이 부른다)로
848
884
  * 같은 글이 나간다
@@ -910,7 +946,8 @@ const COMMANDS = {
910
946
  // 리서치 초안 — 무엇을 알고 싶은지는 대화로 묻는다(인자 없음, question 과 같은 이유)
911
947
  "new-research": {
912
948
  description: "Plan a Usercase research from your data and code — your agent suggests what to research, writes the tasks and questions, and saves a draft",
913
- body: () => agentDoc("new-research.md", { research_cmd: cmd("research"), path_cmd: cmd("path") }),
949
+ // 경로를 걷고 확인하는 규칙은 path 명령과 한 벌 — path.md 의 3·4 단계를 그대로 싣는다
950
+ body: () => agentDoc("new-research.md", { research_cmd: cmd("research"), path_cmd: cmd("path"), path_walk: pathWalkRules() }),
914
951
  },
915
952
  // 프로젝트 바꾸기 — 연결은 저장소 폴더마다라 이 저장소만 바뀐다
916
953
  connect: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usercase/mcp",
3
- "version": "0.2.8-next.2",
3
+ "version": "0.2.8-next.4",
4
4
  "description": "Virtual users that browse your website and tell you what they thought — an MCP server for Claude Code",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://usercase.io",