@agentic-survey/mcp-server 0.1.2 → 0.1.4
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 +6 -2
- package/dist/cli.js +0 -0
- package/dist/server.js +85 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -63,9 +63,13 @@ Publishing a survey returns a share link carrying your project ref and publishab
|
|
|
63
63
|
|
|
64
64
|
## Tools
|
|
65
65
|
|
|
66
|
-
`setup_connection`, `create_survey`, `add_question`, `update_question`, `remove_question`, `reorder_questions`, `publish_survey`, `get_share_link`, `list_surveys`, `get_survey`, `list_responses`, `get_results`.
|
|
66
|
+
`setup_connection`, `create_survey`, `add_question`, `update_question`, `remove_question`, `reorder_questions`, `set_question_logic`, `validate_survey`, `publish_survey`, `get_share_link`, `list_surveys`, `get_survey`, `list_responses`, `get_results`.
|
|
67
67
|
|
|
68
|
-
Question types: `single_choice`, `multi_choice`, `rating`, `yes_no`, `number`, `short_text`, `long_text
|
|
68
|
+
Question types: `single_choice`, `multi_choice`, `rating`, `yes_no`, `number`, `short_text`, `long_text`, `date`, `time` (24-hour), `slider` (0–100 percentage by default).
|
|
69
|
+
|
|
70
|
+
### Branching / skip logic
|
|
71
|
+
|
|
72
|
+
`set_question_logic` makes a question conditional: it shows (or hides) based on the answers to **earlier** questions — e.g. only ask the follow-up if someone rated you ≤ 2. The page-service evaluates this live and submission validation respects it (a hidden required question isn't required; answers to hidden questions are rejected). Run `validate_survey` after wiring logic and before `publish_survey` — it catches forward/dangling references, conditions citing options that don't exist (so a question could never appear), and choice questions with no options.
|
|
69
73
|
|
|
70
74
|
## License
|
|
71
75
|
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { createDb, createSurvey, updateSurvey, publishSurvey, closeSurvey, listSurveys, getSurvey, addQuestion, updateQuestion, removeQuestion, reorderQuestions, listResponses, getResults, buildShareUrl, isErr, } from '@agentic-survey/core';
|
|
3
|
+
import { createDb, createSurvey, updateSurvey, publishSurvey, closeSurvey, listSurveys, getSurvey, addQuestion, updateQuestion, removeQuestion, reorderQuestions, listResponses, getResults, validateSurvey, buildShareUrl, isErr, } from '@agentic-survey/core';
|
|
4
4
|
import { readInitialMigrationSql } from '@agentic-survey/schema';
|
|
5
5
|
import { loadConfig, isConnectable, projectRefFromUrl, configPath, DEFAULT_PAGE_ENDPOINT, } from './config.js';
|
|
6
6
|
const QUESTION_TYPE = z.enum([
|
|
@@ -11,8 +11,36 @@ const QUESTION_TYPE = z.enum([
|
|
|
11
11
|
'rating',
|
|
12
12
|
'yes_no',
|
|
13
13
|
'number',
|
|
14
|
+
'date',
|
|
15
|
+
'time',
|
|
16
|
+
'slider',
|
|
14
17
|
]);
|
|
15
18
|
const SURVEY_STATUS = z.enum(['draft', 'published', 'closed']);
|
|
19
|
+
const LOGIC_CONDITION = z.object({
|
|
20
|
+
questionId: z.string().describe('An EARLIER question (lower position) whose answer is tested.'),
|
|
21
|
+
op: z.enum([
|
|
22
|
+
'equals',
|
|
23
|
+
'not_equals',
|
|
24
|
+
'includes',
|
|
25
|
+
'gt',
|
|
26
|
+
'gte',
|
|
27
|
+
'lt',
|
|
28
|
+
'lte',
|
|
29
|
+
'answered',
|
|
30
|
+
'not_answered',
|
|
31
|
+
]),
|
|
32
|
+
value: z
|
|
33
|
+
.union([z.string(), z.number(), z.boolean()])
|
|
34
|
+
.optional()
|
|
35
|
+
.describe('Choice ops → option id (string); rating/number → number; yes_no equals → boolean. Omit for answered/not_answered.'),
|
|
36
|
+
});
|
|
37
|
+
const LOGIC = z
|
|
38
|
+
.object({
|
|
39
|
+
action: z.enum(['show', 'hide']).describe('`show`: visible only when conditions match. `hide`: hidden when they match.'),
|
|
40
|
+
match: z.enum(['all', 'any']).describe('Combine conditions with AND (`all`) or OR (`any`).'),
|
|
41
|
+
conditions: z.array(LOGIC_CONDITION).min(1),
|
|
42
|
+
})
|
|
43
|
+
.describe('Skip-logic rule. Conditions may reference earlier questions only. Validate with validate_survey before publishing.');
|
|
16
44
|
const ok = (summary, data) => ({
|
|
17
45
|
content: [{ type: 'text', text: summary }],
|
|
18
46
|
structuredContent: data,
|
|
@@ -99,16 +127,20 @@ export function buildServer() {
|
|
|
99
127
|
description: 'Append (or insert at `position`) a question. `config` shape depends on `type`: ' +
|
|
100
128
|
'single_choice/multi_choice → { options: [{ id, label }] }; rating → { min, max }; ' +
|
|
101
129
|
'number → { min?, max?, step?, unit? }; short_text/long_text → { placeholder?, maxLength? }; ' +
|
|
102
|
-
'
|
|
130
|
+
'date → { min?, max? } (ISO YYYY-MM-DD); time → { min?, max? } (24-hour HH:MM); ' +
|
|
131
|
+
'slider → { min?, max?, step?, unit? } (defaults 0–100, unit "%"); ' +
|
|
132
|
+
'yes_no → {}. Give each choice option a stable `id`. Optional `logic` makes the question ' +
|
|
133
|
+
'conditional — see set_question_logic.',
|
|
103
134
|
inputSchema: {
|
|
104
135
|
surveyId: z.string(),
|
|
105
136
|
type: QUESTION_TYPE,
|
|
106
137
|
prompt: z.string().min(1),
|
|
107
138
|
required: z.boolean().optional(),
|
|
108
139
|
config: z.record(z.string(), z.unknown()).optional().describe('Per-type config (see description)'),
|
|
140
|
+
logic: LOGIC.optional(),
|
|
109
141
|
position: z.number().int().min(0).optional(),
|
|
110
142
|
},
|
|
111
|
-
}, async ({ surveyId, type, prompt, required, config, position }) => {
|
|
143
|
+
}, async ({ surveyId, type, prompt, required, config, logic, position }) => {
|
|
112
144
|
const c = getConn();
|
|
113
145
|
if (!c.ok)
|
|
114
146
|
return c.res;
|
|
@@ -117,6 +149,7 @@ export function buildServer() {
|
|
|
117
149
|
prompt,
|
|
118
150
|
required,
|
|
119
151
|
config: config,
|
|
152
|
+
logic,
|
|
120
153
|
position,
|
|
121
154
|
});
|
|
122
155
|
if (isErr(r))
|
|
@@ -125,13 +158,15 @@ export function buildServer() {
|
|
|
125
158
|
});
|
|
126
159
|
server.registerTool('update_question', {
|
|
127
160
|
title: 'Update a question',
|
|
128
|
-
description: 'Patch a question (prompt, type, required, config, position). Only provided fields change.'
|
|
161
|
+
description: 'Patch a question (prompt, type, required, config, logic, position). Only provided fields change. ' +
|
|
162
|
+
'Pass `logic: null` to clear an existing conditional rule.',
|
|
129
163
|
inputSchema: {
|
|
130
164
|
questionId: z.string(),
|
|
131
165
|
prompt: z.string().min(1).optional(),
|
|
132
166
|
type: QUESTION_TYPE.optional(),
|
|
133
167
|
required: z.boolean().optional(),
|
|
134
168
|
config: z.record(z.string(), z.unknown()).optional(),
|
|
169
|
+
logic: LOGIC.nullable().optional(),
|
|
135
170
|
position: z.number().int().min(0).optional(),
|
|
136
171
|
},
|
|
137
172
|
}, async ({ questionId, ...patch }) => {
|
|
@@ -170,6 +205,49 @@ export function buildServer() {
|
|
|
170
205
|
return fail(r.error.code, r.error.message);
|
|
171
206
|
return ok('Questions reordered.', { reordered: true });
|
|
172
207
|
});
|
|
208
|
+
server.registerTool('set_question_logic', {
|
|
209
|
+
title: 'Set a question’s branching / skip logic',
|
|
210
|
+
description: 'Make a question conditional. The question appears (action "show") or is hidden (action "hide") ' +
|
|
211
|
+
'when its conditions match. Conditions test the answers to EARLIER questions: ' +
|
|
212
|
+
'{ questionId, op, value }. Ops: equals/not_equals (single_choice option id, yes_no boolean), ' +
|
|
213
|
+
'includes (multi_choice contains an option id), gt/gte/lt/lte (rating/number), answered/not_answered. ' +
|
|
214
|
+
'Combine with match "all" (AND) or "any" (OR). Pass logic null to clear. ' +
|
|
215
|
+
'Always run validate_survey after wiring logic and before publishing.',
|
|
216
|
+
inputSchema: {
|
|
217
|
+
questionId: z.string(),
|
|
218
|
+
logic: LOGIC.nullable().describe('The rule to set, or null to clear.'),
|
|
219
|
+
},
|
|
220
|
+
}, async ({ questionId, logic }) => {
|
|
221
|
+
const c = getConn();
|
|
222
|
+
if (!c.ok)
|
|
223
|
+
return c.res;
|
|
224
|
+
const r = await updateQuestion(c.db, questionId, { logic });
|
|
225
|
+
if (isErr(r))
|
|
226
|
+
return fail(r.error.code, r.error.message);
|
|
227
|
+
return ok(logic ? 'Question logic set.' : 'Question logic cleared.', { question: r.data });
|
|
228
|
+
});
|
|
229
|
+
server.registerTool('validate_survey', {
|
|
230
|
+
title: 'Validate a survey’s structure and branching logic',
|
|
231
|
+
description: 'Static pre-publish check. Reports errors (broken/forward/cyclic logic references, conditions citing ' +
|
|
232
|
+
'option ids that don’t exist so a question can never show, choice questions with no options) and ' +
|
|
233
|
+
'warnings (comparison op on a non-numeric question, logic with no conditions). Returns { ok, issues }; ' +
|
|
234
|
+
'ok is false when there is at least one error. Call this after building/editing logic, before publish_survey.',
|
|
235
|
+
inputSchema: { surveyId: z.string() },
|
|
236
|
+
annotations: { readOnlyHint: true },
|
|
237
|
+
}, async ({ surveyId }) => {
|
|
238
|
+
const c = getConn();
|
|
239
|
+
if (!c.ok)
|
|
240
|
+
return c.res;
|
|
241
|
+
const r = await getSurvey(c.db, surveyId);
|
|
242
|
+
if (isErr(r))
|
|
243
|
+
return fail(r.error.code, r.error.message);
|
|
244
|
+
const issues = validateSurvey(r.data.questions);
|
|
245
|
+
const errorCount = issues.filter((i) => i.level === 'error').length;
|
|
246
|
+
const summary = issues.length
|
|
247
|
+
? `${errorCount} error(s), ${issues.length - errorCount} warning(s).`
|
|
248
|
+
: 'No issues — ready to publish.';
|
|
249
|
+
return ok(summary, { ok: errorCount === 0, issues });
|
|
250
|
+
});
|
|
173
251
|
server.registerTool('publish_survey', {
|
|
174
252
|
title: 'Publish a survey',
|
|
175
253
|
description: 'Publish a draft survey and return its public share link (if a publishable key is configured). ' +
|
|
@@ -208,7 +286,9 @@ export function buildServer() {
|
|
|
208
286
|
const link = linkConfig(c.cfg);
|
|
209
287
|
if (!link)
|
|
210
288
|
return fail('no_publishable_key', 'No publishable key configured; re-run `init`.');
|
|
211
|
-
return ok('Share link ready.', {
|
|
289
|
+
return ok('Share link ready.', {
|
|
290
|
+
shareUrl: buildShareUrl(link, surveyId, r.data.survey.title),
|
|
291
|
+
});
|
|
212
292
|
});
|
|
213
293
|
server.registerTool('list_surveys', {
|
|
214
294
|
title: 'List surveys',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentic-survey/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "MCP server (stdio) exposing survey tools over core, plus the agentic-survey CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Monty Daley <monty@daley.org.nz>",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"prepublishOnly": "tsc -b"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@agentic-survey/core": "^0.1.
|
|
53
|
+
"@agentic-survey/core": "^0.1.2",
|
|
54
54
|
"@agentic-survey/schema": "^0.1.0",
|
|
55
55
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
56
56
|
"zod": "^3.23.0"
|