@agentic-survey/mcp-server 0.1.1 → 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 +10 -2
- package/dist/cli.js +25 -5
- package/dist/server.js +82 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,6 +47,10 @@ The agent chains the tools for you: `create_survey` → `add_question` → `publ
|
|
|
47
47
|
|
|
48
48
|
> Tip: `npm i -g @agentic-survey/mcp-server` gives you the shorter `agentic-survey init` command.
|
|
49
49
|
|
|
50
|
+
### Do I need to enable RLS / anything in the Supabase dashboard?
|
|
51
|
+
|
|
52
|
+
No. The `init --print-sql` migration enables Row Level Security and the access policies for you, on all four tables. **Don't disable RLS:** it's what fences the publishable key (which travels in the share link) so it can *only* read **published** surveys and **insert** responses. It cannot read anyone's responses or your drafts. Your secret key, stored locally on your machine, bypasses RLS for authoring and analysis.
|
|
53
|
+
|
|
50
54
|
## How it works
|
|
51
55
|
|
|
52
56
|
```
|
|
@@ -59,9 +63,13 @@ Publishing a survey returns a share link carrying your project ref and publishab
|
|
|
59
63
|
|
|
60
64
|
## Tools
|
|
61
65
|
|
|
62
|
-
`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
|
+
|
|
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
|
|
63
71
|
|
|
64
|
-
|
|
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.
|
|
65
73
|
|
|
66
74
|
## License
|
|
67
75
|
|
package/dist/cli.js
CHANGED
|
@@ -72,13 +72,30 @@ async function runInit(flags) {
|
|
|
72
72
|
existing.secretKey ||
|
|
73
73
|
'';
|
|
74
74
|
const publishableKey = flags['publishable-key'] ||
|
|
75
|
-
(await ask(`Supabase publishable key (sb_publishable_…,
|
|
75
|
+
(await ask(`Supabase publishable key (sb_publishable_…, needed for share links${existing.publishableKey ? ', already set' : ''}): `)) ||
|
|
76
76
|
existing.publishableKey ||
|
|
77
77
|
'';
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
// Page-service: the domain published survey links point at. Default to the
|
|
79
|
+
// hosted instance; let people opt into their own self-hosted/custom domain.
|
|
80
|
+
let pageEndpoint = flags['page-endpoint'] || '';
|
|
81
|
+
if (!pageEndpoint) {
|
|
82
|
+
const current = existing.pageEndpoint ?? DEFAULT_PAGE_ENDPOINT;
|
|
83
|
+
console.log('\nWhich domain do you want your surveys to appear on?');
|
|
84
|
+
console.log(` 1. ${DEFAULT_PAGE_ENDPOINT}/s/your-survey (default, quick start)`);
|
|
85
|
+
console.log(' 2. A custom domain (your own self-hosted page-service)');
|
|
86
|
+
const choice = (await ask(`Choose 1 or 2 [keep ${current}]: `)).trim();
|
|
87
|
+
if (choice === '1') {
|
|
88
|
+
pageEndpoint = DEFAULT_PAGE_ENDPOINT;
|
|
89
|
+
}
|
|
90
|
+
else if (choice === '2') {
|
|
91
|
+
pageEndpoint =
|
|
92
|
+
(await ask('Your page-service base URL (e.g. https://surveys.example.com): ')).trim() ||
|
|
93
|
+
current;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
pageEndpoint = current; // Enter keeps the shown default
|
|
97
|
+
}
|
|
98
|
+
}
|
|
82
99
|
if (!supabaseUrl || !secretKey) {
|
|
83
100
|
console.error('\nA project URL and secret key are required. Aborting.');
|
|
84
101
|
process.exitCode = 1;
|
|
@@ -107,6 +124,9 @@ async function runInit(flags) {
|
|
|
107
124
|
else {
|
|
108
125
|
console.log(`\n✓ Connected. Schema present (${probe.data.length} survey(s)).`);
|
|
109
126
|
}
|
|
127
|
+
if (!publishableKey) {
|
|
128
|
+
console.log("\n⚠ No publishable key set. Share links and response collection won't work until you add one (re-run init).");
|
|
129
|
+
}
|
|
110
130
|
printAgentSnippet();
|
|
111
131
|
}
|
|
112
132
|
async function main() {
|
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). ' +
|
package/package.json
CHANGED