@formlm/cli 0.3.0 → 0.5.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 +326 -65
- package/dist/commands/app.d.ts.map +1 -1
- package/dist/commands/app.js +74 -16
- package/dist/commands/app.js.map +1 -1
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +186 -7
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/connect.js +3 -3
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/doctor.d.ts +11 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +109 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/expert.d.ts.map +1 -1
- package/dist/commands/expert.js +42 -18
- package/dist/commands/expert.js.map +1 -1
- package/dist/commands/field.d.ts.map +1 -1
- package/dist/commands/field.js +9 -11
- package/dist/commands/field.js.map +1 -1
- package/dist/commands/profile.js +1 -1
- package/dist/commands/profile.js.map +1 -1
- package/dist/commands/report.d.ts.map +1 -1
- package/dist/commands/report.js +39 -12
- package/dist/commands/report.js.map +1 -1
- package/dist/commands/scale.d.ts.map +1 -1
- package/dist/commands/scale.js +4 -1
- package/dist/commands/scale.js.map +1 -1
- package/dist/commands/share.d.ts.map +1 -1
- package/dist/commands/share.js +258 -28
- package/dist/commands/share.js.map +1 -1
- package/dist/commands/skill.d.ts.map +1 -1
- package/dist/commands/skill.js +2 -3
- package/dist/commands/skill.js.map +1 -1
- package/dist/commands/smart.d.ts.map +1 -1
- package/dist/commands/smart.js +337 -59
- package/dist/commands/smart.js.map +1 -1
- package/dist/commands/snapshot.d.ts +0 -11
- package/dist/commands/snapshot.d.ts.map +1 -1
- package/dist/commands/snapshot.js +168 -42
- package/dist/commands/snapshot.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/doctor.d.ts +51 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +202 -0
- package/dist/doctor.js.map +1 -0
- package/dist/exec.d.ts +39 -1
- package/dist/exec.d.ts.map +1 -1
- package/dist/exec.js +309 -4
- package/dist/exec.js.map +1 -1
- package/dist/index.js +55 -6
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +283 -67
- package/dist/mcp.js.map +1 -1
- package/dist/output.d.ts +23 -1
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +69 -7
- package/dist/output.js.map +1 -1
- package/dist/summary.d.ts +17 -0
- package/dist/summary.d.ts.map +1 -0
- package/dist/summary.js +64 -0
- package/dist/summary.js.map +1 -0
- package/dist/utils.d.ts +10 -2
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +45 -6
- package/dist/utils.js.map +1 -1
- package/package.json +11 -2
package/dist/mcp.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { execCommand, authLogin, authMe } from './exec.js';
|
|
4
|
+
import { execCommand, authLogin, authLoginCode, authMe, authSendEmailCode, fetchCaptchaImage } from './exec.js';
|
|
5
5
|
import { addProfile, getActiveProfile, getBaseUrl } from './config.js';
|
|
6
6
|
import { VERSION } from './version.js';
|
|
7
7
|
import { escapeArg } from './utils.js';
|
|
8
|
+
import { buildSummary, shareTokenFromUrl } from './summary.js';
|
|
9
|
+
import { runDoctor, isValidLangCode } from './doctor.js';
|
|
8
10
|
// escapeArg now lives in ./utils.js (shared with commands/smart.ts) so that the
|
|
9
11
|
// direct CLI and the MCP Server use a single, consistent escaping implementation.
|
|
10
12
|
// ── Helper: format exec result as MCP text content ──────────────────────────
|
|
@@ -12,7 +14,7 @@ function toText(r) {
|
|
|
12
14
|
return r.code === 0 ? (r.data || r.message) : `❌ [${r.code}] ${r.message}`;
|
|
13
15
|
}
|
|
14
16
|
// ── Helper: format formlm_generate (smart plan) result for AI consumption ──────
|
|
15
|
-
// Extracts key fields (appId, planType, tasks) from plan result
|
|
17
|
+
// Extracts key fields (appId, planType, name, tasks) from plan result
|
|
16
18
|
// and presents them with next-step guidance for sequential module execution.
|
|
17
19
|
function formatGenerateResult(r) {
|
|
18
20
|
if (r.code !== 0) {
|
|
@@ -30,6 +32,8 @@ function formatGenerateResult(r) {
|
|
|
30
32
|
lines.push(`📋 App ID: ${parsed.appId}`);
|
|
31
33
|
if (parsed.planType)
|
|
32
34
|
lines.push(`🎯 Type: ${parsed.planType}`);
|
|
35
|
+
if (parsed.name)
|
|
36
|
+
lines.push(`📛 Name: ${parsed.name}`);
|
|
33
37
|
if (parsed.description)
|
|
34
38
|
lines.push(`📝 Description: ${parsed.description}`);
|
|
35
39
|
if (parsed.taskCount != null)
|
|
@@ -41,12 +45,17 @@ function formatGenerateResult(r) {
|
|
|
41
45
|
lines.push(` ${t.seq}. [${t.skill}] ${t.title}`);
|
|
42
46
|
}
|
|
43
47
|
lines.push('');
|
|
44
|
-
lines.push('
|
|
45
|
-
lines.push(parsed.plan || '');
|
|
46
|
-
lines.push('');
|
|
47
|
-
lines.push('Next: Use formlm_execute to execute each module sequentially:');
|
|
48
|
+
lines.push('Next: Use formlm_execute to execute each module sequentially (plan is cached server-side for ~10 minutes):');
|
|
48
49
|
for (const t of parsed.tasks) {
|
|
49
|
-
lines.push(` formlm_execute: appId=${parsed.appId}, module=${t.skill}
|
|
50
|
+
lines.push(` formlm_execute: appId=${parsed.appId}, module=${t.skill}`);
|
|
51
|
+
}
|
|
52
|
+
// Coverage warning: plans other than consultation have no expert task, so callers
|
|
53
|
+
// must not assume an AI expert exists (pages advertising one would be dead links).
|
|
54
|
+
const done = parsed.tasks.map((t) => t.skill);
|
|
55
|
+
if (!done.includes('expert')) {
|
|
56
|
+
lines.push('');
|
|
57
|
+
lines.push(`⚠️ This plan has NO 'expert' module — smart execute --module expert will fail.`);
|
|
58
|
+
lines.push(` Need an AI expert? Add it explicitly: formlm_exec "assess expert config --app ${parsed.appId} --name ... --role ... --kbText ... --json"`);
|
|
50
59
|
}
|
|
51
60
|
}
|
|
52
61
|
return lines.join('\n');
|
|
@@ -60,10 +69,10 @@ function formatGenerateResult(r) {
|
|
|
60
69
|
// ── MCP timeouts ─────────────────────────────────────────────────────────────
|
|
61
70
|
// Most commands complete in < 5 seconds.
|
|
62
71
|
// Smart pipeline (AssessAgent) takes 30-300 seconds.
|
|
63
|
-
const TIMEOUT_DEFAULT = 60_000; // 60s for all direct CLI commands
|
|
64
|
-
const TIMEOUT_PLAN = 120_000; // 2min for smart plan (Phase 1 only, ~10-30s)
|
|
65
|
-
const TIMEOUT_EXECUTE = 300_000; // 5min for smart execute (single module, ~30-120s)
|
|
66
|
-
const TIMEOUT_STYLE = 600_000; // 10min for connect style apply/apply-all (AI-generated styles can take 60-120s)
|
|
72
|
+
const TIMEOUT_DEFAULT = Number(process.env.FORMLM_TIMEOUT_MS) || 60_000; // 60s for all direct CLI commands
|
|
73
|
+
const TIMEOUT_PLAN = Number(process.env.FORMLM_TIMEOUT_PLAN) || 120_000; // 2min for smart plan (Phase 1 only, ~10-30s)
|
|
74
|
+
const TIMEOUT_EXECUTE = Number(process.env.FORMLM_TIMEOUT_EXECUTE) || 300_000; // 5min for smart execute (single module, ~30-120s)
|
|
75
|
+
const TIMEOUT_STYLE = Number(process.env.FORMLM_TIMEOUT_STYLE) || 600_000; // 10min for connect style apply/apply-all (AI-generated styles can take 60-120s)
|
|
67
76
|
export async function startMcpServer() {
|
|
68
77
|
const server = new McpServer({
|
|
69
78
|
name: 'formlm',
|
|
@@ -82,8 +91,8 @@ export async function startMcpServer() {
|
|
|
82
91
|
scale: 'Scale dimension scoring — P0: range boundaries must be continuous (next.min = prev.max + 1), highest tier max must be 999, --kbText is required.',
|
|
83
92
|
connect: 'Page styling & themes — P0: --theme must be one of 6 design modes (scenic/skeuomorphic/liquid/glassmorphism/immersive/minimalist). --look must include scenario + visual style description.',
|
|
84
93
|
report: 'Report pages & widgets — P0: system variables use {{double braces}} e.g. {{TotalScore}}, logic conditions use actual score values (never percentages).',
|
|
85
|
-
expert: 'AI expert agent config — P0: "expert config" for full setup (requires --kbText), "expert set"
|
|
86
|
-
share: 'Share & publish settings — share set command is naturally idempotent (safe to re-run).
|
|
94
|
+
expert: 'AI expert agent config — P0: "expert config" for full setup (requires --name AND --kbText), "expert set --property <p> --value <v>" for single-property micro-adjustments (e.g. --property enable). NOTE: only consultation plans auto-generate an expert; assessment/exam plans do not — add one explicitly.',
|
|
95
|
+
share: 'Share & publish settings — share set command is naturally idempotent (safe to re-run). Access types: visitor = anonymous no-login ("anyone can fill", the usual public choice); all = every LOGGED-IN FormLM user (anonymous visitors hit the login page); secret = password; owner = creator only. Day rule: 1..30 finite, <=0 or >30 normalize to permanent (sentinel 3650000).',
|
|
87
96
|
};
|
|
88
97
|
for (const skillId of skillIds) {
|
|
89
98
|
server.resource(skillId, `formlm://skills/${skillId}`, {
|
|
@@ -101,26 +110,40 @@ export async function startMcpServer() {
|
|
|
101
110
|
});
|
|
102
111
|
}
|
|
103
112
|
// ════════════════════════════════════════════════════════════════
|
|
104
|
-
//
|
|
113
|
+
// MCP TOOLS — Layered Architecture (9 tools)
|
|
105
114
|
//
|
|
106
115
|
// Recommended workflow (smart pipeline):
|
|
107
116
|
// 1. auth_login → authenticate
|
|
108
117
|
// 2. formlm_generate → generate plan (Phase 1, ~10-30s)
|
|
109
118
|
// 3. formlm_execute → execute each module sequentially (form→scale→connect→report→expert→share)
|
|
110
|
-
// 4.
|
|
119
|
+
// 4. formlm_doctor → verify quality (share access / expert / language / coverage)
|
|
120
|
+
// (formlm_snapshot → full or --summary state when you need the raw modules)
|
|
111
121
|
// 5. formlm_exec → fine-grained modifications (advanced)
|
|
112
122
|
// ════════════════════════════════════════════════════════════════
|
|
113
123
|
// ── Tier 0: Authentication ────────────────────────────────────
|
|
114
124
|
server.tool('auth_login', [
|
|
115
|
-
'Login to FormLM
|
|
125
|
+
'Login to FormLM. Three methods, in order of preference:',
|
|
126
|
+
'',
|
|
127
|
+
'1. Access Token (recommended): ask the user to copy it from',
|
|
128
|
+
' formlm.me → Workspace → Account Settings → Access Token → Copy, then pass it as `token`.',
|
|
129
|
+
'',
|
|
130
|
+
'2. Email verification code (no password needed, fully in-chat, works for ALL accounts):',
|
|
131
|
+
' a. Call auth_email_code { email } — returns a captcha image (show it to the user)',
|
|
132
|
+
' b. Ask the user to read the 4 digits from the image, then call auth_email_code { email, captcha }',
|
|
133
|
+
' c. A 6-digit code is emailed to the user (valid 5 minutes) — ask them for it',
|
|
134
|
+
' d. Call this tool with { email, code }',
|
|
135
|
+
'',
|
|
136
|
+
'3. email + password: only for accounts that HAVE set a password.',
|
|
137
|
+
' Accounts registered via email verification code or Google have NO password.',
|
|
138
|
+
' If password login fails with 401/403, do NOT retry — switch to method 1 or 2.',
|
|
116
139
|
'',
|
|
117
140
|
'IMPORTANT: At the START of any FormLM session (before calling formlm_generate/formlm_exec),',
|
|
118
|
-
'call auth_status first. If not logged in,
|
|
119
|
-
'(easiest, no browser needed) or a token. Do NOT wait for a 401 error before authenticating.',
|
|
141
|
+
'call auth_status first. If not logged in, authenticate immediately. Do NOT wait for a 401 error.',
|
|
120
142
|
].join('\n'), {
|
|
121
|
-
token: z.string().optional().describe('
|
|
122
|
-
email: z.string().optional().describe('Account email (use
|
|
123
|
-
|
|
143
|
+
token: z.string().optional().describe('Access token from formlm.me → Workspace → Account Settings → Access Token → Copy'),
|
|
144
|
+
email: z.string().optional().describe('Account email (use with `code` for verification-code login, or with `password`)'),
|
|
145
|
+
code: z.string().optional().describe('Email verification code (6 digits, valid 5 min) — request it via auth_email_code first'),
|
|
146
|
+
password: z.string().optional().describe('Account password (use together with email; verification-code/Google accounts have no password)'),
|
|
124
147
|
}, async (params) => {
|
|
125
148
|
if (params.token) {
|
|
126
149
|
addProfile({ name: 'default', url: getBaseUrl(), token: params.token, active: true });
|
|
@@ -131,6 +154,21 @@ export async function startMcpServer() {
|
|
|
131
154
|
}
|
|
132
155
|
return { content: [{ type: 'text', text: `⚠️ Token saved but verification failed: ${result.message}` }] };
|
|
133
156
|
}
|
|
157
|
+
if (params.email && params.code) {
|
|
158
|
+
const result = await authLoginCode(params.email, params.code);
|
|
159
|
+
if (result.code === 0 && result.data) {
|
|
160
|
+
const token = typeof result.data === 'string' ? result.data : result.data.token || '';
|
|
161
|
+
addProfile({ name: 'default', url: getBaseUrl(), token, active: true });
|
|
162
|
+
return { content: [{ type: 'text', text: '✅ Login successful!' }] };
|
|
163
|
+
}
|
|
164
|
+
if (result.code === 403) {
|
|
165
|
+
return { content: [{ type: 'text', text: '❌ Invalid or expired verification code. The code is valid for 5 minutes — ask the user to double-check it and retry. If it expired, call auth_email_code again (new captcha → new code). If the account reports 429, it is locked for 30 minutes after 5 failed attempts — wait and retry later.' }] };
|
|
166
|
+
}
|
|
167
|
+
if (result.code === 429) {
|
|
168
|
+
return { content: [{ type: 'text', text: '❌ Too many attempts — rate limited. Wait a minute, then request a fresh code via auth_email_code.' }] };
|
|
169
|
+
}
|
|
170
|
+
return { content: [{ type: 'text', text: `❌ Login failed: ${result.message}` }] };
|
|
171
|
+
}
|
|
134
172
|
if (params.email && params.password) {
|
|
135
173
|
const result = await authLogin(params.email, params.password);
|
|
136
174
|
if (result.code === 0 && result.data) {
|
|
@@ -138,21 +176,70 @@ export async function startMcpServer() {
|
|
|
138
176
|
addProfile({ name: 'default', url: getBaseUrl(), token, active: true });
|
|
139
177
|
return { content: [{ type: 'text', text: '✅ Login successful!' }] };
|
|
140
178
|
}
|
|
141
|
-
return { content: [{ type: 'text', text: `❌ Login failed: ${result.message}
|
|
179
|
+
return { content: [{ type: 'text', text: `❌ Login failed: ${result.message}\nHint: accounts registered via email verification code or Google have no password. Use the token method (Account Settings → Access Token) or email verification code (auth_email_code) instead.` }] };
|
|
142
180
|
}
|
|
143
|
-
return { content: [{ type: 'text', text: '❌
|
|
181
|
+
return { content: [{ type: 'text', text: '❌ Provide `token`, or `email` + `code`, or `email` + `password`. Recommended: ask the user for the Access Token (formlm.me → Workspace → Account Settings), or start an email verification-code login via auth_email_code.' }] };
|
|
144
182
|
});
|
|
145
|
-
server.tool('
|
|
183
|
+
server.tool('auth_email_code', [
|
|
184
|
+
'Request an email verification code for FormLM login (part of auth_login method 2 — no password needed).',
|
|
185
|
+
'',
|
|
186
|
+
'Two-step usage:',
|
|
187
|
+
'1. Call WITHOUT `captcha` → returns a captcha image. Show it to the user and ask them to read the 4 digits.',
|
|
188
|
+
' (The image is a human-verification gate — you cannot solve it yourself; the user must read it.)',
|
|
189
|
+
'2. Call WITH `captcha` (the 4 digits) → if correct, a 6-digit code is emailed to the user (valid 5 minutes).',
|
|
190
|
+
' Then ask the user for that code and call auth_login with { email, code }.',
|
|
191
|
+
'',
|
|
192
|
+
'Server rate limits (anti-abuse): 1 send per email per 60s, 10 sends per IP per minute.',
|
|
193
|
+
'On 429, tell the user to wait a minute. On 403, the digits were wrong — fetch a fresh image and try once more.',
|
|
194
|
+
].join('\n'), {
|
|
195
|
+
email: z.string().describe('Account email (the verification code will be sent here)'),
|
|
196
|
+
captcha: z.string().optional().describe('The 4 digits the user read from the captcha image (omit on first call to get the image)'),
|
|
197
|
+
}, async (params) => {
|
|
198
|
+
if (!params.captcha) {
|
|
199
|
+
const cap = await fetchCaptchaImage(params.email);
|
|
200
|
+
if (!cap.ok || !cap.image) {
|
|
201
|
+
return { content: [{ type: 'text', text: `❌ Failed to fetch captcha image: ${cap.message}` }] };
|
|
202
|
+
}
|
|
203
|
+
const content = [
|
|
204
|
+
{
|
|
205
|
+
type: 'image',
|
|
206
|
+
data: cap.image.toString('base64'),
|
|
207
|
+
mimeType: 'image/gif',
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
type: 'text',
|
|
211
|
+
text: '👆 Captcha image (valid 60s). Show it to the user and ask them to read the 4 digits, then call auth_email_code again with { email, captcha }. If this client cannot display images, run `formlm-cli auth login` in a terminal instead (option 2 saves the image to ~/.formlm/captcha.gif and opens it).',
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
return { content };
|
|
215
|
+
}
|
|
216
|
+
const send = await authSendEmailCode(params.email, params.captcha);
|
|
217
|
+
if (send.code === 200) {
|
|
218
|
+
return { content: [{ type: 'text', text: `✅ Verification code sent to ${params.email}. Ask the user to check their inbox (and spam folder) and give you the 6-digit code (valid 5 minutes), then call auth_login with { email, code }.` }] };
|
|
219
|
+
}
|
|
220
|
+
if (send.code === 403) {
|
|
221
|
+
return { content: [{ type: 'text', text: '❌ Wrong captcha digits. Call auth_email_code again WITHOUT captcha to get a fresh image, and ask the user to read it once more.' }] };
|
|
222
|
+
}
|
|
223
|
+
if (send.code === 429) {
|
|
224
|
+
return { content: [{ type: 'text', text: '❌ Rate limited (1 send per email per 60s, 10 per IP per minute). Ask the user to wait a minute, then request a fresh captcha image.' }] };
|
|
225
|
+
}
|
|
226
|
+
return { content: [{ type: 'text', text: `❌ Failed to send code: ${send.message}` }] };
|
|
227
|
+
});
|
|
228
|
+
server.tool('auth_status', [
|
|
229
|
+
'Check current login status.',
|
|
230
|
+
'Tokens expire after 7 days — if this returns "Token invalid (expired)", ask the user to copy a fresh',
|
|
231
|
+
'Access Token from formlm.me → Workspace → Account Settings and re-login via auth_login.',
|
|
232
|
+
].join('\n'), {}, async () => {
|
|
146
233
|
const profile = getActiveProfile();
|
|
147
234
|
if (!profile) {
|
|
148
|
-
return { content: [{ type: 'text', text: '❌ Not logged in. Use auth_login to authenticate.' }] };
|
|
235
|
+
return { content: [{ type: 'text', text: '❌ Not logged in. Use auth_login to authenticate (recommended: Access Token from formlm.me → Workspace → Account Settings).' }] };
|
|
149
236
|
}
|
|
150
237
|
const result = await authMe();
|
|
151
238
|
if (result.code === 0) {
|
|
152
239
|
const userInfo = typeof result.data === 'object' ? result.data.userName : result.data;
|
|
153
240
|
return { content: [{ type: 'text', text: `✅ Logged in. Server: ${profile.url}\nUser: ${userInfo}` }] };
|
|
154
241
|
}
|
|
155
|
-
return { content: [{ type: 'text', text: `❌ Token invalid: ${result.message}
|
|
242
|
+
return { content: [{ type: 'text', text: `❌ Token invalid: ${result.message}\nTokens expire after 7 days. Ask the user to copy a fresh Access Token from formlm.me → Workspace → Account Settings, then re-login via auth_login.` }] };
|
|
156
243
|
});
|
|
157
244
|
// ── Tier 1: Smart Pipeline (Natural Language → Plan + Sequential Execute) ─
|
|
158
245
|
//
|
|
@@ -161,19 +248,27 @@ export async function startMcpServer() {
|
|
|
161
248
|
server.tool('formlm_generate', [
|
|
162
249
|
'Generate an execution plan from natural language description (Phase 1 only — does NOT execute any module).',
|
|
163
250
|
'Server runs: Plan AI (assess-plan.md) → generates task list for 6 modules (form/scale/connect/report/expert/share).',
|
|
164
|
-
'Returns: appId, planType, plan JSON, and task list. Takes ~10-30 seconds.',
|
|
251
|
+
'Returns: appId, planType, name, plan JSON, and task list. Takes ~10-30 seconds.',
|
|
252
|
+
'This tool is plan-only — the name "generate" predates the split. There is no single call that produces a live app;',
|
|
253
|
+
'that is generate → execute×N (or the CLI `formlm-cli smart generate` wrapper).',
|
|
165
254
|
'',
|
|
166
255
|
'## Scene Templates (when user description is vague, present these and ask them to choose):',
|
|
167
256
|
'1. assessment: 评估量表 — 多维度打分 + 分值区间解读报告 (MOST COMMON, for psych/workplace/health)',
|
|
168
257
|
'2. consultation: 咨询评估 — 评估 + AI专家对话解读 (for mental health / coaching)',
|
|
169
258
|
'3. survey: 问卷调查 — 仅收集数据,无打分 (for feedback / research)',
|
|
170
259
|
'4. exam: 考试测验 — 标准答案 + 对错判分 (for education / training)',
|
|
171
|
-
'5.
|
|
260
|
+
'5. report: 外部评测 — 对外部对象打分评价+报告 (for performance reviews / 360 feedback)',
|
|
172
261
|
'6. learn: 学习卡片 — 知识点 + 自测题 (for micro-learning)',
|
|
173
262
|
'',
|
|
174
263
|
'## AFTER SUCCESS — execute modules sequentially via formlm_execute:',
|
|
175
|
-
'
|
|
176
|
-
'
|
|
264
|
+
'The plan is cached server-side (~10 min TTL) — just call formlm_execute with appId and module for each module.',
|
|
265
|
+
'No need to pass the plan JSON back — it is handled automatically.',
|
|
266
|
+
'IMPORTANT: module coverage depends on planType. Only `consultation` plans include the `expert` module;',
|
|
267
|
+
'assessment / exam / report / survey / learn plans do NOT. If the app must have an AI expert, create it',
|
|
268
|
+
'explicitly afterwards with formlm_exec: "assess expert config --app <id> --name ... --role ... --kbText ..."',
|
|
269
|
+
'(otherwise the published page promises an expert the app does not have — silent functional gap).',
|
|
270
|
+
'After the ~10 min cache expires, formlm_execute still works if you pass the plan JSON back via `plan`',
|
|
271
|
+
'(the full plan is returned in this tool\'s output) — re-running this tool would create a NEW app instead.',
|
|
177
272
|
'',
|
|
178
273
|
'## USER FEEDBACK (IMPORTANT for good UX):',
|
|
179
274
|
'After getting the plan, tell the user: "✅ 计划已生成!" and list the modules.',
|
|
@@ -181,7 +276,7 @@ export async function startMcpServer() {
|
|
|
181
276
|
'After each call, tell the user the module is done.',
|
|
182
277
|
'After the last module (share), the 3 app URLs are returned automatically — present them to the user.',
|
|
183
278
|
'',
|
|
184
|
-
'After all modules are executed, use
|
|
279
|
+
'After all modules are executed, use formlm_doctor to verify quality (and formlm_snapshot for raw state).',
|
|
185
280
|
'',
|
|
186
281
|
'## Reference Documents:',
|
|
187
282
|
'If the user has reference documents (questionnaire files, scoring criteria), ask them to paste the content',
|
|
@@ -190,22 +285,34 @@ export async function startMcpServer() {
|
|
|
190
285
|
input: z.string().describe('Natural language description of the app. Be specific: mention topic, audience, number of questions, dimensions/subscales, scoring, visual style. ' +
|
|
191
286
|
'Example: "A workplace stress assessment for office workers with 3 dimensions (workload, autonomy, support), 15 questions, score 0-60, detailed result interpretation, dark professional style". ' +
|
|
192
287
|
'Supports up to 8000 characters. If user pastes reference documents, include them here.'),
|
|
193
|
-
planType: z.enum(['assessment', 'consultation', 'survey', 'exam', '
|
|
288
|
+
planType: z.enum(['assessment', 'consultation', 'survey', 'exam', 'report', 'learn']).optional().describe('Plan type. Choose based on user needs: ' +
|
|
194
289
|
'"assessment" (scoring+report, for psych/workplace/health evaluations, MOST COMMON), ' +
|
|
195
290
|
'"consultation" (scoring+report+AI expert chat, for mental health/coaching), ' +
|
|
196
291
|
'"survey" (no scoring, for feedback/research), ' +
|
|
197
292
|
'"exam" (correct-answer scoring, for education/training), ' +
|
|
198
|
-
'"
|
|
293
|
+
'"report" (external evaluation/rating of others, for performance reviews), ' +
|
|
199
294
|
'"learn" (knowledge cards + self-test, for micro-learning). ' +
|
|
200
295
|
'Default: auto-detected by Plan AI based on your description.'),
|
|
201
296
|
style: z.string().optional().describe('Visual style preference. Suggested options: "温暖亲切" (warm friendly, for health/care), ' +
|
|
202
297
|
'"正式专业" (formal professional, for workplace/corporate), ' +
|
|
203
298
|
'"简洁直接" (minimal clean, for general use), ' +
|
|
204
|
-
'"轻松活泼" (lively playful, for
|
|
299
|
+
'"轻松活泼" (lively playful, for exam/learn scenarios). ' +
|
|
205
300
|
'Or custom: "深色科技风" / "warm friendly pastel" / "minimal clean white".'),
|
|
206
|
-
questionCount: z.enum(['10-15', '15-20', '20-30']).optional().describe('Target question count range: "
|
|
301
|
+
questionCount: z.enum(['5-9', '10-15', '15-20', '20-30', '30-50', '50-100']).optional().describe('Target question count range: "5-9" (micro check-in, ~2 min), '
|
|
302
|
+
+ '"10-15" (quick screening, 3-5 min), ' +
|
|
207
303
|
'"15-20" (standard assessment, 5-8 min), ' +
|
|
208
|
-
'"20-30" (deep assessment, 8-15 min)
|
|
304
|
+
'"20-30" (deep assessment, 8-15 min), ' +
|
|
305
|
+
'"30-50" (facet-level), "50-100" (full inventory). Default: auto-decided by AI based on planType.'),
|
|
306
|
+
dimensions: z.string().optional().describe('PIN the scoring dimensions (names and count) instead of letting the Plan AI invent them — separate with | or ; ' +
|
|
307
|
+
'(e.g. "Natural Finish|Glam Intensity|Editorial|Color Confidence"). Use whenever an external page/ledger already ' +
|
|
308
|
+
'names the dimensions: unpinned dimensions get renamed/recounted by the AI and every dependent page needs rework.'),
|
|
309
|
+
appName: z.string().optional().describe('PIN the app display name (default: chosen by the Plan AI). Use for batch creation when the caller must know the ' +
|
|
310
|
+
'exact name up front to map apps to pages/ledgers.'),
|
|
311
|
+
lang: z.string().optional().describe('BCP-47 language code (e.g. "zh", "zh-hant", "zh-tw", "en", "ja"). Injects an [OUTPUT LANGUAGE] directive ' +
|
|
312
|
+
'that anchors AI output language for the plan AND every module execute. This is the only language entry point ' +
|
|
313
|
+
'on the MCP channel (no Accept-Language header available there) — without it, non-default-language apps drift ' +
|
|
314
|
+
'(e.g. zh-hant apps got Simplified Chinese page names and missed certificate pages). ' +
|
|
315
|
+
'Default: server default (Simplified Chinese).'),
|
|
209
316
|
}, async (params) => {
|
|
210
317
|
let cmd = `assess smart plan --input "${escapeArg(params.input)}"`;
|
|
211
318
|
if (params.planType)
|
|
@@ -214,6 +321,12 @@ export async function startMcpServer() {
|
|
|
214
321
|
cmd += ` --style "${escapeArg(params.style)}"`;
|
|
215
322
|
if (params.questionCount)
|
|
216
323
|
cmd += ` --question-count ${params.questionCount}`;
|
|
324
|
+
if (params.dimensions)
|
|
325
|
+
cmd += ` --dimensions "${escapeArg(params.dimensions)}"`;
|
|
326
|
+
if (params.appName)
|
|
327
|
+
cmd += ` --app-name "${escapeArg(params.appName)}"`;
|
|
328
|
+
if (params.lang)
|
|
329
|
+
cmd += ` --lang ${params.lang}`;
|
|
217
330
|
cmd += ' --json';
|
|
218
331
|
const r = await execCommand(cmd, undefined, TIMEOUT_PLAN);
|
|
219
332
|
const text = formatGenerateResult(r);
|
|
@@ -236,14 +349,31 @@ export async function startMcpServer() {
|
|
|
236
349
|
'After the last module (share), present the 3 URLs to the user.',
|
|
237
350
|
'',
|
|
238
351
|
'## PLAN PASSING:',
|
|
239
|
-
'
|
|
240
|
-
'
|
|
352
|
+
'Plan is cached server-side (~10 min) after formlm_generate — no need to pass it manually.',
|
|
353
|
+
'Just call formlm_execute with appId and module for each module in sequence.',
|
|
354
|
+
'If you get "Plan not found in server cache", pass the plan JSON from the formlm_generate output into `plan`',
|
|
355
|
+
'— do NOT re-run formlm_generate (that creates a brand-new app).',
|
|
356
|
+
'Success is structured: the response carries appId / module / taskStatus / status(success|error) — trust taskStatus,',
|
|
357
|
+
'not human wording. A retried `scale` execute appends dimensions unless existing ones are cleared first',
|
|
358
|
+
'(formlm_exec: "assess scale clear --app <id>").',
|
|
241
359
|
].join('\n'), {
|
|
242
360
|
appId: z.string().describe('App ID from formlm_generate result'),
|
|
243
361
|
module: z.string().describe('Module to execute: form / scale / connect / report / expert / share'),
|
|
244
|
-
plan: z.string().describe('
|
|
362
|
+
plan: z.string().optional().describe('Full plan JSON (optional — the plan is cached server-side for ~10 min after formlm_generate; pass this only to resume after the cache expired)'),
|
|
245
363
|
}, async (params) => {
|
|
246
|
-
|
|
364
|
+
let cmd = `assess smart execute --app ${params.appId} --module ${params.module}`;
|
|
365
|
+
if (params.plan) {
|
|
366
|
+
// Same transport contract as the CLI: plan goes as Base64 in ONE token. Inline JSON is
|
|
367
|
+
// cut apart by the server command preprocessor (task text contains ", assess ..." or ";"),
|
|
368
|
+
// which silently replaces the plan with unrelated sub-commands.
|
|
369
|
+
try {
|
|
370
|
+
cmd += ` --plan-b64 ${Buffer.from(JSON.stringify(JSON.parse(params.plan)), 'utf-8').toString('base64')}`;
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return { content: [{ type: 'text', text: '❌ `plan` is not valid JSON — pass the full plan object returned by formlm_generate (its "plan" field).' }] };
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
cmd += ' --json';
|
|
247
377
|
const r = await execCommand(cmd, undefined, TIMEOUT_EXECUTE);
|
|
248
378
|
if (r.code !== 0) {
|
|
249
379
|
return { content: [{ type: 'text', text: `❌ [${r.code}] ${r.message}` }] };
|
|
@@ -262,12 +392,6 @@ export async function startMcpServer() {
|
|
|
262
392
|
else {
|
|
263
393
|
lines.push(`✅ Module '${params.module}' completed (status: ${status})`);
|
|
264
394
|
}
|
|
265
|
-
// Include updated plan JSON for the next call
|
|
266
|
-
if (parsed.plan) {
|
|
267
|
-
lines.push('');
|
|
268
|
-
lines.push('📋 Updated Plan JSON (use this for the next formlm_execute call):');
|
|
269
|
-
lines.push(parsed.plan);
|
|
270
|
-
}
|
|
271
395
|
// If this is the share module (last one), auto-fetch URLs
|
|
272
396
|
if (params.module === 'share') {
|
|
273
397
|
const urlResult = await execCommand(`assess app urls --app ${params.appId} --json`, undefined, TIMEOUT_DEFAULT);
|
|
@@ -276,8 +400,14 @@ export async function startMcpServer() {
|
|
|
276
400
|
const urls = JSON.parse(urlResult.data || urlResult.message);
|
|
277
401
|
lines.push('');
|
|
278
402
|
lines.push('🎉 All modules complete! Your app is ready:');
|
|
279
|
-
if (urls.shareUrl)
|
|
280
|
-
lines.push(`🔗 Fill-in URL: ${urls.shareUrl}`);
|
|
403
|
+
if (urls.shareUrlAbsolute || urls.shareUrl)
|
|
404
|
+
lines.push(`🔗 Fill-in URL: ${urls.shareUrlAbsolute || urls.shareUrl}`);
|
|
405
|
+
// shareToken is a first-class field server-side; fall back to URL extraction for older servers
|
|
406
|
+
const token = urls.shareToken || shareTokenFromUrl(urls.shareUrl);
|
|
407
|
+
if (token)
|
|
408
|
+
lines.push(`🔑 ShareToken: ${token}`);
|
|
409
|
+
if (urls.shareType)
|
|
410
|
+
lines.push(`🌐 Access type: ${urls.shareType}${urls.shareDay >= 3650000 ? ' (permanent)' : ''}${urls.shareType === 'all' ? ' — requires login!' : ''}`);
|
|
281
411
|
if (urls.builderUrl)
|
|
282
412
|
lines.push(`🎨 Editor URL: ${urls.builderUrl}`);
|
|
283
413
|
if (urls.dataUrl)
|
|
@@ -299,7 +429,7 @@ export async function startMcpServer() {
|
|
|
299
429
|
}
|
|
300
430
|
else {
|
|
301
431
|
lines.push('');
|
|
302
|
-
lines.push('Next: Call formlm_execute with the next module
|
|
432
|
+
lines.push('Next: Call formlm_execute with the next module (plan is cached server-side).');
|
|
303
433
|
}
|
|
304
434
|
text = lines.join('\n');
|
|
305
435
|
}
|
|
@@ -315,9 +445,16 @@ export async function startMcpServer() {
|
|
|
315
445
|
'',
|
|
316
446
|
'Call this BEFORE making changes to understand what already exists.',
|
|
317
447
|
'Call this AFTER formlm_generate to verify the generated app.',
|
|
448
|
+
'',
|
|
449
|
+
'Prefer summary=true for batch audits / consistency checks: it returns a compact profile',
|
|
450
|
+
'(appName, fieldCount, questionCount, dimCount+dimNames, reportPageCount+reportPages,',
|
|
451
|
+
'styled, expertEnabled/expertHasContent, share.type/perm/day/forever/shareToken) instead of',
|
|
452
|
+
'the full module payloads, so you do not have to parse widget layoutData or markdown tables.',
|
|
453
|
+
'If any module fetch fails the result carries _errors/_degraded — treat it as UNKNOWN, not as "empty module".',
|
|
318
454
|
].join('\n'), {
|
|
319
455
|
appId: z.string().describe('App ID'),
|
|
320
456
|
module: z.string().optional().describe('Get only one module: form / scale / connect / report / expert / share (default: all 6)'),
|
|
457
|
+
summary: z.boolean().optional().describe('Return the compact audit profile instead of full module payloads (recommended for batch verification)'),
|
|
321
458
|
}, async (params) => {
|
|
322
459
|
const validModules = ['form', 'scale', 'connect', 'report', 'expert', 'share'];
|
|
323
460
|
const modules = params.module ? [params.module] : validModules;
|
|
@@ -334,28 +471,98 @@ export async function startMcpServer() {
|
|
|
334
471
|
share: `assess share query --app ${params.appId} --json`,
|
|
335
472
|
};
|
|
336
473
|
const entries = modules.filter((m) => commands[m]);
|
|
337
|
-
|
|
338
|
-
const
|
|
474
|
+
// App title is not part of any module payload — fetch it in parallel for name↔page audits
|
|
475
|
+
const [results, appResult] = await Promise.all([
|
|
476
|
+
Promise.all(entries.map((m) => execCommand(commands[m], undefined, TIMEOUT_DEFAULT).then((r) => [m, r]))),
|
|
477
|
+
execCommand(`assess app use --app ${params.appId} --json`, undefined, TIMEOUT_DEFAULT),
|
|
478
|
+
]);
|
|
479
|
+
let appName = '';
|
|
480
|
+
if (appResult.code === 0 && appResult.data) {
|
|
481
|
+
try {
|
|
482
|
+
appName = (typeof appResult.data === 'string' ? JSON.parse(appResult.data) : appResult.data)?.name || '';
|
|
483
|
+
}
|
|
484
|
+
catch { }
|
|
485
|
+
}
|
|
486
|
+
const snapshot = { appId: params.appId, appName: appName || null };
|
|
339
487
|
const errors = [];
|
|
488
|
+
const parsedMods = {};
|
|
340
489
|
for (const [module, result] of results) {
|
|
341
490
|
if (result.code === 0) {
|
|
342
491
|
// All modules now use --json, so parse uniformly
|
|
343
492
|
try {
|
|
344
493
|
snapshot[module] = JSON.parse(result.data);
|
|
494
|
+
parsedMods[module] = snapshot[module];
|
|
345
495
|
}
|
|
346
496
|
catch {
|
|
347
497
|
snapshot[module] = result.data || result.message;
|
|
498
|
+
parsedMods[module] = null;
|
|
348
499
|
}
|
|
349
500
|
}
|
|
350
501
|
else {
|
|
502
|
+
// Partial-failure visibility so auditors never read a fetch error as "module is empty"
|
|
351
503
|
snapshot[module] = null;
|
|
352
|
-
|
|
504
|
+
snapshot['_degraded'] = true;
|
|
505
|
+
errors.push(`${module}: [${result.code}] ${result.message}`);
|
|
353
506
|
}
|
|
354
507
|
}
|
|
355
508
|
if (errors.length > 0)
|
|
356
509
|
snapshot['_errors'] = errors;
|
|
510
|
+
if (params.summary) {
|
|
511
|
+
const summary = buildSummary(params.appId, appName, parsedMods);
|
|
512
|
+
if (errors.length > 0) {
|
|
513
|
+
summary['_errors'] = errors;
|
|
514
|
+
summary['_degraded'] = true;
|
|
515
|
+
// Zero counts from a failed read are UNKNOWNS — say so, or batch auditors report
|
|
516
|
+
// "app has 0 fields / not published" for apps that were simply unreachable.
|
|
517
|
+
if (Object.values(parsedMods).every(v => v === null)) {
|
|
518
|
+
summary['_note'] = 'All module reads failed — the zero counts are UNKNOWNS, not empty state. Retry before concluding.';
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return { content: [{ type: 'text', text: JSON.stringify(summary) }] };
|
|
522
|
+
}
|
|
357
523
|
return { content: [{ type: 'text', text: JSON.stringify(snapshot, null, 2) }] };
|
|
358
524
|
});
|
|
525
|
+
server.tool('formlm_doctor', [
|
|
526
|
+
'Read-only quality inspection of ONE app — the fastest way to verify a generated app before wiring it into a page.',
|
|
527
|
+
'Returns {ok, failCount, warnCount, summary, findings[]} where each finding is {name, level(pass|warn|fail), detail, fix?}.',
|
|
528
|
+
'',
|
|
529
|
+
'Checks: content/scoring coverage, expert enabled-but-empty or referenced-but-missing (dead AI assistant),',
|
|
530
|
+
'styling applied, dimensions without report pages, unexpected certificate page, share access semantics',
|
|
531
|
+
'(type=all/owner = login required — anonymous respondents get the login page, NOT the form), link expiry,',
|
|
532
|
+
'and fill-in URL reachability.',
|
|
533
|
+
'Set expectLang (e.g. "en", "zh-hant") to catch text written in the wrong script — the classic batch defect',
|
|
534
|
+
'where an English app ends up with Chinese certificate/report boilerplate. Use deep=true to also scan report',
|
|
535
|
+
'widget BODY text (extra queries; titles/labels are always scanned).',
|
|
536
|
+
'',
|
|
537
|
+
'Read-only: this tool never writes. Apply the returned `fix` commands via formlm_exec.',
|
|
538
|
+
].join('\n'), {
|
|
539
|
+
appId: z.string().describe('App ID'),
|
|
540
|
+
expectLang: z.string().optional().describe('BCP-47 expected language, e.g. "en" / "zh" / "zh-hant" / "ja". Enables the script-consistency scan.'),
|
|
541
|
+
deep: z.boolean().optional().describe('Also fetch and scan report widget body text (slower, more accurate language check)'),
|
|
542
|
+
probe: z.boolean().optional().describe('Probe the fill-in URL for reachability (default true)'),
|
|
543
|
+
}, async (params) => {
|
|
544
|
+
if (params.expectLang && !isValidLangCode(params.expectLang)) {
|
|
545
|
+
return { content: [{ type: 'text', text: `❌ Invalid expectLang "${params.expectLang}". Use a BCP-47 code like en / zh / zh-hant / ja.` }] };
|
|
546
|
+
}
|
|
547
|
+
const report = await runDoctor(params.appId, {
|
|
548
|
+
expectLang: params.expectLang,
|
|
549
|
+
deep: !!params.deep,
|
|
550
|
+
probe: params.probe !== false,
|
|
551
|
+
});
|
|
552
|
+
const lines = [];
|
|
553
|
+
lines.push(report.ok
|
|
554
|
+
? `✅ Doctor passed (${report.warnCount} warning(s)) — ${report.appName || report.appId}`
|
|
555
|
+
: `❌ Doctor found ${report.failCount} blocking issue(s), ${report.warnCount} warning(s) — ${report.appName || report.appId}`);
|
|
556
|
+
for (const f of report.findings) {
|
|
557
|
+
const icon = f.level === 'pass' ? '✅' : f.level === 'warn' ? '⚠️ ' : '❌';
|
|
558
|
+
lines.push(`${icon} ${f.name}: ${f.detail}`);
|
|
559
|
+
if (f.fix)
|
|
560
|
+
lines.push(` ↳ fix: ${f.fix}`);
|
|
561
|
+
}
|
|
562
|
+
lines.push('');
|
|
563
|
+
lines.push('Compact summary: ' + JSON.stringify(report.summary));
|
|
564
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
565
|
+
});
|
|
359
566
|
server.tool('formlm_skill', [
|
|
360
567
|
'Get the full SKILL.md domain knowledge for a specific skill module.',
|
|
361
568
|
'Same documents that AssessAgent loads internally — contains P0/P1/P2 constraints, parameter rules, examples.',
|
|
@@ -374,16 +581,19 @@ export async function startMcpServer() {
|
|
|
374
581
|
});
|
|
375
582
|
// ── Tier 3: Direct Execution (Advanced) ──────────────────────────
|
|
376
583
|
//
|
|
377
|
-
// Whitelisted
|
|
378
|
-
// assess app : list / create / use / update / remove
|
|
584
|
+
// Whitelisted commands — server granularity is 3 tokens (McpV1Api.ALLOWED_SUBCOMMANDS):
|
|
585
|
+
// assess app : list / create / use / current / update / remove / urls
|
|
379
586
|
// assess form : query / find / types / config / add / update / remove / move / set-property
|
|
380
587
|
// assess scale : query / find / add / update / set / remove / clear / config / keys / data
|
|
381
588
|
// assess connect: query / find / types / config / cover-page / final-page / main-page / style
|
|
382
|
-
// assess report : query / find / update / page / widget
|
|
589
|
+
// assess report : query / find / update / page / widget (widget carries the `logic` sub-command)
|
|
383
590
|
// assess expert : query / find / config / set / avatar / remove / chat
|
|
384
|
-
// assess share : set / query / url
|
|
591
|
+
// assess share : set / query / url / api / flavor
|
|
385
592
|
// assess smart : plan / execute
|
|
386
593
|
// assess skill : form / scale / connect / report / expert / share
|
|
594
|
+
// NOTE: `snapshot` and `field ...` are CLIENT-side synthesized CLI commands (they fan out
|
|
595
|
+
// to `assess <module> query`), so "assess snapshot" / "assess field list" are not on this
|
|
596
|
+
// channel and return 403 by design — use the formlm_snapshot tool or the module queries.
|
|
387
597
|
server.tool('formlm_exec', [
|
|
388
598
|
'Execute a raw FormLM CLI command directly.',
|
|
389
599
|
'Use this for fine-grained control that formlm_generate doesn\'t cover, or for modifying existing apps.',
|
|
@@ -397,26 +607,32 @@ export async function startMcpServer() {
|
|
|
397
607
|
' NEVER call a remove command based on a vague reference ("delete it", "remove that one")',
|
|
398
608
|
' without first resolving and confirming the exact target. Deletion is irreversible.',
|
|
399
609
|
'',
|
|
400
|
-
'Whitelisted
|
|
401
|
-
' App: assess app list/create/use/update/remove/urls',
|
|
610
|
+
'Whitelisted commands (3-token granularity; anything else returns 403):',
|
|
611
|
+
' App: assess app list/create/use/current/update/remove/urls',
|
|
402
612
|
' Form: assess form query/find/types/config/add/update/remove/move/set-property',
|
|
403
613
|
' Scale: assess scale query/find/add/update/set/remove/clear/config/keys/data',
|
|
404
614
|
' Connect: assess connect query/find/types/config/cover-page/final-page/main-page/style',
|
|
405
|
-
' Report: assess report query/find/update/page/widget/
|
|
615
|
+
' Report: assess report query/find/update/page/widget (conditional rules: assess report widget logic add/list/remove)',
|
|
406
616
|
' Expert: assess expert query/find/config/set/avatar/remove/chat',
|
|
407
|
-
' Share: assess share set/query/url',
|
|
617
|
+
' Share: assess share set/query/url/api/flavor',
|
|
408
618
|
' Smart: assess smart plan/execute',
|
|
619
|
+
' Skill: assess skill form/scale/connect/report/expert/share',
|
|
620
|
+
' NOT available here: assess snapshot / assess field * (CLI-only wrappers of the module queries)',
|
|
409
621
|
'',
|
|
410
622
|
'Common patterns:',
|
|
411
|
-
' "assess app list --json"
|
|
412
|
-
' "assess app urls --app <id> --json"
|
|
623
|
+
' "assess app list --all --with-urls --json" (full inventory incl. shareToken per app)',
|
|
624
|
+
' "assess app urls --app <id> --json" (URLs + published/shareType/shareToken)',
|
|
413
625
|
' "assess form add --app <id> --id q1 --name \\"Name\\" --type radio --options \\"A:1,B:2,C:3\\" --json"',
|
|
414
626
|
' "assess scale add --app <id> --id stress --name \\"Stress\\" --format sum --kbText \\"...\\" --json"',
|
|
415
627
|
' "assess scale keys add --app <id> --scale stress --fields X1,X2,X3 --json"',
|
|
416
|
-
' "assess scale data add --app <id> --scale stress --
|
|
417
|
-
' "assess
|
|
418
|
-
' "assess share
|
|
419
|
-
' "assess
|
|
628
|
+
' "assess scale data add --app <id> --scale stress --bands \\"Normal:desc||Mild:desc||Severe:desc\\" --json" (recommended: server auto-computes even boundaries + 999 sentinel)',
|
|
629
|
+
' "assess scale data add --app <id> --scale stress --ranges \\"0-7:Normal,8-14:Mild,15-21:Severe\\" --json" (manual boundaries; only for non-even knowledge-specified thresholds)',
|
|
630
|
+
' "assess share set --app <id> --form-type visitor --form-perm 1 --form-day 0 --json" (anonymous + permanent)',
|
|
631
|
+
' "assess share set --app <id> --form-type all --form-perm 1 --json" (logged-in users only)',
|
|
632
|
+
' "assess share query --app <id> --json" (verify type/perm/day triple)',
|
|
633
|
+
' "assess expert config --app <id> --name \\"...\\" --role \\"...\\" --kbText \\"...\\" --json" (adds + enables an expert; --name/--kbText required)',
|
|
634
|
+
' "assess expert set --app <id> --property enable --value false --json" (single-property toggle)',
|
|
635
|
+
' "assess connect style apply-all --app <id> --look \\"心理健康评估,温暖治愈风格\\" --theme minimalist --json" (AI style, takes 30-120s)',
|
|
420
636
|
' "assess app remove --app <id> --json" (IRREVERSIBLE — confirm first, see SAFETY above)',
|
|
421
637
|
].join('\n'), {
|
|
422
638
|
command: z.string().describe('Full CLI command starting with "assess". Do NOT include the "formlm-cli" prefix. ' +
|
|
@@ -446,6 +662,6 @@ export async function startMcpServer() {
|
|
|
446
662
|
// ── Start Server ──────────────────────────────────────────────────
|
|
447
663
|
const transport = new StdioServerTransport();
|
|
448
664
|
await server.connect(transport);
|
|
449
|
-
console.error(`FormLM MCP Server v${VERSION} running on stdio (
|
|
665
|
+
console.error(`FormLM MCP Server v${VERSION} running on stdio (9 tools + 6 resources)`);
|
|
450
666
|
}
|
|
451
667
|
//# sourceMappingURL=mcp.js.map
|