@formlm/cli 0.3.1 → 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 +336 -61
- 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 +269 -49
- 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 ──────────────────────────
|
|
@@ -43,10 +45,18 @@ function formatGenerateResult(r) {
|
|
|
43
45
|
lines.push(` ${t.seq}. [${t.skill}] ${t.title}`);
|
|
44
46
|
}
|
|
45
47
|
lines.push('');
|
|
46
|
-
lines.push('Next: Use formlm_execute to execute each module sequentially (plan is cached server-side):');
|
|
48
|
+
lines.push('Next: Use formlm_execute to execute each module sequentially (plan is cached server-side for ~10 minutes):');
|
|
47
49
|
for (const t of parsed.tasks) {
|
|
48
50
|
lines.push(` formlm_execute: appId=${parsed.appId}, module=${t.skill}`);
|
|
49
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"`);
|
|
59
|
+
}
|
|
50
60
|
}
|
|
51
61
|
return lines.join('\n');
|
|
52
62
|
}
|
|
@@ -59,10 +69,10 @@ function formatGenerateResult(r) {
|
|
|
59
69
|
// ── MCP timeouts ─────────────────────────────────────────────────────────────
|
|
60
70
|
// Most commands complete in < 5 seconds.
|
|
61
71
|
// Smart pipeline (AssessAgent) takes 30-300 seconds.
|
|
62
|
-
const TIMEOUT_DEFAULT = 60_000; // 60s for all direct CLI commands
|
|
63
|
-
const TIMEOUT_PLAN = 120_000; // 2min for smart plan (Phase 1 only, ~10-30s)
|
|
64
|
-
const TIMEOUT_EXECUTE = 300_000; // 5min for smart execute (single module, ~30-120s)
|
|
65
|
-
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)
|
|
66
76
|
export async function startMcpServer() {
|
|
67
77
|
const server = new McpServer({
|
|
68
78
|
name: 'formlm',
|
|
@@ -81,8 +91,8 @@ export async function startMcpServer() {
|
|
|
81
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.',
|
|
82
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.',
|
|
83
93
|
report: 'Report pages & widgets — P0: system variables use {{double braces}} e.g. {{TotalScore}}, logic conditions use actual score values (never percentages).',
|
|
84
|
-
expert: 'AI expert agent config — P0: "expert config" for full setup (requires --kbText), "expert set"
|
|
85
|
-
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).',
|
|
86
96
|
};
|
|
87
97
|
for (const skillId of skillIds) {
|
|
88
98
|
server.resource(skillId, `formlm://skills/${skillId}`, {
|
|
@@ -100,26 +110,40 @@ export async function startMcpServer() {
|
|
|
100
110
|
});
|
|
101
111
|
}
|
|
102
112
|
// ════════════════════════════════════════════════════════════════
|
|
103
|
-
//
|
|
113
|
+
// MCP TOOLS — Layered Architecture (9 tools)
|
|
104
114
|
//
|
|
105
115
|
// Recommended workflow (smart pipeline):
|
|
106
116
|
// 1. auth_login → authenticate
|
|
107
117
|
// 2. formlm_generate → generate plan (Phase 1, ~10-30s)
|
|
108
118
|
// 3. formlm_execute → execute each module sequentially (form→scale→connect→report→expert→share)
|
|
109
|
-
// 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)
|
|
110
121
|
// 5. formlm_exec → fine-grained modifications (advanced)
|
|
111
122
|
// ════════════════════════════════════════════════════════════════
|
|
112
123
|
// ── Tier 0: Authentication ────────────────────────────────────
|
|
113
124
|
server.tool('auth_login', [
|
|
114
|
-
'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.',
|
|
115
139
|
'',
|
|
116
140
|
'IMPORTANT: At the START of any FormLM session (before calling formlm_generate/formlm_exec),',
|
|
117
|
-
'call auth_status first. If not logged in,
|
|
118
|
-
'(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.',
|
|
119
142
|
].join('\n'), {
|
|
120
|
-
token: z.string().optional().describe('
|
|
121
|
-
email: z.string().optional().describe('Account email (use
|
|
122
|
-
|
|
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)'),
|
|
123
147
|
}, async (params) => {
|
|
124
148
|
if (params.token) {
|
|
125
149
|
addProfile({ name: 'default', url: getBaseUrl(), token: params.token, active: true });
|
|
@@ -130,6 +154,21 @@ export async function startMcpServer() {
|
|
|
130
154
|
}
|
|
131
155
|
return { content: [{ type: 'text', text: `⚠️ Token saved but verification failed: ${result.message}` }] };
|
|
132
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
|
+
}
|
|
133
172
|
if (params.email && params.password) {
|
|
134
173
|
const result = await authLogin(params.email, params.password);
|
|
135
174
|
if (result.code === 0 && result.data) {
|
|
@@ -137,21 +176,70 @@ export async function startMcpServer() {
|
|
|
137
176
|
addProfile({ name: 'default', url: getBaseUrl(), token, active: true });
|
|
138
177
|
return { content: [{ type: 'text', text: '✅ Login successful!' }] };
|
|
139
178
|
}
|
|
140
|
-
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.` }] };
|
|
141
180
|
}
|
|
142
|
-
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.' }] };
|
|
143
182
|
});
|
|
144
|
-
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 () => {
|
|
145
233
|
const profile = getActiveProfile();
|
|
146
234
|
if (!profile) {
|
|
147
|
-
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).' }] };
|
|
148
236
|
}
|
|
149
237
|
const result = await authMe();
|
|
150
238
|
if (result.code === 0) {
|
|
151
239
|
const userInfo = typeof result.data === 'object' ? result.data.userName : result.data;
|
|
152
240
|
return { content: [{ type: 'text', text: `✅ Logged in. Server: ${profile.url}\nUser: ${userInfo}` }] };
|
|
153
241
|
}
|
|
154
|
-
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.` }] };
|
|
155
243
|
});
|
|
156
244
|
// ── Tier 1: Smart Pipeline (Natural Language → Plan + Sequential Execute) ─
|
|
157
245
|
//
|
|
@@ -161,6 +249,8 @@ export async function startMcpServer() {
|
|
|
161
249
|
'Generate an execution plan from natural language description (Phase 1 only — does NOT execute any module).',
|
|
162
250
|
'Server runs: Plan AI (assess-plan.md) → generates task list for 6 modules (form/scale/connect/report/expert/share).',
|
|
163
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).',
|
|
164
254
|
'',
|
|
165
255
|
'## Scene Templates (when user description is vague, present these and ask them to choose):',
|
|
166
256
|
'1. assessment: 评估量表 — 多维度打分 + 分值区间解读报告 (MOST COMMON, for psych/workplace/health)',
|
|
@@ -171,8 +261,14 @@ export async function startMcpServer() {
|
|
|
171
261
|
'6. learn: 学习卡片 — 知识点 + 自测题 (for micro-learning)',
|
|
172
262
|
'',
|
|
173
263
|
'## AFTER SUCCESS — execute modules sequentially via formlm_execute:',
|
|
174
|
-
'The plan is cached server-side — just call formlm_execute with appId and module for each module.',
|
|
264
|
+
'The plan is cached server-side (~10 min TTL) — just call formlm_execute with appId and module for each module.',
|
|
175
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.',
|
|
176
272
|
'',
|
|
177
273
|
'## USER FEEDBACK (IMPORTANT for good UX):',
|
|
178
274
|
'After getting the plan, tell the user: "✅ 计划已生成!" and list the modules.',
|
|
@@ -180,7 +276,7 @@ export async function startMcpServer() {
|
|
|
180
276
|
'After each call, tell the user the module is done.',
|
|
181
277
|
'After the last module (share), the 3 app URLs are returned automatically — present them to the user.',
|
|
182
278
|
'',
|
|
183
|
-
'After all modules are executed, use
|
|
279
|
+
'After all modules are executed, use formlm_doctor to verify quality (and formlm_snapshot for raw state).',
|
|
184
280
|
'',
|
|
185
281
|
'## Reference Documents:',
|
|
186
282
|
'If the user has reference documents (questionnaire files, scoring criteria), ask them to paste the content',
|
|
@@ -202,9 +298,21 @@ export async function startMcpServer() {
|
|
|
202
298
|
'"简洁直接" (minimal clean, for general use), ' +
|
|
203
299
|
'"轻松活泼" (lively playful, for exam/learn scenarios). ' +
|
|
204
300
|
'Or custom: "深色科技风" / "warm friendly pastel" / "minimal clean white".'),
|
|
205
|
-
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), ' +
|
|
206
303
|
'"15-20" (standard assessment, 5-8 min), ' +
|
|
207
|
-
'"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).'),
|
|
208
316
|
}, async (params) => {
|
|
209
317
|
let cmd = `assess smart plan --input "${escapeArg(params.input)}"`;
|
|
210
318
|
if (params.planType)
|
|
@@ -213,6 +321,12 @@ export async function startMcpServer() {
|
|
|
213
321
|
cmd += ` --style "${escapeArg(params.style)}"`;
|
|
214
322
|
if (params.questionCount)
|
|
215
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}`;
|
|
216
330
|
cmd += ' --json';
|
|
217
331
|
const r = await execCommand(cmd, undefined, TIMEOUT_PLAN);
|
|
218
332
|
const text = formatGenerateResult(r);
|
|
@@ -235,16 +349,30 @@ export async function startMcpServer() {
|
|
|
235
349
|
'After the last module (share), present the 3 URLs to the user.',
|
|
236
350
|
'',
|
|
237
351
|
'## PLAN PASSING:',
|
|
238
|
-
'Plan is cached server-side after formlm_generate — no need to pass it manually.',
|
|
352
|
+
'Plan is cached server-side (~10 min) after formlm_generate — no need to pass it manually.',
|
|
239
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>").',
|
|
240
359
|
].join('\n'), {
|
|
241
360
|
appId: z.string().describe('App ID from formlm_generate result'),
|
|
242
361
|
module: z.string().describe('Module to execute: form / scale / connect / report / expert / share'),
|
|
243
|
-
plan: z.string().optional().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)'),
|
|
244
363
|
}, async (params) => {
|
|
245
364
|
let cmd = `assess smart execute --app ${params.appId} --module ${params.module}`;
|
|
246
|
-
if (params.plan)
|
|
247
|
-
|
|
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
|
+
}
|
|
248
376
|
cmd += ' --json';
|
|
249
377
|
const r = await execCommand(cmd, undefined, TIMEOUT_EXECUTE);
|
|
250
378
|
if (r.code !== 0) {
|
|
@@ -272,8 +400,14 @@ export async function startMcpServer() {
|
|
|
272
400
|
const urls = JSON.parse(urlResult.data || urlResult.message);
|
|
273
401
|
lines.push('');
|
|
274
402
|
lines.push('🎉 All modules complete! Your app is ready:');
|
|
275
|
-
if (urls.shareUrl)
|
|
276
|
-
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!' : ''}`);
|
|
277
411
|
if (urls.builderUrl)
|
|
278
412
|
lines.push(`🎨 Editor URL: ${urls.builderUrl}`);
|
|
279
413
|
if (urls.dataUrl)
|
|
@@ -311,9 +445,16 @@ export async function startMcpServer() {
|
|
|
311
445
|
'',
|
|
312
446
|
'Call this BEFORE making changes to understand what already exists.',
|
|
313
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".',
|
|
314
454
|
].join('\n'), {
|
|
315
455
|
appId: z.string().describe('App ID'),
|
|
316
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)'),
|
|
317
458
|
}, async (params) => {
|
|
318
459
|
const validModules = ['form', 'scale', 'connect', 'report', 'expert', 'share'];
|
|
319
460
|
const modules = params.module ? [params.module] : validModules;
|
|
@@ -330,28 +471,98 @@ export async function startMcpServer() {
|
|
|
330
471
|
share: `assess share query --app ${params.appId} --json`,
|
|
331
472
|
};
|
|
332
473
|
const entries = modules.filter((m) => commands[m]);
|
|
333
|
-
|
|
334
|
-
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 };
|
|
335
487
|
const errors = [];
|
|
488
|
+
const parsedMods = {};
|
|
336
489
|
for (const [module, result] of results) {
|
|
337
490
|
if (result.code === 0) {
|
|
338
491
|
// All modules now use --json, so parse uniformly
|
|
339
492
|
try {
|
|
340
493
|
snapshot[module] = JSON.parse(result.data);
|
|
494
|
+
parsedMods[module] = snapshot[module];
|
|
341
495
|
}
|
|
342
496
|
catch {
|
|
343
497
|
snapshot[module] = result.data || result.message;
|
|
498
|
+
parsedMods[module] = null;
|
|
344
499
|
}
|
|
345
500
|
}
|
|
346
501
|
else {
|
|
502
|
+
// Partial-failure visibility so auditors never read a fetch error as "module is empty"
|
|
347
503
|
snapshot[module] = null;
|
|
348
|
-
|
|
504
|
+
snapshot['_degraded'] = true;
|
|
505
|
+
errors.push(`${module}: [${result.code}] ${result.message}`);
|
|
349
506
|
}
|
|
350
507
|
}
|
|
351
508
|
if (errors.length > 0)
|
|
352
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
|
+
}
|
|
353
523
|
return { content: [{ type: 'text', text: JSON.stringify(snapshot, null, 2) }] };
|
|
354
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
|
+
});
|
|
355
566
|
server.tool('formlm_skill', [
|
|
356
567
|
'Get the full SKILL.md domain knowledge for a specific skill module.',
|
|
357
568
|
'Same documents that AssessAgent loads internally — contains P0/P1/P2 constraints, parameter rules, examples.',
|
|
@@ -370,16 +581,19 @@ export async function startMcpServer() {
|
|
|
370
581
|
});
|
|
371
582
|
// ── Tier 3: Direct Execution (Advanced) ──────────────────────────
|
|
372
583
|
//
|
|
373
|
-
// Whitelisted
|
|
374
|
-
// 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
|
|
375
586
|
// assess form : query / find / types / config / add / update / remove / move / set-property
|
|
376
587
|
// assess scale : query / find / add / update / set / remove / clear / config / keys / data
|
|
377
588
|
// assess connect: query / find / types / config / cover-page / final-page / main-page / style
|
|
378
|
-
// assess report : query / find / update / page / widget
|
|
589
|
+
// assess report : query / find / update / page / widget (widget carries the `logic` sub-command)
|
|
379
590
|
// assess expert : query / find / config / set / avatar / remove / chat
|
|
380
|
-
// assess share : set / query / url
|
|
591
|
+
// assess share : set / query / url / api / flavor
|
|
381
592
|
// assess smart : plan / execute
|
|
382
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.
|
|
383
597
|
server.tool('formlm_exec', [
|
|
384
598
|
'Execute a raw FormLM CLI command directly.',
|
|
385
599
|
'Use this for fine-grained control that formlm_generate doesn\'t cover, or for modifying existing apps.',
|
|
@@ -393,26 +607,32 @@ export async function startMcpServer() {
|
|
|
393
607
|
' NEVER call a remove command based on a vague reference ("delete it", "remove that one")',
|
|
394
608
|
' without first resolving and confirming the exact target. Deletion is irreversible.',
|
|
395
609
|
'',
|
|
396
|
-
'Whitelisted
|
|
397
|
-
' 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',
|
|
398
612
|
' Form: assess form query/find/types/config/add/update/remove/move/set-property',
|
|
399
613
|
' Scale: assess scale query/find/add/update/set/remove/clear/config/keys/data',
|
|
400
614
|
' Connect: assess connect query/find/types/config/cover-page/final-page/main-page/style',
|
|
401
|
-
' 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)',
|
|
402
616
|
' Expert: assess expert query/find/config/set/avatar/remove/chat',
|
|
403
|
-
' Share: assess share set/query/url',
|
|
617
|
+
' Share: assess share set/query/url/api/flavor',
|
|
404
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)',
|
|
405
621
|
'',
|
|
406
622
|
'Common patterns:',
|
|
407
|
-
' "assess app list --json"
|
|
408
|
-
' "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)',
|
|
409
625
|
' "assess form add --app <id> --id q1 --name \\"Name\\" --type radio --options \\"A:1,B:2,C:3\\" --json"',
|
|
410
626
|
' "assess scale add --app <id> --id stress --name \\"Stress\\" --format sum --kbText \\"...\\" --json"',
|
|
411
627
|
' "assess scale keys add --app <id> --scale stress --fields X1,X2,X3 --json"',
|
|
412
|
-
' "assess scale data add --app <id> --scale stress --
|
|
413
|
-
' "assess
|
|
414
|
-
' "assess share
|
|
415
|
-
' "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)',
|
|
416
636
|
' "assess app remove --app <id> --json" (IRREVERSIBLE — confirm first, see SAFETY above)',
|
|
417
637
|
].join('\n'), {
|
|
418
638
|
command: z.string().describe('Full CLI command starting with "assess". Do NOT include the "formlm-cli" prefix. ' +
|
|
@@ -442,6 +662,6 @@ export async function startMcpServer() {
|
|
|
442
662
|
// ── Start Server ──────────────────────────────────────────────────
|
|
443
663
|
const transport = new StdioServerTransport();
|
|
444
664
|
await server.connect(transport);
|
|
445
|
-
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)`);
|
|
446
666
|
}
|
|
447
667
|
//# sourceMappingURL=mcp.js.map
|