@usercase/mcp 0.2.4-next.8 → 0.2.5-next.0

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/agents/install.md CHANGED
@@ -23,4 +23,4 @@ If the site is running locally (or the user can start it), call `check_site` wit
23
23
  ## 4. Tell the user what's next
24
24
 
25
25
  - Real visitors are recorded once this change is **deployed**. The dashboard's setup screen ticks itself off when the live site sends its first visit.
26
- - They don't have to wait for real visitors: offer to send a few virtual users now with the `research` prompt (`/mcp__usercase__research <url> 3`).
26
+ - They don't have to wait for real visitors: offer to send a few virtual users now with the `research` prompt (`{{research_cmd}} <url> 3`).
@@ -88,7 +88,7 @@ The goal is still the person's own reason for coming, in their words — never "
88
88
 
89
89
  ## 4. Send the visitors
90
90
 
91
- For each person, start an `usercase-visitor` sub-agent with a prompt like:
91
+ For each person, start a `{{visitor}}` sub-agent with a prompt like:
92
92
 
93
93
  > Call open_visit with person "<id>" and live this visit as that person until you leave.
94
94
 
package/mcp/report.js CHANGED
@@ -18,6 +18,14 @@ const KEY_RE = /^eb_sk_[A-Za-z0-9]{32}$/;
18
18
  const OUTBOX = path.join(ws.DIR, "outbox.jsonl");
19
19
 
20
20
  const apiKey = () => (process.env[KEY_ENV] || "").trim();
21
+ // 플러그인이면 키는 플러그인 설정에 있다(server.js 의 PLUGIN 과 같은 표시)
22
+ const PLUGIN = process.env.USERCASE_PLUGIN === "1";
23
+ const NO_KEY = () =>
24
+ PLUGIN
25
+ ? "No API key. Create one in the dashboard (Settings → Integrations → API keys), then enter it with /plugin → Installed → usercase → Configure"
26
+ : `No API key (${KEY_ENV}). Create one in the dashboard (Settings → Integrations → API keys), then add this MCP server again with -e ${KEY_ENV}=<key>`;
27
+ const BAD_KEY = () =>
28
+ `${PLUGIN ? "The API key" : KEY_ENV} does not look right (38 characters starting with eb_sk_). Copy it again from the dashboard${PLUGIN ? " and enter it with /plugin → Installed → usercase → Configure" : ""}`;
21
29
  const apiBase = () => (process.env[URL_ENV] || DEFAULT_API).replace(/\/$/, "");
22
30
 
23
31
  /** 이 MCP 세션 하나 = 실행 하나. 대시보드가 실행별로 묶는다 */
@@ -58,8 +66,8 @@ async function describeKey(sdkProject) {
58
66
  /** 키의 프로젝트와 그 설치 스크립트(서버가 대시보드 설치 화면과 같은 글을 준다) — install_snippet 이 쓴다 */
59
67
  async function getInstall() {
60
68
  const key = apiKey();
61
- if (!key) throw new Error(`No API key (${KEY_ENV}). Create one in the dashboard (Settings → Integrations → API keys), then add this MCP server again with -e ${KEY_ENV}=<key>`);
62
- if (!KEY_RE.test(key)) throw new Error(`${KEY_ENV} does not look right (38 characters starting with eb_sk_). Copy it again from the dashboard`);
69
+ if (!key) throw new Error(NO_KEY());
70
+ if (!KEY_RE.test(key)) throw new Error(BAD_KEY());
63
71
  try {
64
72
  const me = await call("GET", "/agent/me");
65
73
  if (!me.snippet) throw new Error("The Usercase server did not return a script. It may be an older server version");
@@ -188,8 +196,8 @@ async function sendDiagnostics(events, device, shots = []) {
188
196
  /** 키가 있어야 하는 호출 — 없거나 틀렸으면 사용자에게 할 말로 바꿔 던진다 */
189
197
  async function keyed(method, route, body, timeoutMs) {
190
198
  const key = apiKey();
191
- if (!key) throw new Error(`No API key (${KEY_ENV}). Create one in the dashboard (Settings → Integrations → API keys), then add this MCP server again with -e ${KEY_ENV}=<key>`);
192
- if (!KEY_RE.test(key)) throw new Error(`${KEY_ENV} does not look right (38 characters starting with eb_sk_). Copy it again from the dashboard`);
199
+ if (!key) throw new Error(NO_KEY());
200
+ if (!KEY_RE.test(key)) throw new Error(BAD_KEY());
193
201
  try {
194
202
  return await call(method, route, body, timeoutMs);
195
203
  } catch (e) {
package/mcp/server.js CHANGED
@@ -28,6 +28,13 @@ const { PathRecorder } = require("./path.js");
28
28
  const hub = new Hub();
29
29
  const server = new McpServer({ name: "usercase", version: require("../package.json").version });
30
30
 
31
+ // Claude Code 플러그인(plugin/)으로 떴는지. 플러그인의 .mcp.json 이 USERCASE_PLUGIN=1 을 준다.
32
+ // 플러그인에서는 방문자 에이전트가 플러그인에 들어 있어 이름이 usercase:usercase-visitor 이고(도구도 mcp__plugin_usercase_usercase__*),
33
+ // 명령은 /usercase:research 같은 스킬이다 — 스킬은 MCP 프롬프트를 못 부르니 instructions 도구로 같은 글을 받는다
34
+ const PLUGIN = process.env.USERCASE_PLUGIN === "1";
35
+ const VISITOR = PLUGIN ? "usercase:usercase-visitor" : "usercase-visitor";
36
+ const cmd = (name) => (PLUGIN ? `/usercase:${name}` : `/mcp__usercase__${name}`);
37
+
31
38
  const text = (t) => ({ content: [{ type: "text", text: String(t) }] });
32
39
  /** 방문 화면 + 그 화면의 이름 없는 아이콘 그림(있으면). 방문자는 그림을 보고 모양으로 안다 */
33
40
  const screen = (t, visitId) => {
@@ -301,6 +308,7 @@ server.registerTool(
301
308
  inputSchema: {},
302
309
  },
303
310
  wrap(async () => {
311
+ if (PLUGIN) return text(`Nothing to install — the visitor agent comes with the Usercase plugin as ${VISITOR}.`);
304
312
  const r = installAgent();
305
313
  return text(
306
314
  r === "same"
@@ -655,105 +663,110 @@ function runStatus() {
655
663
  const next = run.total === null ? ROUND : Math.min(ROUND, run.total - done);
656
664
  return [
657
665
  `▶ The run is not finished — ${done} visited so far (${left}).`,
658
- `Do not write the report and do not look into the code. Tell the user one short line about this round, then: call list_people, cast ${next} new people unlike the ones already there with save_people, start one usercase-visitor for each, and call results again when they finish.`,
666
+ `Do not write the report and do not look into the code. Tell the user one short line about this round, then: call list_people, cast ${next} new people unlike the ones already there with save_people, start one ${VISITOR} for each, and call results again when they finish.`,
659
667
  ].join("\n");
660
668
  }
661
669
 
662
- server.registerPrompt(
663
- "research",
664
- {
665
- description: "Research this site with Usercase virtual users",
666
- argsSchema: {
667
- url: z.string().optional(),
668
- count: z.string().optional().describe('How many people (default 5), or "unlimited" to keep going until you stop it'),
669
- focus: z.string().optional().describe("Only people who come for this part of the site, e.g. calendar, or comments and sharing"),
670
- },
671
- },
672
- ({ url, count, focus }) => {
673
- const plan = researchPlan(count);
674
- run = { total: plan.total, since: Date.now() };
675
- // 사람 만드는 규칙은 우리 AI 로 만들 때와 같은 원본(people.md)에서 가져온다 — 두 벌로 두지 않는다.
676
- // 자유 탐방이라 원본이 말하는 "path" 는 "자기 목적으로 왔다" 뿐이다
677
- const rules =
678
- "(Here the \"path\" is simply: came to the site with something of their own to find out.)\n\n" +
679
- fs
680
- .readFileSync(path.join(__dirname, "..", "src", "prompts", "people.md"), "utf8")
681
- .replace(/\r\n/g, "\n")
682
- .replace(/^# Role:.*\n+/, "")
683
- .replace(/^#/gm, "###");
684
- // focus 가 있으면 그 기능에 올 사람만 — 바뀌는 단계만 따로 적은 글을 앞에 붙인다. 방문자는 모른다
685
- const f = (focus || "").trim();
686
- const focusPart = f
687
- ? fs.readFileSync(path.join(__dirname, "..", "agents", "focus.md"), "utf8").replace(/\r\n/g, "\n").replaceAll("{{focus}}", f).trim() + "\n\n"
688
- : "";
689
- const body = fs
690
- .readFileSync(path.join(__dirname, "..", "agents", "research.md"), "utf8")
691
- .replace("{{focus}}\n", focusPart)
692
- .replace("{{url}}", url || "(ask the user)")
693
- .replace("{{count_line}}", plan.line)
694
- .replace("{{rounds}}", plan.rounds)
695
- .replace("{{people_rules}}", rules);
696
- return { messages: [{ role: "user", content: { type: "text", text: body } }] };
697
- },
698
- );
699
-
700
- server.registerPrompt(
701
- "install",
702
- { description: "Install the Usercase script on this site from its repository", argsSchema: { url: z.string().optional() } },
703
- ({ url }) => {
704
- const body = fs
705
- .readFileSync(path.join(__dirname, "..", "agents", "install.md"), "utf8")
706
- .replace(/\r\n/g, "\n")
707
- .replace("{{url}}", url || "(the site in this repository)");
708
- return { messages: [{ role: "user", content: { type: "text", text: body } }] };
709
- },
710
- );
711
-
712
- server.registerPrompt(
713
- "path",
714
- {
715
- description: "Record the expected path of a Usercase research's tasks",
716
- // 대시보드 과제 화면의 명령은 과제 id 를 붙인다 — 그 과제만, 경로가 있어도 다시
717
- argsSchema: { research: z.string().optional(), task: z.string().optional() },
718
- },
719
- ({ research, task }) => {
720
- const body = fs
721
- .readFileSync(path.join(__dirname, "..", "agents", "path.md"), "utf8")
722
- .replace(/\r\n/g, "\n")
723
- .replace("{{research}}", research || "(ask the user — the dashboard shows it)")
724
- .replace("{{task}}", task ? `Task: ${task} — record this task only, and record it again even if it already has a path.` : "Task: all tasks of the research");
725
- return { messages: [{ role: "user", content: { type: "text", text: body } }] };
726
- },
727
- );
728
-
729
- /** 조건 쓰는 규칙은 question·steps 가 같이 쓴다 — 한 벌만 둔다 */
730
- const agentDoc = (name, vars = {}) => {
731
- let body = fs.readFileSync(path.join(__dirname, "..", "agents", name), "utf8").replace(/\r\n/g, "\n");
732
- const rules = fs.readFileSync(path.join(__dirname, "..", "agents", "condition.md"), "utf8").replace(/\r\n/g, "\n").trim();
733
- body = body.replace("{{condition_rules}}", rules);
734
- for (const [k, v] of Object.entries(vars)) body = body.replace(`{{${k}}}`, v);
735
- return { messages: [{ role: "user", content: { type: "text", text: body } }] };
736
- };
737
-
738
- // 인자를 두지 않는다 — Claude Code 는 명령어 인자를 공백으로 나눠 문장을 못 싣는다. 무엇을 물을지는 대화로 묻는다
739
- server.registerPrompt("question", { description: "Make a Usercase question for a precise moment on your site — Claude suggests the moment, question and answers" }, () =>
740
- agentDoc("question.md"),
741
- );
742
-
743
- server.registerPrompt(
744
- "steps",
745
- {
746
- description: "Write the steps for the question open in your dashboard (New question → Several steps)",
747
- argsSchema: { session: z.string().optional().describe("The window id shown in the dashboard command") },
748
- },
749
- ({ session }) => agentDoc("steps.md", { session: session || "(missing — ask the user to copy the command from the dashboard)" }),
750
- );
751
-
670
+ const doc = (name) => fs.readFileSync(path.join(__dirname, "..", "agents", name), "utf8").replace(/\r\n/g, "\n");
671
+ /** 조건 쓰는 규칙은 question·steps 가 같이 쓴다 — 한 벌만 둔다 */
672
+ const agentDoc = (name, vars = {}) => {
673
+ let body = doc(name).replace("{{condition_rules}}", doc("condition.md").trim());
674
+ for (const [k, v] of Object.entries(vars)) body = body.replace(`{{${k}}}`, v);
675
+ return body;
676
+ };
677
+
678
+ /**
679
+ * 명령 지시문. 따로 쓰면 MCP 프롬프트(/mcp__usercase__research), 플러그인이면 instructions 도구(/usercase:research 스킬이 부른다)로
680
+ * 같은 글이 나간다
681
+ */
682
+ const COMMANDS = {
683
+ research: {
684
+ description: "Research this site with Usercase virtual users",
685
+ argsSchema: {
686
+ url: z.string().optional(),
687
+ count: z.string().optional().describe('How many people (default 5), or "unlimited" to keep going until you stop it'),
688
+ focus: z.string().optional().describe("Only people who come for this part of the site, e.g. calendar, or comments and sharing"),
689
+ },
690
+ body: ({ url, count, focus }) => {
691
+ const plan = researchPlan(count);
692
+ run = { total: plan.total, since: Date.now() };
693
+ // 사람 만드는 규칙은 우리 AI 로 만들 때와 같은 원본(people.md)에서 가져온다 — 두 벌로 두지 않는다.
694
+ // 자유 탐방이라 원본이 말하는 "path" 는 "자기 목적으로 왔다" 뿐이다
695
+ const rules =
696
+ "(Here the \"path\" is simply: came to the site with something of their own to find out.)\n\n" +
697
+ fs
698
+ .readFileSync(path.join(__dirname, "..", "src", "prompts", "people.md"), "utf8")
699
+ .replace(/\r\n/g, "\n")
700
+ .replace(/^# Role:.*\n+/, "")
701
+ .replace(/^#/gm, "###");
702
+ // focus 가 있으면 그 기능에 올 사람만 — 바뀌는 단계만 따로 적은 글을 앞에 붙인다. 방문자는 모른다
703
+ const f = (focus || "").trim();
704
+ const focusPart = f ? doc("focus.md").replaceAll("{{focus}}", f).trim() + "\n\n" : "";
705
+ return doc("research.md")
706
+ .replace("{{focus}}\n", focusPart)
707
+ .replace("{{url}}", url || "(ask the user)")
708
+ .replace("{{count_line}}", plan.line)
709
+ .replace("{{rounds}}", plan.rounds)
710
+ .replace("{{people_rules}}", rules)
711
+ .replaceAll("{{visitor}}", VISITOR);
712
+ },
713
+ },
714
+ install: {
715
+ description: "Install the Usercase script on this site from its repository",
716
+ argsSchema: { url: z.string().optional() },
717
+ body: ({ url }) =>
718
+ doc("install.md")
719
+ .replace("{{url}}", url || "(the site in this repository)")
720
+ .replace("{{research_cmd}}", cmd("research")),
721
+ },
722
+ path: {
723
+ description: "Record the expected path of a Usercase research's tasks",
724
+ // 대시보드 과제 화면의 명령은 과제 id 를 붙인다 — 그 과제만, 경로가 있어도 다시
725
+ argsSchema: { research: z.string().optional(), task: z.string().optional() },
726
+ body: ({ research, task }) =>
727
+ doc("path.md")
728
+ .replace("{{research}}", research || "(ask the user — the dashboard shows it)")
729
+ .replace("{{task}}", task ? `Task: ${task} — record this task only, and record it again even if it already has a path.` : "Task: all tasks of the research"),
730
+ },
731
+ // 인자를 두지 않는다 — Claude Code 는 명령어 인자를 공백으로 나눠 문장을 못 싣는다. 무엇을 물을지는 대화로 묻는다
732
+ question: {
733
+ description: "Make a Usercase question for a precise moment on your site — Claude suggests the moment, question and answers",
734
+ body: () => agentDoc("question.md"),
735
+ },
736
+ steps: {
737
+ description: "Write the steps for the question open in your dashboard (New question → Several steps)",
738
+ argsSchema: { session: z.string().optional().describe("The window id shown in the dashboard command") },
739
+ body: ({ session }) => agentDoc("steps.md", { session: session || "(missing — ask the user to copy the command from the dashboard)" }),
740
+ },
741
+ };
742
+
743
+ if (PLUGIN) {
744
+ server.registerTool(
745
+ "instructions",
746
+ {
747
+ description:
748
+ "The step-by-step instructions for a Usercase command (research, install, path, question, steps). The plugin's /usercase:* commands call this; follow what it returns as the user's request.",
749
+ inputSchema: {
750
+ command: z.enum(Object.keys(COMMANDS)),
751
+ ...COMMANDS.research.argsSchema,
752
+ ...COMMANDS.path.argsSchema,
753
+ ...COMMANDS.steps.argsSchema,
754
+ },
755
+ },
756
+ wrap(async ({ command, ...args }) => text(COMMANDS[command].body(args))),
757
+ );
758
+ } else {
759
+ for (const [name, c] of Object.entries(COMMANDS)) {
760
+ const meta = c.argsSchema ? { description: c.description, argsSchema: c.argsSchema } : { description: c.description };
761
+ server.registerPrompt(name, meta, (args = {}) => ({ messages: [{ role: "user", content: { type: "text", text: c.body(args) } }] }));
762
+ }
763
+ }
764
+
752
765
  async function main() {
753
766
  // 예전에 깐 방문자 에이전트가 있으면 이 버전 것으로 바꿔 둔다 — setup 은 처음 한 번만 불려서 옛 지시문이 남는다.
754
767
  // 리서치를 한 번도 안 쓴 사람에게는 만들지 않는다
755
768
  try {
756
- if ((fs.existsSync(AGENT_DEST) || fs.existsSync(AGENT_OLD)) && installAgent() === "updated") process.stderr.write(`Updated the visitor agent → ${AGENT_DEST}\n`);
769
+ if (!PLUGIN && (fs.existsSync(AGENT_DEST) || fs.existsSync(AGENT_OLD)) && installAgent() === "updated") process.stderr.write(`Updated the visitor agent → ${AGENT_DEST}\n`);
757
770
  } catch {}
758
771
  await server.connect(new StdioServerTransport());
759
772
  process.stderr.write(`Usercase MCP ready — working folder ${ws.DIR}${hub.inject ? ` · local SDK injected (${hub.inject.project})` : ""}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usercase/mcp",
3
- "version": "0.2.4-next.8",
3
+ "version": "0.2.5-next.0",
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",