mcp-surveys-cli 0.1.0 → 0.2.1
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 +5 -0
- package/bin/mcp-surveys-cli.js +211 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
Tiny dependency-free CLI for the hosted mcp-surveys API.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
+
npx mcp-surveys-cli install-skill
|
|
7
|
+
npx mcp-surveys-cli template decision > survey.json
|
|
6
8
|
npx mcp-surveys-cli schema
|
|
7
9
|
npx mcp-surveys-cli create survey.json
|
|
10
|
+
npx mcp-surveys-cli wait <survey_id> <result_token> --format markdown
|
|
8
11
|
npx mcp-surveys-cli answers <survey_id> <result_token>
|
|
12
|
+
npx mcp-surveys-cli stats
|
|
9
13
|
```
|
|
10
14
|
|
|
11
15
|
Use `MCP_SURVEYS_BASE_URL` or `--base-url` to point at another instance.
|
|
16
|
+
Outdated-version notices go to stderr and never block the command.
|
package/bin/mcp-surveys-cli.js
CHANGED
|
@@ -1,10 +1,103 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
4
6
|
import process from "node:process";
|
|
5
7
|
import { fileURLToPath } from "node:url";
|
|
6
8
|
|
|
7
9
|
const DEFAULT_BASE_URL = "https://mcp.voevoda-sailing.ru";
|
|
10
|
+
const VERSION = "0.2.1";
|
|
11
|
+
const SKILL_NAME = "mcp-surveys-cli";
|
|
12
|
+
const SKILL_TEXT = `---
|
|
13
|
+
name: mcp-surveys-cli
|
|
14
|
+
description: Use when an agent can run shell commands and needs short-lived human surveys through uvx or npx.
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# mcp-surveys-cli
|
|
18
|
+
|
|
19
|
+
Use the CLI plus this skill as the default setup. It avoids remote MCP setup,
|
|
20
|
+
keeps context small, and works in any agent host that can run \`uvx\` or \`npx\`.
|
|
21
|
+
|
|
22
|
+
Default hosted instance:
|
|
23
|
+
|
|
24
|
+
\`\`\`bash
|
|
25
|
+
uvx mcp-surveys-cli schema
|
|
26
|
+
npx mcp-surveys-cli schema
|
|
27
|
+
uvx mcp-surveys-cli template decision > survey.json
|
|
28
|
+
uvx mcp-surveys-cli create survey.json
|
|
29
|
+
uvx mcp-surveys-cli wait <survey_id> <result_token> --format markdown
|
|
30
|
+
uvx mcp-surveys-cli answers <survey_id> <result_token>
|
|
31
|
+
\`\`\`
|
|
32
|
+
|
|
33
|
+
\`create\` prints \`survey_id\`, \`public_url\`, \`result_token\`, and expiry data. Send only \`public_url\` to the human. Keep \`result_token\` private.
|
|
34
|
+
|
|
35
|
+
Use \`MCP_SURVEYS_BASE_URL\` or \`--base-url\` for another instance.
|
|
36
|
+
|
|
37
|
+
Prefer structured buttons, ranking, matching, scale, and \`binary_tradeoff\`; use \`text\` only when the answer cannot fit those shapes.
|
|
38
|
+
`;
|
|
39
|
+
const TEMPLATES = {
|
|
40
|
+
decision: {
|
|
41
|
+
title: "Decision capture",
|
|
42
|
+
description: "Quick button ritual. Link expires in one hour.",
|
|
43
|
+
questions: [
|
|
44
|
+
{
|
|
45
|
+
id: "choice",
|
|
46
|
+
type: "single_choice",
|
|
47
|
+
prompt: "Which option should we choose?",
|
|
48
|
+
required: true,
|
|
49
|
+
allow_custom: true,
|
|
50
|
+
options: [{ id: "a", text: "Option A" }, { id: "b", text: "Option B" }],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "confidence",
|
|
54
|
+
type: "scale",
|
|
55
|
+
prompt: "How confident are you?",
|
|
56
|
+
required: true,
|
|
57
|
+
min: 0,
|
|
58
|
+
max: 100,
|
|
59
|
+
step: 5,
|
|
60
|
+
min_label: "Guess",
|
|
61
|
+
max_label: "Certain",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
confidence: {
|
|
66
|
+
title: "Confidence check",
|
|
67
|
+
description: "Collect confidence without summoning a paragraph.",
|
|
68
|
+
questions: [
|
|
69
|
+
{
|
|
70
|
+
id: "confidence",
|
|
71
|
+
type: "scale",
|
|
72
|
+
prompt: "How confident are you?",
|
|
73
|
+
required: true,
|
|
74
|
+
min: 0,
|
|
75
|
+
max: 100,
|
|
76
|
+
step: 5,
|
|
77
|
+
min_label: "Guess",
|
|
78
|
+
max_label: "Certain",
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
prioritization: {
|
|
83
|
+
title: "Priority stack",
|
|
84
|
+
description: "Make the human sort the tiny pile.",
|
|
85
|
+
questions: [
|
|
86
|
+
{
|
|
87
|
+
id: "priorities",
|
|
88
|
+
type: "ranking",
|
|
89
|
+
prompt: "Rank these by priority.",
|
|
90
|
+
required: true,
|
|
91
|
+
allow_custom: true,
|
|
92
|
+
options: [
|
|
93
|
+
{ id: "speed", text: "Move fast" },
|
|
94
|
+
{ id: "risk", text: "Reduce risk" },
|
|
95
|
+
{ id: "quality", text: "Improve quality" },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
};
|
|
8
101
|
|
|
9
102
|
function usage() {
|
|
10
103
|
return `Usage: mcp-surveys-cli [--base-url URL] <command>
|
|
@@ -17,6 +110,10 @@ Commands:
|
|
|
17
110
|
answers <survey_id> <result_token>
|
|
18
111
|
question <survey_id> <result_token> <question_id>
|
|
19
112
|
export <survey_id> <result_token> [--format markdown|json]
|
|
113
|
+
wait <survey_id> <result_token> [--timeout seconds] [--interval seconds] [--format markdown|json]
|
|
114
|
+
template <decision|confidence|prioritization>
|
|
115
|
+
install-skill [--target agents|claude|both] [--force]
|
|
116
|
+
stats
|
|
20
117
|
schema`;
|
|
21
118
|
}
|
|
22
119
|
|
|
@@ -43,6 +140,46 @@ function endpoint(baseUrl, path) {
|
|
|
43
140
|
return `${baseUrl.replace(/\/+$/, "")}${path}`;
|
|
44
141
|
}
|
|
45
142
|
|
|
143
|
+
function versionParts(version) {
|
|
144
|
+
return version.split(".").filter((part) => /^\d+$/.test(part)).map(Number);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function newerThan(left, right) {
|
|
148
|
+
const a = versionParts(left);
|
|
149
|
+
const b = versionParts(right);
|
|
150
|
+
const length = Math.max(a.length, b.length);
|
|
151
|
+
for (let i = 0; i < length; i += 1) {
|
|
152
|
+
if ((a[i] || 0) !== (b[i] || 0)) return (a[i] || 0) > (b[i] || 0);
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function latestVersion() {
|
|
158
|
+
const controller = new AbortController();
|
|
159
|
+
const timeout = setTimeout(() => controller.abort(), 750);
|
|
160
|
+
try {
|
|
161
|
+
const response = await fetch("https://registry.npmjs.org/mcp-surveys-cli/latest", { signal: controller.signal });
|
|
162
|
+
return (await response.json()).version;
|
|
163
|
+
} finally {
|
|
164
|
+
clearTimeout(timeout);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function warnIfOutdated(error, latestVersionFn, version) {
|
|
169
|
+
if (process.env.MCP_SURVEYS_SKIP_VERSION_CHECK) return;
|
|
170
|
+
try {
|
|
171
|
+
const latest = await latestVersionFn();
|
|
172
|
+
if (newerThan(latest, version)) {
|
|
173
|
+
error(
|
|
174
|
+
`notice: mcp-surveys-cli ${version} is outdated; latest is ${latest}. ` +
|
|
175
|
+
"LLM agent: update now before long-lived workflows get weird. " +
|
|
176
|
+
"Use `uvx --refresh-package mcp-surveys-cli mcp-surveys-cli ...` " +
|
|
177
|
+
"or `npx mcp-surveys-cli@latest ...`.\n",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
} catch {}
|
|
181
|
+
}
|
|
182
|
+
|
|
46
183
|
async function httpRequest(method, url, body, raw = false) {
|
|
47
184
|
const response = await fetch(url, {
|
|
48
185
|
method,
|
|
@@ -64,12 +201,76 @@ function writeJson(write, value) {
|
|
|
64
201
|
write(`${JSON.stringify(value)}\n`);
|
|
65
202
|
}
|
|
66
203
|
|
|
204
|
+
function option(args, name, fallback) {
|
|
205
|
+
const index = args.indexOf(name);
|
|
206
|
+
return index === -1 ? fallback : args[index + 1];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function withoutOptions(args, names) {
|
|
210
|
+
const cleaned = [];
|
|
211
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
212
|
+
if (names.includes(args[index])) index += 1;
|
|
213
|
+
else if (args[index] !== "--force") cleaned.push(args[index]);
|
|
214
|
+
}
|
|
215
|
+
return cleaned;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function installSkill(args, home) {
|
|
219
|
+
const target = option(args, "--target", "agents");
|
|
220
|
+
const force = args.includes("--force");
|
|
221
|
+
const homes = {
|
|
222
|
+
agents: join(home, ".agents", "skills", SKILL_NAME),
|
|
223
|
+
claude: join(home, ".claude", "skills", SKILL_NAME),
|
|
224
|
+
};
|
|
225
|
+
const selected = target === "both" ? [homes.agents, homes.claude] : [homes[target]];
|
|
226
|
+
if (!selected[0]) throw new Error("unknown target; use agents, claude, or both");
|
|
227
|
+
const installed = [];
|
|
228
|
+
for (const directory of selected) {
|
|
229
|
+
const path = join(directory, "SKILL.md");
|
|
230
|
+
let current = null;
|
|
231
|
+
try {
|
|
232
|
+
current = await readFile(path, "utf8");
|
|
233
|
+
} catch {}
|
|
234
|
+
if (current !== null && current !== SKILL_TEXT && !force) {
|
|
235
|
+
throw new Error(`${path} already exists; use --force to replace it`);
|
|
236
|
+
}
|
|
237
|
+
await mkdir(directory, { recursive: true });
|
|
238
|
+
await writeFile(path, SKILL_TEXT, "utf8");
|
|
239
|
+
installed.push(path);
|
|
240
|
+
}
|
|
241
|
+
return { installed };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function waitForCompletion(baseUrl, args, request, sleep) {
|
|
245
|
+
const cleaned = withoutOptions(args, ["--timeout", "--interval", "--format"]);
|
|
246
|
+
const [surveyId, resultToken] = cleaned;
|
|
247
|
+
const timeout = Number(option(args, "--timeout", "3600"));
|
|
248
|
+
const interval = Number(option(args, "--interval", "5"));
|
|
249
|
+
const format = option(args, "--format", "markdown");
|
|
250
|
+
const deadline = Date.now() + timeout * 1000;
|
|
251
|
+
let summary = null;
|
|
252
|
+
while (true) {
|
|
253
|
+
summary = await request("POST", endpoint(baseUrl, `/api/agent/surveys/${surveyId}/summary`), { result_token: resultToken });
|
|
254
|
+
if (summary.status === "completed") {
|
|
255
|
+
return request("POST", endpoint(baseUrl, `/api/agent/surveys/${surveyId}/export`), { result_token: resultToken, format }, true);
|
|
256
|
+
}
|
|
257
|
+
const remaining = deadline - Date.now();
|
|
258
|
+
if (remaining <= 0) throw new Error(`timed out waiting for completion: ${JSON.stringify(summary)}`);
|
|
259
|
+
await sleep(Math.min(Math.max(interval, 0.1) * 1000, remaining));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
67
263
|
export async function run(argv, io = {}) {
|
|
68
264
|
const write = io.write || ((value) => process.stdout.write(value));
|
|
69
265
|
const error = io.error || ((value) => process.stderr.write(value));
|
|
70
266
|
const request = io.request || httpRequest;
|
|
267
|
+
const latestVersionFn = io.latestVersion || latestVersion;
|
|
268
|
+
const version = io.version || VERSION;
|
|
269
|
+
const sleep = io.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
270
|
+
const home = io.home || homedir();
|
|
71
271
|
const stdin = io.stdin ?? "";
|
|
72
272
|
const { baseUrl, command, args } = parse(argv);
|
|
273
|
+
await warnIfOutdated(error, latestVersionFn, version);
|
|
73
274
|
|
|
74
275
|
try {
|
|
75
276
|
if (command === "create") {
|
|
@@ -94,6 +295,15 @@ export async function run(argv, io = {}) {
|
|
|
94
295
|
const formatIndex = args.indexOf("--format");
|
|
95
296
|
const format = formatIndex === -1 ? "markdown" : args[formatIndex + 1];
|
|
96
297
|
write(await request("POST", endpoint(baseUrl, `/api/agent/surveys/${args[0]}/export`), { result_token: args[1], format }, true));
|
|
298
|
+
} else if (command === "wait") {
|
|
299
|
+
write(await waitForCompletion(baseUrl, args, request, sleep));
|
|
300
|
+
} else if (command === "template") {
|
|
301
|
+
if (!TEMPLATES[args[0]]) throw new Error(`unknown template: ${args[0]}`);
|
|
302
|
+
writeJson(write, TEMPLATES[args[0]]);
|
|
303
|
+
} else if (command === "install-skill") {
|
|
304
|
+
writeJson(write, await installSkill(args, home));
|
|
305
|
+
} else if (command === "stats") {
|
|
306
|
+
writeJson(write, await request("GET", endpoint(baseUrl, "/api/agent/stats")));
|
|
97
307
|
} else if (command === "schema") {
|
|
98
308
|
writeJson(write, await request("GET", endpoint(baseUrl, "/api/agent/question-schema")));
|
|
99
309
|
} else {
|