@formlm/cli 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +231 -77
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +7 -4
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/connect.d.ts +3 -0
- package/dist/commands/connect.d.ts.map +1 -0
- package/dist/commands/connect.js +555 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/expert.d.ts +3 -0
- package/dist/commands/expert.d.ts.map +1 -0
- package/dist/commands/expert.js +123 -0
- package/dist/commands/expert.js.map +1 -0
- package/dist/commands/field.d.ts.map +1 -1
- package/dist/commands/field.js +38 -21
- package/dist/commands/field.js.map +1 -1
- package/dist/commands/profile.d.ts.map +1 -1
- package/dist/commands/profile.js +4 -3
- package/dist/commands/profile.js.map +1 -1
- package/dist/commands/report.d.ts +3 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/report.js +507 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/scale.d.ts +3 -0
- package/dist/commands/scale.d.ts.map +1 -0
- package/dist/commands/scale.js +289 -0
- package/dist/commands/scale.js.map +1 -0
- package/dist/commands/share.js +2 -2
- package/dist/commands/share.js.map +1 -1
- package/dist/commands/skill.d.ts +15 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +31 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/smart.d.ts +16 -0
- package/dist/commands/smart.d.ts.map +1 -0
- package/dist/commands/smart.js +81 -0
- package/dist/commands/smart.js.map +1 -0
- package/dist/commands/snapshot.d.ts +13 -0
- package/dist/commands/snapshot.d.ts.map +1 -0
- package/dist/commands/snapshot.js +61 -0
- package/dist/commands/snapshot.js.map +1 -0
- package/dist/exec.d.ts +8 -2
- package/dist/exec.d.ts.map +1 -1
- package/dist/exec.js +28 -2
- package/dist/exec.js.map +1 -1
- package/dist/index.js +23 -8
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +444 -269
- package/dist/mcp.js.map +1 -1
- package/package.json +13 -5
package/dist/mcp.js
CHANGED
|
@@ -2,41 +2,214 @@ 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
4
|
import { execCommand, authLogin, authMe } from './exec.js';
|
|
5
|
-
import { addProfile, getActiveProfile } from './config.js';
|
|
5
|
+
import { addProfile, getActiveProfile, getBaseUrl } from './config.js';
|
|
6
|
+
// ── Helper: escape user input for embedding in CLI command strings ──────────
|
|
7
|
+
// tokenizeCommand (server-side PipelineFilterCommand) supports \" as escaped
|
|
8
|
+
// double-quote inside a quoted arg. Backslashes must also be escaped first.
|
|
9
|
+
// Newlines / tabs are normalized to spaces since CLI commands must be single-line.
|
|
10
|
+
function escapeArg(s) {
|
|
11
|
+
return s
|
|
12
|
+
.replace(/\\/g, '\\\\') // \ → \\ (must be first)
|
|
13
|
+
.replace(/"/g, '\\"') // " → \"
|
|
14
|
+
.replace(/\r?\n|\r/g, ' ') // newlines → space
|
|
15
|
+
.replace(/\t/g, ' ') // tabs → space
|
|
16
|
+
.trim();
|
|
17
|
+
}
|
|
18
|
+
// ── Helper: format exec result as MCP text content ──────────────────────────
|
|
19
|
+
function toText(r) {
|
|
20
|
+
return r.code === 0 ? (r.data || r.message) : `❌ [${r.code}] ${r.message}`;
|
|
21
|
+
}
|
|
22
|
+
// ── Helper: format formlm_create result for better AI consumption ──────────────
|
|
23
|
+
// Extracts key fields (appId, shareUrl, planType, taskCount) from JSON result
|
|
24
|
+
// and presents them in a structured summary for the AI Agent to relay to the user.
|
|
25
|
+
// The full raw JSON is intentionally NOT dumped here (it can be very long for
|
|
26
|
+
// multi-task pipelines) — use formlm_snapshot if full detail is needed.
|
|
27
|
+
function formatCreateResult(r) {
|
|
28
|
+
if (r.code !== 0) {
|
|
29
|
+
return `❌ [${r.code}] ${r.message}`;
|
|
30
|
+
}
|
|
31
|
+
const raw = r.data || r.message;
|
|
32
|
+
if (typeof raw !== 'string') {
|
|
33
|
+
return String(raw);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
if (parsed && typeof parsed === 'object' && parsed.appId) {
|
|
38
|
+
const lines = [];
|
|
39
|
+
const status = parsed.status === 'partial_error' ? '⚠️ Partial Success' : '✅ Success';
|
|
40
|
+
lines.push(status);
|
|
41
|
+
if (parsed.appId)
|
|
42
|
+
lines.push(`📋 App ID: ${parsed.appId}`);
|
|
43
|
+
if (parsed.planType)
|
|
44
|
+
lines.push(`🎯 Type: ${parsed.planType}`);
|
|
45
|
+
if (parsed.description)
|
|
46
|
+
lines.push(`📝 Description: ${parsed.description}`);
|
|
47
|
+
if (parsed.taskCount != null)
|
|
48
|
+
lines.push(`📊 Tasks: ${parsed.taskCount}`);
|
|
49
|
+
if (parsed.shareUrl)
|
|
50
|
+
lines.push(`🔗 Fill-in URL: ${parsed.shareUrl}`);
|
|
51
|
+
if (parsed.builderUrl)
|
|
52
|
+
lines.push(`🎨 Editor URL: ${parsed.builderUrl}`);
|
|
53
|
+
if (parsed.dataUrl)
|
|
54
|
+
lines.push(`📊 Data URL: ${parsed.dataUrl}`);
|
|
55
|
+
if (parsed.tasks && Array.isArray(parsed.tasks)) {
|
|
56
|
+
const done = parsed.tasks.filter((t) => t.status === 'done').length;
|
|
57
|
+
const errors = parsed.tasks.filter((t) => t.status === 'error').length;
|
|
58
|
+
if (errors > 0) {
|
|
59
|
+
lines.push(`⚠️ ${done}/${parsed.tasks.length} tasks done, ${errors} errors`);
|
|
60
|
+
if (Array.isArray(parsed.tasks)) {
|
|
61
|
+
const errorTasks = parsed.tasks.filter((t) => t.status === 'error');
|
|
62
|
+
for (const t of errorTasks) {
|
|
63
|
+
lines.push(` ✗ ${t.name || t.id || 'task'}: ${t.error || t.message || 'unknown error'}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
lines.push(`✅ All ${parsed.tasks.length} tasks completed`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
lines.push('');
|
|
72
|
+
lines.push('🔍 Use formlm_snapshot to see full app details (do not dump raw JSON to the user).');
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Not JSON, return as-is
|
|
78
|
+
}
|
|
79
|
+
return raw;
|
|
80
|
+
}
|
|
81
|
+
// ── Helper: format formlm_modify result, surfacing plan-preview payloads ──────
|
|
82
|
+
// When a medium/high risk change is detected server-side, execution pauses and
|
|
83
|
+
// returns { planPreview: true, riskLevel, plan }. This formats that into a
|
|
84
|
+
// human-reviewable task list and tells the AI exactly how to proceed (call
|
|
85
|
+
// formlm_confirm with the SAME plan object) — no web UI needed.
|
|
86
|
+
function formatModifyResult(r) {
|
|
87
|
+
if (r.code !== 0) {
|
|
88
|
+
return `❌ [${r.code}] ${r.message}`;
|
|
89
|
+
}
|
|
90
|
+
const raw = r.data || r.message;
|
|
91
|
+
if (typeof raw !== 'string') {
|
|
92
|
+
return String(raw);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(raw);
|
|
96
|
+
if (parsed && typeof parsed === 'object' && parsed.planPreview === true && parsed.plan) {
|
|
97
|
+
const lines = [];
|
|
98
|
+
lines.push(`⏸️ Plan Preview — Confirmation Required (risk level: ${parsed.riskLevel || parsed.plan.riskLevel || 'unknown'})`);
|
|
99
|
+
lines.push('');
|
|
100
|
+
lines.push('This change was classified as medium/high risk and was NOT executed yet.');
|
|
101
|
+
lines.push('Show this task list to the user and ask for explicit confirmation before proceeding:');
|
|
102
|
+
lines.push('');
|
|
103
|
+
const tasks = Array.isArray(parsed.plan.tasks) ? parsed.plan.tasks : [];
|
|
104
|
+
for (const t of tasks) {
|
|
105
|
+
lines.push(` ${t.seq}. [${t.skill}] ${t.title}`);
|
|
106
|
+
}
|
|
107
|
+
lines.push('');
|
|
108
|
+
lines.push(`If the user confirms, call formlm_confirm with appId="${parsed.appId || ''}" and plan=<the exact JSON below, unmodified>:`);
|
|
109
|
+
lines.push('');
|
|
110
|
+
lines.push(JSON.stringify(parsed.plan));
|
|
111
|
+
return lines.join('\n');
|
|
112
|
+
}
|
|
113
|
+
if (parsed && typeof parsed === 'object' && parsed.status) {
|
|
114
|
+
const lines = [];
|
|
115
|
+
lines.push(parsed.status === 'partial_error' ? '⚠️ Partial Success' : '✅ Success');
|
|
116
|
+
if (parsed.appId)
|
|
117
|
+
lines.push(`📋 App ID: ${parsed.appId}`);
|
|
118
|
+
if (parsed.operationType)
|
|
119
|
+
lines.push(`🔧 Operation: ${parsed.operationType}`);
|
|
120
|
+
if (parsed.output) {
|
|
121
|
+
lines.push('');
|
|
122
|
+
lines.push(parsed.output);
|
|
123
|
+
}
|
|
124
|
+
return lines.join('\n');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Not JSON, return as-is
|
|
129
|
+
}
|
|
130
|
+
return raw;
|
|
131
|
+
}
|
|
132
|
+
// ── MCP timeouts ─────────────────────────────────────────────────────────────
|
|
133
|
+
// Most commands complete in < 5 seconds.
|
|
134
|
+
// Smart pipeline (AssessAgent/BuilderAgent) takes 30-300 seconds.
|
|
135
|
+
const TIMEOUT_DEFAULT = 60_000; // 60s for all direct CLI commands
|
|
136
|
+
const TIMEOUT_SMART = 600_000; // 10min for formlm_create / formlm_modify (consultation can take 5min+)
|
|
6
137
|
export async function startMcpServer() {
|
|
7
138
|
const server = new McpServer({
|
|
8
139
|
name: 'formlm',
|
|
9
|
-
version: '0.
|
|
140
|
+
version: '0.2.0',
|
|
10
141
|
});
|
|
11
|
-
//
|
|
12
|
-
|
|
142
|
+
// ════════════════════════════════════════════════════════════════
|
|
143
|
+
// MCP RESOURCES — Domain Knowledge (SKILL.md × 6)
|
|
144
|
+
// ════════════════════════════════════════════════════════════════
|
|
145
|
+
// AI agents should read these BEFORE generating commands to ensure
|
|
146
|
+
// they follow all P0/P1/P2 domain constraints.
|
|
147
|
+
// Resources are lazy-loaded — call formlm_skill to fetch on demand.
|
|
148
|
+
// ════════════════════════════════════════════════════════════════
|
|
149
|
+
const skillIds = ['form', 'scale', 'connect', 'report', 'expert', 'share'];
|
|
150
|
+
const skillDescriptions = {
|
|
151
|
+
form: 'Form field management — field types, scoring options, P0: --id uses snake_case (e.g. q1_anxiety), --key uses X-prefix fieldKey (e.g. X1). NEVER mix them.',
|
|
152
|
+
scale: 'Scale dimension scoring — P0: range boundaries must be continuous (next.min = prev.max + 1), highest tier max must be 999, --kbText is required.',
|
|
153
|
+
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.',
|
|
154
|
+
report: 'Report pages & widgets — P0: system variables use {{double braces}} e.g. {{TotalScore}}, logic conditions use actual score values (never percentages).',
|
|
155
|
+
expert: 'AI expert agent config — P0: "expert config" for full setup (requires --kbText), "expert set" only for single-property micro-adjustments. --kbText is required.',
|
|
156
|
+
share: 'Share & publish settings — share set command is naturally idempotent (safe to re-run). --form-type all --form-perm 1 --form-day 3650000 for standard public access.',
|
|
157
|
+
};
|
|
158
|
+
for (const skillId of skillIds) {
|
|
159
|
+
server.resource(skillId, `formlm://skills/${skillId}`, {
|
|
160
|
+
description: skillDescriptions[skillId],
|
|
161
|
+
mimeType: 'text/markdown',
|
|
162
|
+
}, async (uri) => {
|
|
163
|
+
const r = await execCommand(`assess skill ${skillId}`, undefined, TIMEOUT_DEFAULT);
|
|
164
|
+
return {
|
|
165
|
+
contents: [{
|
|
166
|
+
uri: uri.href,
|
|
167
|
+
text: r.code === 0 ? (r.data || r.message) : `Error loading skill: ${r.message}`,
|
|
168
|
+
mimeType: 'text/markdown',
|
|
169
|
+
}],
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
// ════════════════════════════════════════════════════════════════
|
|
174
|
+
// MCP TOOLS — Layered Architecture (9 tools, down from 34)
|
|
175
|
+
//
|
|
176
|
+
// Recommended workflow:
|
|
177
|
+
// 1. auth_login → authenticate
|
|
178
|
+
// 2. formlm_create → build a complete new app from scratch, OR
|
|
179
|
+
// formlm_skill (read domain rules) + formlm_exec (direct commands)
|
|
180
|
+
// 3. formlm_snapshot → check current state before any modifications
|
|
181
|
+
// 4. formlm_modify → modify existing app with natural language, OR
|
|
182
|
+
// formlm_exec (direct fine-grained commands)
|
|
183
|
+
// ════════════════════════════════════════════════════════════════
|
|
184
|
+
// ── Tier 0: Authentication ────────────────────────────────────
|
|
185
|
+
server.tool('auth_login', [
|
|
186
|
+
'Login to FormLM with a token or email + password.',
|
|
187
|
+
'',
|
|
188
|
+
'IMPORTANT: At the START of any FormLM session (before calling formlm_create/formlm_plan/formlm_modify/formlm_exec),',
|
|
189
|
+
'call auth_status first. If not logged in, call this tool immediately — ask the user for their email + password',
|
|
190
|
+
'(easiest, no browser needed) or a token. Do NOT wait for a 401 error before authenticating.',
|
|
191
|
+
].join('\n'), {
|
|
13
192
|
token: z.string().optional().describe('Auth token (get it from formlm.me → DevTools → Cookies → Authorization)'),
|
|
14
193
|
email: z.string().optional().describe('Account email (use together with password)'),
|
|
15
194
|
password: z.string().optional().describe('Account password (use together with email)'),
|
|
16
195
|
}, async (params) => {
|
|
17
|
-
// Method A: direct token
|
|
18
196
|
if (params.token) {
|
|
19
|
-
addProfile({ name: 'default', url:
|
|
197
|
+
addProfile({ name: 'default', url: getBaseUrl(), token: params.token, active: true });
|
|
20
198
|
const result = await authMe();
|
|
21
199
|
if (result.code === 0) {
|
|
22
200
|
const userInfo = typeof result.data === 'object' ? result.data.userName : result.data;
|
|
23
201
|
return { content: [{ type: 'text', text: `✅ Login successful! User: ${userInfo}` }] };
|
|
24
202
|
}
|
|
25
|
-
|
|
26
|
-
return { content: [{ type: 'text', text: `⚠️ Token saved but verification failed: ${result.message}` }] };
|
|
27
|
-
}
|
|
203
|
+
return { content: [{ type: 'text', text: `⚠️ Token saved but verification failed: ${result.message}` }] };
|
|
28
204
|
}
|
|
29
|
-
// Method B: email + password
|
|
30
205
|
if (params.email && params.password) {
|
|
31
206
|
const result = await authLogin(params.email, params.password);
|
|
32
207
|
if (result.code === 0 && result.data) {
|
|
33
208
|
const token = typeof result.data === 'string' ? result.data : result.data.token || '';
|
|
34
|
-
addProfile({ name: 'default', url:
|
|
209
|
+
addProfile({ name: 'default', url: getBaseUrl(), token, active: true });
|
|
35
210
|
return { content: [{ type: 'text', text: '✅ Login successful!' }] };
|
|
36
211
|
}
|
|
37
|
-
|
|
38
|
-
return { content: [{ type: 'text', text: `❌ Login failed: ${result.message}` }] };
|
|
39
|
-
}
|
|
212
|
+
return { content: [{ type: 'text', text: `❌ Login failed: ${result.message}` }] };
|
|
40
213
|
}
|
|
41
214
|
return { content: [{ type: 'text', text: '❌ Either token or both email and password are required.' }] };
|
|
42
215
|
});
|
|
@@ -50,280 +223,282 @@ export async function startMcpServer() {
|
|
|
50
223
|
const userInfo = typeof result.data === 'object' ? result.data.userName : result.data;
|
|
51
224
|
return { content: [{ type: 'text', text: `✅ Logged in. Server: ${profile.url}\nUser: ${userInfo}` }] };
|
|
52
225
|
}
|
|
53
|
-
|
|
54
|
-
return { content: [{ type: 'text', text: `❌ Token invalid: ${result.message}` }] };
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
// ========== App ==========
|
|
58
|
-
server.tool('app_list', 'List all apps', {}, async () => {
|
|
59
|
-
const r = await execCommand('assess app list --json');
|
|
60
|
-
return { content: [{ type: 'text', text: r.code === 0 ? (r.data || r.message) : '❌ [' + r.code + '] ' + r.message }] };
|
|
61
|
-
});
|
|
62
|
-
server.tool('app_create', 'Create a new app', {
|
|
63
|
-
name: z.string().describe('App name'),
|
|
64
|
-
description: z.string().optional().describe('App description'),
|
|
65
|
-
}, async (params) => {
|
|
66
|
-
let cmd = `assess app create --name "${params.name}"`;
|
|
67
|
-
if (params.description)
|
|
68
|
-
cmd += ` --description "${params.description}"`;
|
|
69
|
-
cmd += ' --json';
|
|
70
|
-
const r = await execCommand(cmd);
|
|
71
|
-
return { content: [{ type: 'text', text: r.code === 0 ? (r.data || r.message) : '❌ [' + r.code + '] ' + r.message }] };
|
|
72
|
-
});
|
|
73
|
-
server.tool('app_get', 'Get app details', {
|
|
74
|
-
appId: z.string().describe('App ID'),
|
|
75
|
-
}, async (params) => {
|
|
76
|
-
const r = await execCommand(`assess app use --app ${params.appId} --json`);
|
|
77
|
-
return { content: [{ type: 'text', text: r.code === 0 ? (r.data || r.message) : '❌ [' + r.code + '] ' + r.message }] };
|
|
78
|
-
});
|
|
79
|
-
server.tool('app_update', 'Update an app', {
|
|
80
|
-
appId: z.string().describe('App ID'),
|
|
81
|
-
name: z.string().optional().describe('New app name'),
|
|
82
|
-
description: z.string().optional().describe('New description'),
|
|
83
|
-
theme: z.string().optional().describe('App theme'),
|
|
84
|
-
}, async (params) => {
|
|
85
|
-
let cmd = `assess app update --app ${params.appId}`;
|
|
86
|
-
if (params.name)
|
|
87
|
-
cmd += ` --name "${params.name}"`;
|
|
88
|
-
if (params.description)
|
|
89
|
-
cmd += ` --description "${params.description}"`;
|
|
90
|
-
if (params.theme)
|
|
91
|
-
cmd += ` --theme ${params.theme}`;
|
|
92
|
-
cmd += ' --json';
|
|
93
|
-
const r = await execCommand(cmd);
|
|
94
|
-
return { content: [{ type: 'text', text: r.code === 0 ? (r.data || r.message) : '❌ [' + r.code + '] ' + r.message }] };
|
|
95
|
-
});
|
|
96
|
-
server.tool('app_delete', 'Delete an app', {
|
|
97
|
-
appId: z.string().describe('App ID'),
|
|
98
|
-
}, async (params) => {
|
|
99
|
-
const r = await execCommand(`assess app remove --app ${params.appId} --json`);
|
|
100
|
-
return { content: [{ type: 'text', text: r.code === 0 ? (r.data || r.message) : '❌ [' + r.code + '] ' + r.message }] };
|
|
226
|
+
return { content: [{ type: 'text', text: `❌ Token invalid: ${result.message}` }] };
|
|
101
227
|
});
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
228
|
+
// ── Tier 1: Smart Pipeline (Natural Language → Full App) ────────
|
|
229
|
+
//
|
|
230
|
+
// These tools wrap the server-side AssessAgent / BuilderAgent intelligence.
|
|
231
|
+
// They are the RECOMMENDED entry points for AI agents:
|
|
232
|
+
// - Same engine that powers newapp.html (SKILL.md constraints, reference data injection, retry)
|
|
233
|
+
// - One call replaces 30+ sequential formlm_exec calls
|
|
234
|
+
// - Returns appId + full task execution log
|
|
235
|
+
// - WARNING: may take 60-300 seconds for complex apps (consultation: 6 tasks)
|
|
236
|
+
server.tool('formlm_create', [
|
|
237
|
+
'Create a COMPLETE production-ready assessment app from natural language.',
|
|
238
|
+
'Server runs: Plan AI (assess-plan.md) → per-task SKILL.md injection → CLI execution → result collection.',
|
|
239
|
+
'Equivalent to the full newapp.html pipeline. Returns JSON with appId, planType, shareUrl, task statuses, and full execution log.',
|
|
240
|
+
'',
|
|
241
|
+
'## Scene Templates (when user description is vague, present these and ask them to choose):',
|
|
242
|
+
'1. assessment: 评估量表 — 多维度打分 + 分值区间解读报告 (MOST COMMON, for psych/workplace/health)',
|
|
243
|
+
'2. consultation: 咨询评估 — 评估 + AI专家对话解读 (for mental health / coaching)',
|
|
244
|
+
'3. survey: 问卷调查 — 仅收集数据,无打分 (for feedback / research)',
|
|
245
|
+
'4. exam: 考试测验 — 标准答案 + 对错判分 (for education / training)',
|
|
246
|
+
'5. quiz: 趣味测试 — 轻松风格 + 结果分类 (for engagement / personality)',
|
|
247
|
+
'6. learn: 学习卡片 — 知识点 + 自测题 (for micro-learning)',
|
|
248
|
+
'',
|
|
249
|
+
'## Progress Feedback (tell the user BEFORE calling):',
|
|
250
|
+
'Tell the user: "正在生成完整的评估应用,包含表单/量表/报告/发布等步骤,预计需要2-5分钟,请耐心等待..."',
|
|
251
|
+
'Expected duration: assessment 60-120s / consultation 180-300s / survey 30-60s.',
|
|
252
|
+
'',
|
|
253
|
+
'## AFTER SUCCESS:',
|
|
254
|
+
'1. Parse the returned JSON — it contains THREE URLs you must present to the user:',
|
|
255
|
+
' - shareUrl: the fill-in URL for respondents to submit answers',
|
|
256
|
+
' - builderUrl: the visual editor URL for online modification (builder.html)',
|
|
257
|
+
' - dataUrl: the data management URL for viewing collected responses (data.html)',
|
|
258
|
+
'2. Present ALL THREE URLs to the user prominently — they need all three to manage their app.',
|
|
259
|
+
'3. Use formlm_snapshot to verify the generated app if needed.',
|
|
260
|
+
'4. If the user needs the URLs again later, use formlm_exec: "assess app urls --app <appId> --json".',
|
|
261
|
+
'',
|
|
262
|
+
'## ON FAILURE:',
|
|
263
|
+
'- If timeout: ask user to simplify the description (fewer dimensions, fewer questions).',
|
|
264
|
+
'- If partial success: use formlm_snapshot to check what was generated, then formlm_modify to complete.',
|
|
265
|
+
'- Always offer to retry with a simplified description.',
|
|
266
|
+
'',
|
|
267
|
+
'## Reference Documents:',
|
|
268
|
+
'If the user has reference documents (questionnaire files, scoring criteria), ask them to paste the content',
|
|
269
|
+
'directly into the chat. The input supports up to 8000 characters. For longer documents, summarize key points:',
|
|
270
|
+
'dimensions, question count, scoring rules.',
|
|
271
|
+
'',
|
|
272
|
+
'WARNING: Takes 60-300 seconds. Do NOT cancel — let it complete.',
|
|
273
|
+
].join('\n'), {
|
|
274
|
+
input: z.string().describe('Natural language description of the app. Be specific: mention topic, audience, number of questions, dimensions/subscales, scoring, visual style. ' +
|
|
275
|
+
'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". ' +
|
|
276
|
+
'Supports up to 8000 characters. If user pastes reference documents, include them here.'),
|
|
277
|
+
planType: z.string().optional().describe('Plan type. Choose based on user needs: ' +
|
|
278
|
+
'"assessment" (scoring+report, for psych/workplace/health evaluations, MOST COMMON), ' +
|
|
279
|
+
'"consultation" (scoring+report+AI expert chat, for mental health/coaching), ' +
|
|
280
|
+
'"survey" (no scoring, for feedback/research), ' +
|
|
281
|
+
'"exam" (correct-answer scoring, for education/training), ' +
|
|
282
|
+
'"quiz" (fun result categories, for engagement), ' +
|
|
283
|
+
'"learn" (knowledge cards + self-test, for micro-learning). ' +
|
|
284
|
+
'Default: auto-detected by Plan AI based on your description.'),
|
|
285
|
+
style: z.string().optional().describe('Visual style preference. Suggested options: "温暖亲切" (warm friendly, for health/care), ' +
|
|
286
|
+
'"正式专业" (formal professional, for workplace/corporate), ' +
|
|
287
|
+
'"简洁直接" (minimal clean, for general use), ' +
|
|
288
|
+
'"轻松活泼" (lively playful, for quiz/education). ' +
|
|
289
|
+
'Or custom: "深色科技风" / "warm friendly pastel" / "minimal clean white".'),
|
|
290
|
+
questionCount: z.string().optional().describe('Target question count range: "10-15" (quick screening, 3-5 min), ' +
|
|
291
|
+
'"15-20" (standard assessment, 5-8 min), ' +
|
|
292
|
+
'"20-30" (deep assessment, 8-15 min). Default: auto-decided by AI based on planType.'),
|
|
115
293
|
}, async (params) => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
filter: z.string().optional().describe('Keyword filter (alternative to id/key)'),
|
|
124
|
-
}, async (params) => {
|
|
125
|
-
let cmd = `assess form find --app ${params.appId}`;
|
|
126
|
-
if (params.id)
|
|
127
|
-
cmd += ` --id ${params.id}`;
|
|
128
|
-
if (params.key)
|
|
129
|
-
cmd += ` --key ${params.key}`;
|
|
130
|
-
if (params.filter)
|
|
131
|
-
cmd += ` --filter "${params.filter}"`;
|
|
294
|
+
let cmd = `assess smart create --input "${escapeArg(params.input)}"`;
|
|
295
|
+
if (params.planType)
|
|
296
|
+
cmd += ` --plan-type ${params.planType}`;
|
|
297
|
+
if (params.style)
|
|
298
|
+
cmd += ` --style "${escapeArg(params.style)}"`;
|
|
299
|
+
if (params.questionCount)
|
|
300
|
+
cmd += ` --question-count ${params.questionCount}`;
|
|
132
301
|
cmd += ' --json';
|
|
133
|
-
const r = await execCommand(cmd);
|
|
134
|
-
|
|
302
|
+
const r = await execCommand(cmd, undefined, TIMEOUT_SMART);
|
|
303
|
+
const text = formatCreateResult(r);
|
|
304
|
+
return { content: [{ type: 'text', text }] };
|
|
135
305
|
});
|
|
136
|
-
server.tool('
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
content: z.string().optional().describe('Field content / description'),
|
|
150
|
-
answer: z.string().optional().describe('Correct answer'),
|
|
151
|
-
explanation: z.string().optional().describe('Answer explanation'),
|
|
152
|
-
format: z.string().optional().describe('Field format'),
|
|
153
|
-
unique: z.boolean().optional().describe('Unique value validation'),
|
|
154
|
-
shareable: z.boolean().optional().describe('Visible on share page'),
|
|
306
|
+
server.tool('formlm_plan', [
|
|
307
|
+
'Preview the execution plan WITHOUT executing it.',
|
|
308
|
+
'Server runs: Plan AI (assess-plan.md) only — returns the task list for user review.',
|
|
309
|
+
'Equivalent to newapp.html review stage. Use this BEFORE formlm_create when the user wants to review the plan first.',
|
|
310
|
+
'',
|
|
311
|
+
'WORKFLOW: formlm_plan → user reviews tasks → formlm_create (with same input to execute)',
|
|
312
|
+
'',
|
|
313
|
+
'Takes 10-30 seconds (only the Plan phase, no execution).',
|
|
314
|
+
].join('\n'), {
|
|
315
|
+
input: z.string().describe('Same natural language description you would pass to formlm_create'),
|
|
316
|
+
planType: z.string().optional().describe('Same as formlm_create planType parameter'),
|
|
317
|
+
style: z.string().optional().describe('Same as formlm_create style parameter'),
|
|
318
|
+
questionCount: z.string().optional().describe('Same as formlm_create questionCount parameter'),
|
|
155
319
|
}, async (params) => {
|
|
156
|
-
let cmd = `assess
|
|
157
|
-
if (params.
|
|
158
|
-
cmd += ` --
|
|
159
|
-
if (params.
|
|
160
|
-
cmd += ` --
|
|
161
|
-
if (params.
|
|
162
|
-
cmd += ` --
|
|
163
|
-
if (params.required)
|
|
164
|
-
cmd += ' --required';
|
|
165
|
-
if (params.score !== undefined)
|
|
166
|
-
cmd += ` --score ${params.score}`;
|
|
167
|
-
if (params.min !== undefined)
|
|
168
|
-
cmd += ` --min ${params.min}`;
|
|
169
|
-
if (params.max !== undefined)
|
|
170
|
-
cmd += ` --max ${params.max}`;
|
|
171
|
-
if (params.placeholder)
|
|
172
|
-
cmd += ` --placeholder "${params.placeholder}"`;
|
|
173
|
-
if (params.inputMask)
|
|
174
|
-
cmd += ` --inputMask ${params.inputMask}`;
|
|
175
|
-
if (params.content)
|
|
176
|
-
cmd += ` --content "${params.content}"`;
|
|
177
|
-
if (params.answer)
|
|
178
|
-
cmd += ` --answer "${params.answer}"`;
|
|
179
|
-
if (params.explanation)
|
|
180
|
-
cmd += ` --explanation "${params.explanation}"`;
|
|
181
|
-
if (params.format)
|
|
182
|
-
cmd += ` --format ${params.format}`;
|
|
183
|
-
if (params.unique)
|
|
184
|
-
cmd += ' --unique';
|
|
185
|
-
if (params.shareable)
|
|
186
|
-
cmd += ' --shareable';
|
|
320
|
+
let cmd = `assess smart plan --input "${escapeArg(params.input)}"`;
|
|
321
|
+
if (params.planType)
|
|
322
|
+
cmd += ` --plan-type ${params.planType}`;
|
|
323
|
+
if (params.style)
|
|
324
|
+
cmd += ` --style "${escapeArg(params.style)}"`;
|
|
325
|
+
if (params.questionCount)
|
|
326
|
+
cmd += ` --question-count ${params.questionCount}`;
|
|
187
327
|
cmd += ' --json';
|
|
188
|
-
const r = await execCommand(cmd);
|
|
189
|
-
return { content: [{ type: 'text', text:
|
|
328
|
+
const r = await execCommand(cmd, undefined, TIMEOUT_DEFAULT);
|
|
329
|
+
return { content: [{ type: 'text', text: toText(r) }] };
|
|
190
330
|
});
|
|
191
|
-
server.tool('
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
331
|
+
server.tool('formlm_modify', [
|
|
332
|
+
'Modify an EXISTING app using natural language.',
|
|
333
|
+
'Server runs: Think (query current state → analyze intent) → Plan (PATCH task list with risk assessment) → Execute (SKILL.md constraints + reference injection) → Reflect (validate results).',
|
|
334
|
+
'Equivalent to newapp.html\'s modification chat. Returns JSON with operationType, task statuses, and AI reflection summary.',
|
|
335
|
+
'',
|
|
336
|
+
'BEFORE calling: use formlm_snapshot to understand current state.',
|
|
337
|
+
'AFTER calling: use formlm_snapshot to verify changes.',
|
|
338
|
+
'',
|
|
339
|
+
'MEDIUM/HIGH risk changes (5+ tasks, or form→scale cascades, or question/dimension deletion) are NOT executed immediately.',
|
|
340
|
+
'Instead the response has planPreview=true and a full "plan" object (task list + riskLevel).',
|
|
341
|
+
'When this happens:',
|
|
342
|
+
' 1. Show the task list and riskLevel to the user in plain language.',
|
|
343
|
+
' 2. Ask for explicit confirmation ("Should I proceed with these N changes?").',
|
|
344
|
+
' 3. If the user confirms, call formlm_confirm with the SAME appId and the exact "plan" object (unmodified) to execute it.',
|
|
345
|
+
' 4. If the user declines or wants changes, do NOT call formlm_confirm — just call formlm_modify again with a revised input.',
|
|
346
|
+
'This entire flow works purely through chat — no web UI is required at any step.',
|
|
347
|
+
'',
|
|
348
|
+
'Common use cases:',
|
|
349
|
+
'- "Add a social support subscale with 5 questions"',
|
|
350
|
+
'- "Change the cover page to dark blue with a professional theme"',
|
|
351
|
+
'- "Update the expert agent to use a warmer communication style"',
|
|
352
|
+
'- "Add a new report page showing dimension comparison"',
|
|
353
|
+
'',
|
|
354
|
+
'WARNING: Takes 30-120 seconds. Do NOT cancel — let it complete.',
|
|
355
|
+
].join('\n'), {
|
|
356
|
+
appId: z.string().describe('App ID to modify (get it from app list or previous formlm_create result)'),
|
|
357
|
+
input: z.string().describe('Natural language description of the change. Be specific about WHAT to change. ' +
|
|
358
|
+
'Examples: "Add a social support subscale with 5 questions" / ' +
|
|
359
|
+
'"Change the cover page to dark blue with a professional theme" / ' +
|
|
360
|
+
'"Update the expert agent to use a warmer communication style"'),
|
|
211
361
|
}, async (params) => {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
cmd += ` --key ${params.key}`;
|
|
217
|
-
if (params.type)
|
|
218
|
-
cmd += ` --type ${params.type}`;
|
|
219
|
-
if (params.title)
|
|
220
|
-
cmd += ` --title "${params.title}"`;
|
|
221
|
-
if (params.name)
|
|
222
|
-
cmd += ` --name "${params.name}"`;
|
|
223
|
-
if (params.options)
|
|
224
|
-
cmd += ` --options "${params.options}"`;
|
|
225
|
-
if (params.required)
|
|
226
|
-
cmd += ' --required';
|
|
227
|
-
if (params.score !== undefined)
|
|
228
|
-
cmd += ` --score ${params.score}`;
|
|
229
|
-
if (params.min !== undefined)
|
|
230
|
-
cmd += ` --min ${params.min}`;
|
|
231
|
-
if (params.max !== undefined)
|
|
232
|
-
cmd += ` --max ${params.max}`;
|
|
233
|
-
if (params.placeholder)
|
|
234
|
-
cmd += ` --placeholder "${params.placeholder}"`;
|
|
235
|
-
if (params.inputMask)
|
|
236
|
-
cmd += ` --inputMask ${params.inputMask}`;
|
|
237
|
-
if (params.content)
|
|
238
|
-
cmd += ` --content "${params.content}"`;
|
|
239
|
-
if (params.answer)
|
|
240
|
-
cmd += ` --answer "${params.answer}"`;
|
|
241
|
-
if (params.explanation)
|
|
242
|
-
cmd += ` --explanation "${params.explanation}"`;
|
|
243
|
-
if (params.format)
|
|
244
|
-
cmd += ` --format ${params.format}`;
|
|
245
|
-
if (params.unique)
|
|
246
|
-
cmd += ' --unique';
|
|
247
|
-
if (params.shareable)
|
|
248
|
-
cmd += ' --shareable';
|
|
249
|
-
cmd += ' --json';
|
|
250
|
-
const r = await execCommand(cmd);
|
|
251
|
-
return { content: [{ type: 'text', text: r.code === 0 ? (r.data || r.message) : '❌ [' + r.code + '] ' + r.message }] };
|
|
362
|
+
const cmd = `assess smart modify --app ${params.appId} --input "${escapeArg(params.input)}" --json`;
|
|
363
|
+
const r = await execCommand(cmd, undefined, TIMEOUT_SMART);
|
|
364
|
+
const text = formatModifyResult(r);
|
|
365
|
+
return { content: [{ type: 'text', text }] };
|
|
252
366
|
});
|
|
253
|
-
server.tool('
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return { content: [{ type: 'text', text: r.code === 0 ? (r.data || r.message) : '❌ [' + r.code + '] ' + r.message }] };
|
|
266
|
-
});
|
|
267
|
-
server.tool('field_move', 'Move a field to a specific position', {
|
|
268
|
-
appId: z.string().describe('App ID'),
|
|
269
|
-
id: z.string().optional().describe('Field ID (at least one of id/key is required)'),
|
|
270
|
-
key: z.string().optional().describe('Field Key (alternative to id)'),
|
|
271
|
-
pos: z.number().describe('Target position (1-based)'),
|
|
367
|
+
server.tool('formlm_confirm', [
|
|
368
|
+
'Confirm and execute a MEDIUM/HIGH risk change plan that was previously returned by formlm_modify with planPreview=true.',
|
|
369
|
+
'Server runs: Execute (skip Think/Plan) → Reflect, using the exact plan you pass in.',
|
|
370
|
+
'This lets the user approve risky changes entirely through chat — no web UI needed.',
|
|
371
|
+
'',
|
|
372
|
+
'ONLY call this after the user has seen the task list from formlm_modify\'s planPreview response and explicitly confirmed.',
|
|
373
|
+
'The "plan" argument MUST be the exact JSON object formlm_modify returned (do not edit, reformat, or hand-write it).',
|
|
374
|
+
'',
|
|
375
|
+
'WARNING: Takes 30-120 seconds. Do NOT cancel — let it complete.',
|
|
376
|
+
].join('\n'), {
|
|
377
|
+
appId: z.string().describe('App ID (same as passed to the preceding formlm_modify call)'),
|
|
378
|
+
plan: z.string().describe('The exact "plan" JSON object returned by formlm_modify, passed through unmodified as a JSON string'),
|
|
272
379
|
}, async (params) => {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
cmd += ` --key ${params.key}`;
|
|
278
|
-
cmd += ` --pos ${params.pos}`;
|
|
279
|
-
const r = await execCommand(cmd);
|
|
280
|
-
return { content: [{ type: 'text', text: r.code === 0 ? (r.data || r.message) : '❌ [' + r.code + '] ' + r.message }] };
|
|
380
|
+
const cmd = `assess smart confirm --app ${params.appId} --plan-json "${escapeArg(params.plan)}" --json`;
|
|
381
|
+
const r = await execCommand(cmd, undefined, TIMEOUT_SMART);
|
|
382
|
+
const text = formatModifyResult(r);
|
|
383
|
+
return { content: [{ type: 'text', text }] };
|
|
281
384
|
});
|
|
282
|
-
|
|
385
|
+
// ── Tier 2: State & Knowledge ────────────────────────────────────
|
|
386
|
+
server.tool('formlm_snapshot', [
|
|
387
|
+
'Get aggregated snapshot of ALL 6 app modules in a single call.',
|
|
388
|
+
'Returns: form fields, scale dimensions+ranges, connect page styles, report pages+widgets, expert config, share status.',
|
|
389
|
+
'',
|
|
390
|
+
'Call this BEFORE formlm_modify to understand what already exists.',
|
|
391
|
+
'Call this AFTER formlm_create to verify the generated app.',
|
|
392
|
+
].join('\n'), {
|
|
283
393
|
appId: z.string().describe('App ID'),
|
|
284
|
-
|
|
285
|
-
key: z.string().optional().describe('Field Key (alternative to id)'),
|
|
286
|
-
property: z.string().describe('Property path (e.g. options.1.score)'),
|
|
287
|
-
value: z.string().describe('Property value'),
|
|
394
|
+
module: z.string().optional().describe('Get only one module: form / scale / connect / report / expert / share (default: all 6)'),
|
|
288
395
|
}, async (params) => {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
appId:
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
396
|
+
const modules = params.module ? [params.module] : ['form', 'scale', 'connect', 'report', 'expert', 'share'];
|
|
397
|
+
const commands = {
|
|
398
|
+
form: `assess form query --app ${params.appId} --json`,
|
|
399
|
+
scale: `assess scale query --app ${params.appId} --md`,
|
|
400
|
+
connect: `assess connect query --app ${params.appId} --md`,
|
|
401
|
+
report: `assess report query --app ${params.appId} --md`,
|
|
402
|
+
expert: `assess expert query --app ${params.appId} --md`,
|
|
403
|
+
share: `assess share query --app ${params.appId} --json`,
|
|
404
|
+
};
|
|
405
|
+
const entries = modules.filter((m) => commands[m]);
|
|
406
|
+
const results = await Promise.all(entries.map((m) => execCommand(commands[m], undefined, TIMEOUT_DEFAULT).then((r) => [m, r])));
|
|
407
|
+
const snapshot = { appId: params.appId };
|
|
408
|
+
const errors = [];
|
|
409
|
+
for (const [module, result] of results) {
|
|
410
|
+
if (result.code === 0) {
|
|
411
|
+
if (module === 'form' || module === 'share') {
|
|
412
|
+
try {
|
|
413
|
+
snapshot[module] = JSON.parse(result.data);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
snapshot[module] = result.data || result.message;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
snapshot[module] = result.data || result.message;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
snapshot[module] = null;
|
|
425
|
+
errors.push(`${module}: ${result.message}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (errors.length > 0)
|
|
429
|
+
snapshot['_errors'] = errors;
|
|
430
|
+
return { content: [{ type: 'text', text: JSON.stringify(snapshot, null, 2) }] };
|
|
310
431
|
});
|
|
311
|
-
server.tool('
|
|
312
|
-
|
|
432
|
+
server.tool('formlm_skill', [
|
|
433
|
+
'Get the full SKILL.md domain knowledge for a specific skill module.',
|
|
434
|
+
'Same documents that AssessAgent/BuilderAgent loads internally — contains P0/P1/P2 constraints, parameter rules, examples.',
|
|
435
|
+
'',
|
|
436
|
+
'You can also read these via MCP resources: formlm://skills/<skillId>',
|
|
437
|
+
'READ the relevant skill BEFORE constructing formlm_exec commands manually.',
|
|
438
|
+
].join('\n'), {
|
|
439
|
+
skillId: z.string().describe('Skill ID: form / scale / connect / report / expert / share'),
|
|
313
440
|
}, async (params) => {
|
|
314
|
-
const
|
|
315
|
-
|
|
441
|
+
const valid = ['form', 'scale', 'connect', 'report', 'expert', 'share'];
|
|
442
|
+
if (!valid.includes(params.skillId)) {
|
|
443
|
+
return { content: [{ type: 'text', text: `❌ Invalid skill ID. Valid: ${valid.join(', ')}` }] };
|
|
444
|
+
}
|
|
445
|
+
const r = await execCommand(`assess skill ${params.skillId}`, undefined, TIMEOUT_DEFAULT);
|
|
446
|
+
return { content: [{ type: 'text', text: toText(r) }] };
|
|
316
447
|
});
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
448
|
+
// ── Tier 3: Direct Execution (Advanced) ──────────────────────────
|
|
449
|
+
//
|
|
450
|
+
// Whitelisted command prefixes (first 3 tokens are checked):
|
|
451
|
+
// assess app : list / create / use / update / remove
|
|
452
|
+
// assess form : query / find / types / config / add / update / remove / move / set-property
|
|
453
|
+
// assess scale : query / find / add / update / set / remove / clear / config / keys / data
|
|
454
|
+
// assess connect: query / find / types / config / cover-page / final-page / main-page / style
|
|
455
|
+
// assess report : query / find / update / page / widget / logic
|
|
456
|
+
// assess expert : query / find / config / set / avatar / remove / chat
|
|
457
|
+
// assess share : set / query / url
|
|
458
|
+
// assess smart : create / modify / plan / confirm
|
|
459
|
+
// assess skill : form / scale / connect / report / expert / share
|
|
460
|
+
server.tool('formlm_exec', [
|
|
461
|
+
'Execute a raw FormLM CLI command directly.',
|
|
462
|
+
'Use this for fine-grained control that formlm_create/formlm_modify don\'t cover.',
|
|
463
|
+
'',
|
|
464
|
+
'IMPORTANT: All commands must start with "assess". Read formlm_skill first to ensure P0 compliance.',
|
|
465
|
+
'',
|
|
466
|
+
'SAFETY — destructive operations (remove/delete):',
|
|
467
|
+
' Before running ANY "remove" command (app/form/scale/expert), first run the matching',
|
|
468
|
+
' "query"/"list"/"find" command to show the user the EXACT name of what will be deleted,',
|
|
469
|
+
' then get explicit confirmation ("Delete app \'X\' (id: Y)? This cannot be undone.").',
|
|
470
|
+
' NEVER call a remove command based on a vague reference ("delete it", "remove that one")',
|
|
471
|
+
' without first resolving and confirming the exact target. Deletion is irreversible.',
|
|
472
|
+
'',
|
|
473
|
+
'Whitelisted prefixes (3-token match):',
|
|
474
|
+
' App: assess app list/create/use/update/remove/urls',
|
|
475
|
+
' Form: assess form query/find/types/config/add/update/remove/move/set-property',
|
|
476
|
+
' Scale: assess scale query/find/add/update/set/remove/clear/config/keys/data',
|
|
477
|
+
' Connect: assess connect query/find/types/config/cover-page/final-page/main-page/style',
|
|
478
|
+
' Report: assess report query/find/update/page/widget/logic',
|
|
479
|
+
' Expert: assess expert query/find/config/set/avatar/remove/chat',
|
|
480
|
+
' Share: assess share set/query/url',
|
|
481
|
+
'',
|
|
482
|
+
'Common patterns:',
|
|
483
|
+
' "assess app list --json" (discover appId by name)',
|
|
484
|
+
' "assess app urls --app <id> --json" (get fill-in/editor/data URLs)',
|
|
485
|
+
' "assess form add --app <id> --id q1 --name \\"Name\\" --type radio --options \\"A:1,B:2,C:3\\" --json"',
|
|
486
|
+
' "assess scale add --app <id> --id stress --name \\"Stress\\" --format sum --kbText \\"...\\" --json"',
|
|
487
|
+
' "assess scale keys add --app <id> --scale stress --fields X1,X2,X3 --json"',
|
|
488
|
+
' "assess scale data add --app <id> --scale stress --ranges \\"0-7:Normal,8-14:Mild,15-21:Severe\\" --json"',
|
|
489
|
+
' "assess share set --app <id> --form-type all --form-perm 1 --form-day 3650000 --json"',
|
|
490
|
+
' "assess share url --app <id>"',
|
|
491
|
+
' "assess app remove --app <id> --json" (IRREVERSIBLE — confirm first, see SAFETY above)',
|
|
492
|
+
].join('\n'), {
|
|
493
|
+
command: z.string().describe('Full CLI command starting with "assess". Do NOT include the "formlm-cli" prefix. ' +
|
|
494
|
+
'Add --json flag for structured output. Escape inner quotes with \\".'),
|
|
320
495
|
}, async (params) => {
|
|
321
|
-
const r = await execCommand(
|
|
322
|
-
return { content: [{ type: 'text', text:
|
|
496
|
+
const r = await execCommand(params.command, undefined, TIMEOUT_DEFAULT);
|
|
497
|
+
return { content: [{ type: 'text', text: toText(r) }] };
|
|
323
498
|
});
|
|
324
|
-
// Start
|
|
499
|
+
// ── Start Server ──────────────────────────────────────────────────
|
|
325
500
|
const transport = new StdioServerTransport();
|
|
326
501
|
await server.connect(transport);
|
|
327
|
-
console.error('FormLM MCP Server running on stdio');
|
|
502
|
+
console.error('FormLM MCP Server v0.2.0 running on stdio (9 tools + 6 resources)');
|
|
328
503
|
}
|
|
329
504
|
//# sourceMappingURL=mcp.js.map
|