@agentic-survey/mcp-server 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 netmonty
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # @agentic-survey/mcp-server
2
+
3
+ An open-source ([MIT](./LICENSE)) [MCP](https://modelcontextprotocol.io) server that lets your own AI agent **build surveys and read results**, straight into **your own Supabase**. You own the data, you carry the AI cost, and your keys never leave your machine. It's free.
4
+
5
+ Part of [agentic-survey-mcp](https://github.com/netmonty/agentic-survey-mcp). Site: [mcpsurveys.com](https://www.mcpsurveys.com).
6
+
7
+ ## Why
8
+
9
+ - **You own the data.** Surveys, questions, and responses live in *your* Supabase project. Nothing is stored on anyone else's infrastructure.
10
+ - **You carry the AI cost.** Authoring and analysis happen inside *your* agent. There's no AI running server-side, so there's nothing to bill you for.
11
+ - **Bring your own keys.** Your Supabase secret key stays in a local config file. It's never transmitted, never logged, and never a tool argument the agent can read.
12
+
13
+ ## Quick start
14
+
15
+ ```bash
16
+ # 1. Print the schema, paste it into your Supabase SQL editor
17
+ npx -y @agentic-survey/mcp-server init --print-sql
18
+
19
+ # 2. Store your keys locally and verify the connection
20
+ # (typed into your terminal, never sent to the agent)
21
+ npx -y @agentic-survey/mcp-server init
22
+ ```
23
+
24
+ Then point your MCP client at the server:
25
+
26
+ ```bash
27
+ # Claude Code
28
+ claude mcp add agentic-survey -- npx -y @agentic-survey/mcp-server
29
+ ```
30
+
31
+ ```jsonc
32
+ // Claude Desktop / Cursor / other (claude_desktop_config.json, .cursor/mcp.json, ...)
33
+ {
34
+ "mcpServers": {
35
+ "agentic-survey": { "command": "npx", "args": ["-y", "@agentic-survey/mcp-server"] }
36
+ }
37
+ }
38
+ ```
39
+
40
+ Now just talk to your agent:
41
+
42
+ > "Build a 4-question customer-satisfaction survey with a 1-5 rating, a yes/no, a multiple choice, and an open comment. Publish it and give me the share link."
43
+
44
+ > "Summarise the results so far, themes from the comments too."
45
+
46
+ The agent chains the tools for you: `create_survey` → `add_question` → `publish_survey` → `get_results`.
47
+
48
+ > Tip: `npm i -g @agentic-survey/mcp-server` gives you the shorter `agentic-survey init` command.
49
+
50
+ ## How it works
51
+
52
+ ```
53
+ your AI agent ──(MCP / stdio)──▶ mcp-server ──┐
54
+ ├──▶ your Supabase
55
+ respondent ──(browser)──▶ page-service ────────┘ (source of truth)
56
+ ```
57
+
58
+ Publishing a survey returns a share link carrying your project ref and publishable key. A respondent opens it, the stateless page-service writes their answers into your Supabase, and your agent reads them back with `get_results`. Supabase is the backend for now; more are planned.
59
+
60
+ ## Tools
61
+
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`.
63
+
64
+ Question types: `single_choice`, `multi_choice`, `rating`, `yes_no`, `number`, `short_text`, `long_text`.
65
+
66
+ ## License
67
+
68
+ [MIT](./LICENSE). Built by [Monty](https://montymakesthings.com). Source and issues on [GitHub](https://github.com/netmonty/agentic-survey-mcp).
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ import readline from 'node:readline';
3
+ import { readInitialMigrationSql } from '@agentic-survey/schema';
4
+ import { createDb, listSurveys, isErr } from '@agentic-survey/core';
5
+ import { runServer } from './index.js';
6
+ import { saveConfig, loadConfig, configPath, projectRefFromUrl, DEFAULT_PAGE_ENDPOINT, } from './config.js';
7
+ function parseFlags(argv) {
8
+ const out = {};
9
+ for (let i = 0; i < argv.length; i++) {
10
+ const a = argv[i];
11
+ if (!a.startsWith('--'))
12
+ continue;
13
+ const key = a.slice(2);
14
+ const next = argv[i + 1];
15
+ if (next && !next.startsWith('--')) {
16
+ out[key] = next;
17
+ i++;
18
+ }
19
+ else {
20
+ out[key] = true;
21
+ }
22
+ }
23
+ return out;
24
+ }
25
+ /** Prompt; when hidden, the typed characters are not echoed. */
26
+ function ask(query, hidden = false) {
27
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
28
+ if (hidden) {
29
+ let shownPrompt = false;
30
+ rl._writeToOutput = (s) => {
31
+ if (!shownPrompt) {
32
+ rl.output.write(s);
33
+ shownPrompt = true;
34
+ }
35
+ // mute keystroke echoes
36
+ };
37
+ }
38
+ return new Promise((resolve) => {
39
+ rl.question(query, (answer) => {
40
+ if (hidden)
41
+ rl.output.write('\n');
42
+ rl.close();
43
+ resolve(answer.trim());
44
+ });
45
+ });
46
+ }
47
+ function printAgentSnippet() {
48
+ const snippet = {
49
+ mcpServers: {
50
+ 'agentic-survey': {
51
+ command: 'npx',
52
+ args: ['-y', '@agentic-survey/mcp-server'],
53
+ },
54
+ },
55
+ };
56
+ console.log('\nAdd this to your agent config (e.g. Claude Desktop):\n');
57
+ console.log(JSON.stringify(snippet, null, 2));
58
+ }
59
+ async function runInit(flags) {
60
+ if (flags['print-sql']) {
61
+ process.stdout.write(readInitialMigrationSql());
62
+ return;
63
+ }
64
+ console.log('agentic-survey init — your keys are stored locally and never transmitted.\n');
65
+ const existing = loadConfig();
66
+ const supabaseUrl = flags.url ||
67
+ (await ask(`Supabase project URL${existing.supabaseUrl ? ` [${existing.supabaseUrl}]` : ''}: `)) ||
68
+ existing.supabaseUrl ||
69
+ '';
70
+ const secretKey = flags['secret-key'] ||
71
+ (await ask('Supabase SECRET key (sb_secret_…, hidden): ', true)) ||
72
+ existing.secretKey ||
73
+ '';
74
+ const publishableKey = flags['publishable-key'] ||
75
+ (await ask(`Supabase publishable key (sb_publishable_…, optional${existing.publishableKey ? ', set' : ''}): `)) ||
76
+ existing.publishableKey ||
77
+ '';
78
+ const pageEndpoint = flags['page-endpoint'] ||
79
+ (await ask(`Page-service endpoint [${existing.pageEndpoint ?? DEFAULT_PAGE_ENDPOINT}]: `)) ||
80
+ existing.pageEndpoint ||
81
+ DEFAULT_PAGE_ENDPOINT;
82
+ if (!supabaseUrl || !secretKey) {
83
+ console.error('\nA project URL and secret key are required. Aborting.');
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+ const cfg = {
88
+ supabaseUrl,
89
+ secretKey,
90
+ publishableKey: publishableKey || undefined,
91
+ pageEndpoint,
92
+ };
93
+ const path = saveConfig(cfg);
94
+ console.log(`\nSaved config to ${path} (mode 0600).`);
95
+ const ref = projectRefFromUrl(supabaseUrl);
96
+ console.log(`Project ref: ${ref ?? '(could not parse from URL)'}`);
97
+ // Verify the schema is present.
98
+ const db = createDb(supabaseUrl, secretKey);
99
+ const probe = await listSurveys(db, {});
100
+ if (isErr(probe)) {
101
+ console.log('\n⚠ Could not read the survey schema — it may not be installed yet.');
102
+ console.log(' Install it one of these ways:');
103
+ console.log(' 1) Run `npx agentic-survey init --print-sql` and paste the output into');
104
+ console.log(' the Supabase SQL editor.');
105
+ console.log(' 2) `supabase db push`, or apply via the Supabase MCP.');
106
+ }
107
+ else {
108
+ console.log(`\n✓ Connected. Schema present (${probe.data.length} survey(s)).`);
109
+ }
110
+ printAgentSnippet();
111
+ }
112
+ async function main() {
113
+ const [cmd, ...rest] = process.argv.slice(2);
114
+ const flags = parseFlags(rest);
115
+ switch (cmd) {
116
+ // Default (no command) = serve: this is what the agent launches via the MCP config.
117
+ case undefined:
118
+ case 'serve':
119
+ await runServer();
120
+ break;
121
+ case 'init':
122
+ await runInit(flags);
123
+ break;
124
+ default:
125
+ console.error(`Unknown command: ${cmd}\nUsage:\n agentic-survey start the MCP server (stdio)\n agentic-survey init [--print-sql] set up / print schema SQL`);
126
+ process.exitCode = 1;
127
+ }
128
+ }
129
+ main().catch((e) => {
130
+ console.error(`fatal: ${e instanceof Error ? e.message : String(e)}`);
131
+ process.exit(1);
132
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Connection config. The SECRET key lives only here (local file) or in env —
3
+ * it is NEVER accepted as an MCP tool argument, so it never reaches the agent
4
+ * or model provider. See docs/trust-model.md.
5
+ */
6
+ export interface StoredConfig {
7
+ supabaseUrl: string;
8
+ secretKey: string;
9
+ publishableKey?: string;
10
+ /** Page-service base URL; defaults to the author's hosted instance. */
11
+ pageEndpoint?: string;
12
+ }
13
+ export declare const DEFAULT_PAGE_ENDPOINT = "https://agentic-survey-pages.vercel.app";
14
+ export declare function configPath(): string;
15
+ /** Derive the Supabase project ref from the project URL. */
16
+ export declare function projectRefFromUrl(url: string): string | undefined;
17
+ /**
18
+ * Resolution order (per the brief): explicit args → local config file → env.
19
+ * The server has no explicit secret args, so: file values win, env fills gaps.
20
+ */
21
+ export declare function loadConfig(): Partial<StoredConfig>;
22
+ export declare function saveConfig(cfg: StoredConfig): string;
23
+ /** A config is "connectable" once it has a URL + secret key. */
24
+ export declare function isConnectable(cfg: Partial<StoredConfig>): cfg is StoredConfig;
package/dist/config.js ADDED
@@ -0,0 +1,61 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ export const DEFAULT_PAGE_ENDPOINT = 'https://agentic-survey-pages.vercel.app';
5
+ export function configPath() {
6
+ const base = process.env.AGENTIC_SURVEY_CONFIG ??
7
+ join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'agentic-survey', 'config.json');
8
+ return base;
9
+ }
10
+ /** Derive the Supabase project ref from the project URL. */
11
+ export function projectRefFromUrl(url) {
12
+ const m = url.match(/^https?:\/\/([a-z0-9]+)\.supabase\.(co|in|net)/i);
13
+ return m?.[1];
14
+ }
15
+ function readFile() {
16
+ const p = configPath();
17
+ if (!existsSync(p))
18
+ return {};
19
+ try {
20
+ return JSON.parse(readFileSync(p, 'utf8'));
21
+ }
22
+ catch {
23
+ return {};
24
+ }
25
+ }
26
+ /**
27
+ * Resolution order (per the brief): explicit args → local config file → env.
28
+ * The server has no explicit secret args, so: file values win, env fills gaps.
29
+ */
30
+ export function loadConfig() {
31
+ const fromEnv = {
32
+ supabaseUrl: process.env.SUPABASE_URL,
33
+ secretKey: process.env.SUPABASE_SECRET_KEY,
34
+ publishableKey: process.env.SUPABASE_PUBLISHABLE_KEY,
35
+ pageEndpoint: process.env.AGENTIC_SURVEY_PAGE_ENDPOINT,
36
+ };
37
+ const fromFile = readFile();
38
+ // Drop undefined so file values aren't clobbered by missing env keys.
39
+ const merged = { ...fromEnv };
40
+ for (const [k, v] of Object.entries(fromFile)) {
41
+ if (v !== undefined && v !== '')
42
+ merged[k] = v;
43
+ }
44
+ return merged;
45
+ }
46
+ export function saveConfig(cfg) {
47
+ const p = configPath();
48
+ mkdirSync(dirname(p), { recursive: true });
49
+ writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
50
+ try {
51
+ chmodSync(p, 0o600); // ensure perms even if the file pre-existed
52
+ }
53
+ catch {
54
+ /* best effort */
55
+ }
56
+ return p;
57
+ }
58
+ /** A config is "connectable" once it has a URL + secret key. */
59
+ export function isConnectable(cfg) {
60
+ return Boolean(cfg.supabaseUrl && cfg.secretKey);
61
+ }
@@ -0,0 +1,3 @@
1
+ export { buildServer } from './server.js';
2
+ /** Start the MCP server over stdio. */
3
+ export declare function runServer(): Promise<void>;
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
+ import { buildServer } from './server.js';
3
+ export { buildServer } from './server.js';
4
+ /** Start the MCP server over stdio. */
5
+ export async function runServer() {
6
+ const server = buildServer();
7
+ const transport = new StdioServerTransport();
8
+ await server.connect(transport);
9
+ // stdio transport keeps the process alive; log to stderr (stdout is the protocol).
10
+ process.stderr.write('agentic-survey MCP server running on stdio\n');
11
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function buildServer(): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,286 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
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';
4
+ import { readInitialMigrationSql } from '@agentic-survey/schema';
5
+ import { loadConfig, isConnectable, projectRefFromUrl, configPath, DEFAULT_PAGE_ENDPOINT, } from './config.js';
6
+ const QUESTION_TYPE = z.enum([
7
+ 'single_choice',
8
+ 'multi_choice',
9
+ 'short_text',
10
+ 'long_text',
11
+ 'rating',
12
+ 'yes_no',
13
+ 'number',
14
+ ]);
15
+ const SURVEY_STATUS = z.enum(['draft', 'published', 'closed']);
16
+ const ok = (summary, data) => ({
17
+ content: [{ type: 'text', text: summary }],
18
+ structuredContent: data,
19
+ });
20
+ const fail = (code, message) => ({
21
+ content: [{ type: 'text', text: `Error (${code}): ${message}` }],
22
+ structuredContent: { error: { code, message } },
23
+ isError: true,
24
+ });
25
+ const NOT_CONNECTED = fail('not_connected', 'No Supabase connection configured. Run `npx agentic-survey init` (it stores your keys locally — never paste them into the agent), then retry.');
26
+ /** Resolve a live connection from local config/env, or a typed error result. */
27
+ function getConn() {
28
+ const cfg = loadConfig();
29
+ if (!isConnectable(cfg))
30
+ return { ok: false, res: NOT_CONNECTED };
31
+ return { ok: true, db: createDb(cfg.supabaseUrl, cfg.secretKey), cfg };
32
+ }
33
+ function linkConfig(cfg) {
34
+ const projectRef = projectRefFromUrl(cfg.supabaseUrl);
35
+ if (!projectRef || !cfg.publishableKey)
36
+ return undefined;
37
+ return {
38
+ pageEndpoint: cfg.pageEndpoint ?? DEFAULT_PAGE_ENDPOINT,
39
+ projectRef,
40
+ publishableKey: cfg.publishableKey,
41
+ };
42
+ }
43
+ export function buildServer() {
44
+ const server = new McpServer({ name: 'agentic-survey', version: '0.0.0' });
45
+ server.registerTool('setup_connection', {
46
+ title: 'Set up / verify Supabase connection',
47
+ description: 'Verify the locally-configured Supabase connection and that the survey schema is present. ' +
48
+ 'Does NOT accept keys as arguments — keys are configured via `npx agentic-survey init` and stored ' +
49
+ 'locally. If the schema is missing, returns the SQL to run (paste into the Supabase SQL editor, or use ' +
50
+ '`supabase db push` / the Supabase MCP). Call this first if other tools report not_connected.',
51
+ inputSchema: {},
52
+ annotations: { readOnlyHint: true },
53
+ }, async () => {
54
+ const cfg = loadConfig();
55
+ if (!isConnectable(cfg)) {
56
+ return ok('Not connected.', {
57
+ connected: false,
58
+ configPath: configPath(),
59
+ next: 'Run `npx agentic-survey init` to store your Supabase URL + secret key locally.',
60
+ });
61
+ }
62
+ const db = createDb(cfg.supabaseUrl, cfg.secretKey);
63
+ const probe = await listSurveys(db, {});
64
+ if (isErr(probe)) {
65
+ return ok('Connected, but the survey schema is missing.', {
66
+ connected: true,
67
+ schemaPresent: false,
68
+ error: probe.error,
69
+ migrationSql: readInitialMigrationSql(),
70
+ next: 'Run the migrationSql in the Supabase SQL editor (or `supabase db push` / the Supabase MCP).',
71
+ });
72
+ }
73
+ return ok(`Connected. Schema present. ${probe.data.length} survey(s) found.`, {
74
+ connected: true,
75
+ schemaPresent: true,
76
+ surveyCount: probe.data.length,
77
+ publishableKeyConfigured: Boolean(cfg.publishableKey),
78
+ });
79
+ });
80
+ server.registerTool('create_survey', {
81
+ title: 'Create a draft survey',
82
+ description: 'Create a new draft survey. Returns its id. Typical flow: create_survey → add_question (×N) → ' +
83
+ 'publish_survey → get_share_link.',
84
+ inputSchema: {
85
+ title: z.string().min(1).describe('Survey title'),
86
+ description: z.string().optional().describe('Optional description'),
87
+ },
88
+ }, async ({ title, description }) => {
89
+ const c = getConn();
90
+ if (!c.ok)
91
+ return c.res;
92
+ const r = await createSurvey(c.db, { title, description });
93
+ if (isErr(r))
94
+ return fail(r.error.code, r.error.message);
95
+ return ok(`Created draft survey "${r.data.title}" (id ${r.data.id}).`, { survey: r.data });
96
+ });
97
+ server.registerTool('add_question', {
98
+ title: 'Add a question to a survey',
99
+ description: 'Append (or insert at `position`) a question. `config` shape depends on `type`: ' +
100
+ 'single_choice/multi_choice → { options: [{ id, label }] }; rating → { min, max }; ' +
101
+ 'number → { min?, max?, step?, unit? }; short_text/long_text → { placeholder?, maxLength? }; ' +
102
+ 'yes_no → {}. Give each choice option a stable `id`.',
103
+ inputSchema: {
104
+ surveyId: z.string(),
105
+ type: QUESTION_TYPE,
106
+ prompt: z.string().min(1),
107
+ required: z.boolean().optional(),
108
+ config: z.record(z.string(), z.unknown()).optional().describe('Per-type config (see description)'),
109
+ position: z.number().int().min(0).optional(),
110
+ },
111
+ }, async ({ surveyId, type, prompt, required, config, position }) => {
112
+ const c = getConn();
113
+ if (!c.ok)
114
+ return c.res;
115
+ const r = await addQuestion(c.db, surveyId, {
116
+ type,
117
+ prompt,
118
+ required,
119
+ config: config,
120
+ position,
121
+ });
122
+ if (isErr(r))
123
+ return fail(r.error.code, r.error.message);
124
+ return ok(`Added ${type} question at position ${r.data.position}.`, { question: r.data });
125
+ });
126
+ server.registerTool('update_question', {
127
+ title: 'Update a question',
128
+ description: 'Patch a question (prompt, type, required, config, position). Only provided fields change.',
129
+ inputSchema: {
130
+ questionId: z.string(),
131
+ prompt: z.string().min(1).optional(),
132
+ type: QUESTION_TYPE.optional(),
133
+ required: z.boolean().optional(),
134
+ config: z.record(z.string(), z.unknown()).optional(),
135
+ position: z.number().int().min(0).optional(),
136
+ },
137
+ }, async ({ questionId, ...patch }) => {
138
+ const c = getConn();
139
+ if (!c.ok)
140
+ return c.res;
141
+ const r = await updateQuestion(c.db, questionId, patch);
142
+ if (isErr(r))
143
+ return fail(r.error.code, r.error.message);
144
+ return ok('Question updated.', { question: r.data });
145
+ });
146
+ server.registerTool('remove_question', {
147
+ title: 'Remove a question',
148
+ description: 'Delete a question from a survey.',
149
+ inputSchema: { questionId: z.string() },
150
+ annotations: { destructiveHint: true },
151
+ }, async ({ questionId }) => {
152
+ const c = getConn();
153
+ if (!c.ok)
154
+ return c.res;
155
+ const r = await removeQuestion(c.db, questionId);
156
+ if (isErr(r))
157
+ return fail(r.error.code, r.error.message);
158
+ return ok('Question removed.', { removed: true });
159
+ });
160
+ server.registerTool('reorder_questions', {
161
+ title: 'Reorder a survey’s questions',
162
+ description: 'Set question order to match `orderedIds` (positions become array index).',
163
+ inputSchema: { surveyId: z.string(), orderedIds: z.array(z.string()).min(1) },
164
+ }, async ({ surveyId, orderedIds }) => {
165
+ const c = getConn();
166
+ if (!c.ok)
167
+ return c.res;
168
+ const r = await reorderQuestions(c.db, surveyId, orderedIds);
169
+ if (isErr(r))
170
+ return fail(r.error.code, r.error.message);
171
+ return ok('Questions reordered.', { reordered: true });
172
+ });
173
+ server.registerTool('publish_survey', {
174
+ title: 'Publish a survey',
175
+ description: 'Publish a draft survey and return its public share link (if a publishable key is configured). ' +
176
+ 'Respondents can then open the link and submit.',
177
+ inputSchema: { surveyId: z.string() },
178
+ }, async ({ surveyId }) => {
179
+ const c = getConn();
180
+ if (!c.ok)
181
+ return c.res;
182
+ const r = await publishSurvey(c.db, surveyId, linkConfig(c.cfg));
183
+ if (isErr(r))
184
+ return fail(r.error.code, r.error.message);
185
+ const note = r.data.shareUrl
186
+ ? `Share link: ${r.data.shareUrl}`
187
+ : 'Published, but no share link — configure a publishable key (re-run `init`) to enable public links.';
188
+ return ok(`Published "${r.data.survey.title}". ${note}`, {
189
+ survey: r.data.survey,
190
+ shareUrl: r.data.shareUrl ?? null,
191
+ });
192
+ });
193
+ server.registerTool('get_share_link', {
194
+ title: 'Get a published survey’s share link',
195
+ description: 'Return the public link for a published survey.',
196
+ inputSchema: { surveyId: z.string() },
197
+ annotations: { readOnlyHint: true },
198
+ }, async ({ surveyId }) => {
199
+ const c = getConn();
200
+ if (!c.ok)
201
+ return c.res;
202
+ const r = await getSurvey(c.db, surveyId);
203
+ if (isErr(r))
204
+ return fail(r.error.code, r.error.message);
205
+ if (r.data.survey.status !== 'published') {
206
+ return fail('not_published', `Survey is ${r.data.survey.status}; publish it first.`);
207
+ }
208
+ const link = linkConfig(c.cfg);
209
+ if (!link)
210
+ return fail('no_publishable_key', 'No publishable key configured; re-run `init`.');
211
+ return ok('Share link ready.', { shareUrl: buildShareUrl(link, surveyId) });
212
+ });
213
+ server.registerTool('list_surveys', {
214
+ title: 'List surveys',
215
+ description: 'List surveys, optionally filtered by status.',
216
+ inputSchema: { status: SURVEY_STATUS.optional() },
217
+ annotations: { readOnlyHint: true },
218
+ }, async ({ status }) => {
219
+ const c = getConn();
220
+ if (!c.ok)
221
+ return c.res;
222
+ const r = await listSurveys(c.db, { status });
223
+ if (isErr(r))
224
+ return fail(r.error.code, r.error.message);
225
+ return ok(`${r.data.length} survey(s).`, { surveys: r.data });
226
+ });
227
+ server.registerTool('get_survey', {
228
+ title: 'Get a survey’s full definition',
229
+ description: 'Return a survey plus its ordered questions.',
230
+ inputSchema: { surveyId: z.string() },
231
+ annotations: { readOnlyHint: true },
232
+ }, async ({ surveyId }) => {
233
+ const c = getConn();
234
+ if (!c.ok)
235
+ return c.res;
236
+ const r = await getSurvey(c.db, surveyId);
237
+ if (isErr(r))
238
+ return fail(r.error.code, r.error.message);
239
+ return ok(`"${r.data.survey.title}" — ${r.data.questions.length} question(s).`, {
240
+ survey: r.data.survey,
241
+ questions: r.data.questions,
242
+ });
243
+ });
244
+ server.registerTool('list_responses', {
245
+ title: 'List raw responses (paginated)',
246
+ description: 'Cursor-paginated raw responses with answers. Pass the returned nextCursor to page.',
247
+ inputSchema: {
248
+ surveyId: z.string(),
249
+ limit: z.number().int().min(1).max(200).optional(),
250
+ cursor: z.string().optional(),
251
+ },
252
+ annotations: { readOnlyHint: true },
253
+ }, async ({ surveyId, limit, cursor }) => {
254
+ const c = getConn();
255
+ if (!c.ok)
256
+ return c.res;
257
+ const r = await listResponses(c.db, surveyId, { limit, cursor });
258
+ if (isErr(r))
259
+ return fail(r.error.code, r.error.message);
260
+ return ok(`${r.data.responses.length} response(s).`, {
261
+ responses: r.data.responses,
262
+ nextCursor: r.data.nextCursor,
263
+ });
264
+ });
265
+ server.registerTool('get_results', {
266
+ title: 'Get survey results (aggregates + raw)',
267
+ description: 'Return pre-computed aggregates over ALL responses (counts/percentages for choices; ' +
268
+ 'mean/median/min/max/distribution for ratings & numbers; raw text for text questions) PLUS a ' +
269
+ 'paginated page of raw responses. Use the aggregates for summaries; theme the raw text yourself.',
270
+ inputSchema: {
271
+ surveyId: z.string(),
272
+ responsesLimit: z.number().int().min(1).max(200).optional(),
273
+ cursor: z.string().optional(),
274
+ },
275
+ annotations: { readOnlyHint: true },
276
+ }, async ({ surveyId, responsesLimit, cursor }) => {
277
+ const c = getConn();
278
+ if (!c.ok)
279
+ return c.res;
280
+ const r = await getResults(c.db, surveyId, { responsesLimit, cursor });
281
+ if (isErr(r))
282
+ return fail(r.error.code, r.error.message);
283
+ return ok(`Results for "${r.data.survey.title}": ${r.data.survey.responseCount} response(s).`, r.data);
284
+ });
285
+ return server;
286
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@agentic-survey/mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server (stdio) exposing survey tools over core, plus the agentic-survey CLI.",
5
+ "license": "MIT",
6
+ "author": "Monty Daley <monty@daley.org.nz>",
7
+ "homepage": "https://www.mcpsurveys.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/netmonty/agentic-survey-mcp.git",
11
+ "directory": "packages/mcp-server"
12
+ },
13
+ "bugs": "https://github.com/netmonty/agentic-survey-mcp/issues",
14
+ "keywords": [
15
+ "mcp",
16
+ "model-context-protocol",
17
+ "survey",
18
+ "surveys",
19
+ "supabase",
20
+ "ai-agent",
21
+ "forms",
22
+ "cli"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "type": "module",
31
+ "main": "dist/index.js",
32
+ "types": "dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js"
37
+ }
38
+ },
39
+ "bin": {
40
+ "agentic-survey": "dist/cli.js"
41
+ },
42
+ "files": [
43
+ "dist"
44
+ ],
45
+ "scripts": {
46
+ "build": "tsc -b",
47
+ "typecheck": "tsc -p tsconfig.json --noEmit",
48
+ "start": "node --import tsx src/cli.ts",
49
+ "test": "node --import tsx --env-file-if-exists=../../.env --test src/**/*.test.ts",
50
+ "prepublishOnly": "tsc -b"
51
+ },
52
+ "dependencies": {
53
+ "@agentic-survey/core": "^0.1.0",
54
+ "@agentic-survey/schema": "^0.1.0",
55
+ "@modelcontextprotocol/sdk": "^1.0.0",
56
+ "zod": "^3.23.0"
57
+ }
58
+ }