@agentic-survey/mcp-server 0.1.2 → 0.1.3

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 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
- 'yes_no → {}. Give each choice option a stable `id`.',
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). ' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentic-survey/mcp-server",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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>",