formback-mcp 1.0.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 ADDED
@@ -0,0 +1,96 @@
1
+ # @formback/mcp
2
+
3
+ MCP (Model Context Protocol) server for [FormBack](https://formback.email) — manage your forms and submissions directly from AI assistants like Claude, Cursor, and others.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @formback/mcp
9
+ # or use directly with npx
10
+ npx @formback/mcp
11
+ ```
12
+
13
+ ## Setup
14
+
15
+ ### 1. Get your API key
16
+
17
+ Go to your [FormBack Dashboard](https://formback.email/dashboard) → Settings → API Keys → Create Key.
18
+
19
+ ### 2. Configure your AI assistant
20
+
21
+ #### Claude Desktop
22
+
23
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
24
+
25
+ ```json
26
+ {
27
+ "mcpServers": {
28
+ "formback": {
29
+ "command": "npx",
30
+ "args": ["@formback/mcp"],
31
+ "env": {
32
+ "FORMBACK_API_KEY": "fb_your_key_here"
33
+ }
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ #### Cursor
40
+
41
+ Add to `.cursor/mcp.json` in your project:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "formback": {
47
+ "command": "npx",
48
+ "args": ["@formback/mcp"],
49
+ "env": {
50
+ "FORMBACK_API_KEY": "fb_your_key_here"
51
+ }
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## Available Tools
58
+
59
+ | Tool | Description |
60
+ |------|-------------|
61
+ | `list_forms` | List all your forms with submission counts |
62
+ | `create_form` | Create a new form (name, redirect URL, email notifications, webhook) |
63
+ | `get_form` | Get form details by ID |
64
+ | `update_form` | Update form settings |
65
+ | `delete_form` | Delete a form and all its submissions |
66
+ | `get_submissions` | Get form submissions with pagination |
67
+ | `delete_submission` | Delete a specific submission |
68
+ | `get_stats` | Account stats: plan, usage, limits |
69
+
70
+ ## Example Prompts
71
+
72
+ - "List all my forms"
73
+ - "Create a contact form called 'Website Contact' with email notifications"
74
+ - "Show me the latest submissions for my contact form"
75
+ - "How many submissions did I get this month?"
76
+ - "Delete the test form"
77
+
78
+ ## Environment Variables
79
+
80
+ | Variable | Required | Default | Description |
81
+ |----------|----------|---------|-------------|
82
+ | `FORMBACK_API_KEY` | Yes | — | Your FormBack API key |
83
+ | `FORMBACK_API_URL` | No | `https://api.formback.email` | API base URL |
84
+
85
+ ## Development
86
+
87
+ ```bash
88
+ cd mcp
89
+ npm install
90
+ npm run build
91
+ npm test
92
+ ```
93
+
94
+ ## License
95
+
96
+ MIT
@@ -0,0 +1,89 @@
1
+ /**
2
+ * FormBack API Client
3
+ */
4
+ export interface FormBackConfig {
5
+ apiKey: string;
6
+ apiUrl: string;
7
+ }
8
+ export interface Form {
9
+ id: string;
10
+ name: string;
11
+ redirectUrl: string | null;
12
+ emailNotify: boolean;
13
+ webhookUrl: string | null;
14
+ createdAt: string;
15
+ updatedAt: string;
16
+ _count?: {
17
+ submissions: number;
18
+ };
19
+ }
20
+ export interface Submission {
21
+ id: string;
22
+ formId: string;
23
+ data: Record<string, unknown>;
24
+ meta: Record<string, unknown>;
25
+ createdAt: string;
26
+ }
27
+ export interface Pagination {
28
+ page: number;
29
+ limit: number;
30
+ total: number;
31
+ pages: number;
32
+ }
33
+ export interface Account {
34
+ email: string;
35
+ plan: string;
36
+ limits: Record<string, number>;
37
+ usage: {
38
+ forms: number;
39
+ submissions: number;
40
+ };
41
+ }
42
+ export declare class FormBackApiError extends Error {
43
+ statusCode: number;
44
+ constructor(statusCode: number, message: string);
45
+ }
46
+ export declare class FormBackClient {
47
+ private apiKey;
48
+ private apiUrl;
49
+ constructor(config: FormBackConfig);
50
+ private request;
51
+ listForms(): Promise<{
52
+ forms: Form[];
53
+ }>;
54
+ createForm(data: {
55
+ name: string;
56
+ redirectUrl?: string;
57
+ emailNotify?: boolean;
58
+ webhookUrl?: string;
59
+ }): Promise<{
60
+ form: Form;
61
+ }>;
62
+ getForm(id: string): Promise<{
63
+ form: Form;
64
+ }>;
65
+ updateForm(id: string, data: {
66
+ name?: string;
67
+ redirectUrl?: string;
68
+ emailNotify?: boolean;
69
+ webhookUrl?: string;
70
+ }): Promise<{
71
+ form: Form;
72
+ }>;
73
+ deleteForm(id: string): Promise<{
74
+ ok: boolean;
75
+ }>;
76
+ getSubmissions(formId: string, params?: {
77
+ page?: number;
78
+ limit?: number;
79
+ }): Promise<{
80
+ submissions: Submission[];
81
+ pagination: Pagination;
82
+ }>;
83
+ deleteSubmission(id: string): Promise<{
84
+ ok: boolean;
85
+ }>;
86
+ getAccount(): Promise<{
87
+ account: Account;
88
+ }>;
89
+ }
package/dist/client.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * FormBack API Client
3
+ */
4
+ export class FormBackApiError extends Error {
5
+ statusCode;
6
+ constructor(statusCode, message) {
7
+ super(message);
8
+ this.statusCode = statusCode;
9
+ this.name = 'FormBackApiError';
10
+ }
11
+ }
12
+ export class FormBackClient {
13
+ apiKey;
14
+ apiUrl;
15
+ constructor(config) {
16
+ this.apiKey = config.apiKey;
17
+ this.apiUrl = config.apiUrl.replace(/\/$/, '');
18
+ }
19
+ async request(method, path, body) {
20
+ const url = `${this.apiUrl}${path}`;
21
+ const headers = {
22
+ 'Authorization': `ApiKey ${this.apiKey}`,
23
+ 'Content-Type': 'application/json',
24
+ };
25
+ const res = await fetch(url, {
26
+ method,
27
+ headers,
28
+ body: body ? JSON.stringify(body) : undefined,
29
+ });
30
+ if (!res.ok) {
31
+ const data = await res.json().catch(() => ({ error: res.statusText }));
32
+ throw new FormBackApiError(res.status, data.error || `HTTP ${res.status}`);
33
+ }
34
+ return res.json();
35
+ }
36
+ async listForms() {
37
+ return this.request('GET', '/api/forms');
38
+ }
39
+ async createForm(data) {
40
+ return this.request('POST', '/api/forms', {
41
+ name: data.name,
42
+ redirectUrl: data.redirectUrl,
43
+ emailNotify: data.emailNotify,
44
+ webhookUrl: data.webhookUrl,
45
+ });
46
+ }
47
+ async getForm(id) {
48
+ return this.request('GET', `/api/forms/${id}`);
49
+ }
50
+ async updateForm(id, data) {
51
+ return this.request('PATCH', `/api/forms/${id}`, data);
52
+ }
53
+ async deleteForm(id) {
54
+ return this.request('DELETE', `/api/forms/${id}`);
55
+ }
56
+ async getSubmissions(formId, params) {
57
+ const query = new URLSearchParams();
58
+ if (params?.page)
59
+ query.set('page', String(params.page));
60
+ if (params?.limit)
61
+ query.set('limit', String(params.limit));
62
+ const qs = query.toString();
63
+ return this.request('GET', `/api/forms/${formId}/submissions${qs ? `?${qs}` : ''}`);
64
+ }
65
+ async deleteSubmission(id) {
66
+ return this.request('DELETE', `/api/submissions/${id}`);
67
+ }
68
+ async getAccount() {
69
+ return this.request('GET', '/api/account');
70
+ }
71
+ }
72
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAwCH,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAEhC;IADT,YACS,UAAkB,EACzB,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;QAHR,eAAU,GAAV,UAAU,CAAQ;QAIzB,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AAED,MAAM,OAAO,cAAc;IACjB,MAAM,CAAS;IACf,MAAM,CAAS;IAEvB,YAAY,MAAsB;QAChC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACjD,CAAC;IAEO,KAAK,CAAC,OAAO,CAAI,MAAc,EAAE,IAAY,EAAE,IAAc;QACnE,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;QACpC,MAAM,OAAO,GAA2B;YACtC,eAAe,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE;YACxC,cAAc,EAAE,kBAAkB;SACnC,CAAC;QAEF,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC3B,MAAM;YACN,OAAO;YACP,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;SAC9C,CAAC,CAAC;QAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;YACvE,MAAM,IAAI,gBAAgB,CACxB,GAAG,CAAC,MAAM,EACT,IAA2B,CAAC,KAAK,IAAI,QAAQ,GAAG,CAAC,MAAM,EAAE,CAC3D,CAAC;QACJ,CAAC;QAED,OAAO,GAAG,CAAC,IAAI,EAAgB,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,SAAS;QACb,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;IAC3C,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,IAKhB;QACC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,YAAY,EAAE;YACxC,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,UAAU,EAAE,IAAI,CAAC,UAAU;SAC5B,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,EAAU;QACtB,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,CAAC,CAAC;IACjD,CAAC;IAED,KAAK,CAAC,UAAU,CACd,EAAU,EACV,IAAyF;QAEzF,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,cAAc,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;IACzD,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,EAAU;QACzB,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,cAAc,EAAE,EAAE,CAAC,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,cAAc,CAClB,MAAc,EACd,MAA0C;QAE1C,MAAM,KAAK,GAAG,IAAI,eAAe,EAAE,CAAC;QACpC,IAAI,MAAM,EAAE,IAAI;YAAE,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QACzD,IAAI,MAAM,EAAE,KAAK;YAAE,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5D,MAAM,EAAE,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,cAAc,MAAM,eAAe,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACtF,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,EAAU;QAC/B,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,oBAAoB,EAAE,EAAE,CAAC,CAAC;IAC1D,CAAC;IAED,KAAK,CAAC,UAAU;QACd,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;IAC7C,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * FormBack MCP Server
4
+ *
5
+ * Provides AI assistants with tools to manage forms and submissions via FormBack API.
6
+ */
7
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * FormBack MCP Server
4
+ *
5
+ * Provides AI assistants with tools to manage forms and submissions via FormBack API.
6
+ */
7
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
8
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
10
+ import { FormBackClient } from './client.js';
11
+ import { TOOLS, handleToolCall } from './tools.js';
12
+ const API_KEY = process.env.FORMBACK_API_KEY;
13
+ const API_URL = process.env.FORMBACK_API_URL || 'https://api.formback.email';
14
+ if (!API_KEY) {
15
+ console.error('Error: FORMBACK_API_KEY environment variable is required.');
16
+ console.error('Get your API key at https://formback.email/dashboard');
17
+ process.exit(1);
18
+ }
19
+ const client = new FormBackClient({ apiKey: API_KEY, apiUrl: API_URL });
20
+ const server = new Server({ name: 'formback', version: '1.0.0' }, { capabilities: { tools: {} } });
21
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
22
+ tools: TOOLS,
23
+ }));
24
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
25
+ const { name, arguments: args = {} } = request.params;
26
+ const result = await handleToolCall(client, name, args);
27
+ return {
28
+ content: [{ type: 'text', text: result }],
29
+ };
30
+ });
31
+ async function main() {
32
+ const transport = new StdioServerTransport();
33
+ await server.connect(transport);
34
+ }
35
+ main().catch((error) => {
36
+ console.error('Fatal error:', error);
37
+ process.exit(1);
38
+ });
39
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EACL,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;AAC7C,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,4BAA4B,CAAC;AAE7E,IAAI,CAAC,OAAO,EAAE,CAAC;IACb,OAAO,CAAC,KAAK,CAAC,2DAA2D,CAAC,CAAC;IAC3E,OAAO,CAAC,KAAK,CAAC,sDAAsD,CAAC,CAAC;IACtE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,cAAc,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;AAExE,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,EACtC,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAChC,CAAC;AAEF,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;IAC5D,KAAK,EAAE,KAAK;CACb,CAAC,CAAC,CAAC;AAEJ,MAAM,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;IAChE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IACtD,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,IAA+B,CAAC,CAAC;IACnF,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;KACnD,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;IACrC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * MCP Tool definitions for FormBack
3
+ */
4
+ import { FormBackClient } from './client.js';
5
+ export interface Tool {
6
+ name: string;
7
+ description: string;
8
+ inputSchema: {
9
+ type: 'object';
10
+ properties: Record<string, unknown>;
11
+ required?: string[];
12
+ };
13
+ }
14
+ export declare const TOOLS: Tool[];
15
+ type ToolArgs = Record<string, unknown>;
16
+ export declare function handleToolCall(client: FormBackClient, name: string, args: ToolArgs): Promise<string>;
17
+ export {};
package/dist/tools.js ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * MCP Tool definitions for FormBack
3
+ */
4
+ import { FormBackApiError } from './client.js';
5
+ export const TOOLS = [
6
+ {
7
+ name: 'list_forms',
8
+ description: 'List all forms for the authenticated user. Returns form names, IDs, and submission counts.',
9
+ inputSchema: { type: 'object', properties: {} },
10
+ },
11
+ {
12
+ name: 'create_form',
13
+ description: 'Create a new form. Returns the created form with its unique ID and endpoint URL.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ name: { type: 'string', description: 'Name of the form' },
18
+ redirect_url: { type: 'string', description: 'URL to redirect after submission (optional)' },
19
+ email_notify: { type: 'boolean', description: 'Send email notifications on submission (default: true)' },
20
+ webhook_url: { type: 'string', description: 'Webhook URL to call on submission (optional)' },
21
+ },
22
+ required: ['name'],
23
+ },
24
+ },
25
+ {
26
+ name: 'get_form',
27
+ description: 'Get details of a specific form by ID, including submission count.',
28
+ inputSchema: {
29
+ type: 'object',
30
+ properties: {
31
+ form_id: { type: 'string', description: 'Form ID' },
32
+ },
33
+ required: ['form_id'],
34
+ },
35
+ },
36
+ {
37
+ name: 'update_form',
38
+ description: 'Update form settings (name, redirect URL, email notifications, webhook URL).',
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: {
42
+ form_id: { type: 'string', description: 'Form ID' },
43
+ name: { type: 'string', description: 'New name' },
44
+ redirect_url: { type: 'string', description: 'New redirect URL' },
45
+ email_notify: { type: 'boolean', description: 'Enable/disable email notifications' },
46
+ webhook_url: { type: 'string', description: 'New webhook URL' },
47
+ },
48
+ required: ['form_id'],
49
+ },
50
+ },
51
+ {
52
+ name: 'delete_form',
53
+ description: 'Delete a form and all its submissions. This action is irreversible.',
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ form_id: { type: 'string', description: 'Form ID to delete' },
58
+ },
59
+ required: ['form_id'],
60
+ },
61
+ },
62
+ {
63
+ name: 'get_submissions',
64
+ description: 'Get submissions for a form with pagination. Returns submission data and metadata.',
65
+ inputSchema: {
66
+ type: 'object',
67
+ properties: {
68
+ form_id: { type: 'string', description: 'Form ID' },
69
+ page: { type: 'number', description: 'Page number (default: 1)' },
70
+ limit: { type: 'number', description: 'Items per page (default: 50, max: 100)' },
71
+ },
72
+ required: ['form_id'],
73
+ },
74
+ },
75
+ {
76
+ name: 'delete_submission',
77
+ description: 'Delete a specific submission by ID.',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: {
81
+ submission_id: { type: 'string', description: 'Submission ID to delete' },
82
+ },
83
+ required: ['submission_id'],
84
+ },
85
+ },
86
+ {
87
+ name: 'get_stats',
88
+ description: 'Get account statistics: plan, form count, submission usage this month, and limits.',
89
+ inputSchema: { type: 'object', properties: {} },
90
+ },
91
+ ];
92
+ export async function handleToolCall(client, name, args) {
93
+ try {
94
+ switch (name) {
95
+ case 'list_forms': {
96
+ const { forms } = await client.listForms();
97
+ if (forms.length === 0)
98
+ return 'No forms found. Create one with create_form.';
99
+ return JSON.stringify(forms, null, 2);
100
+ }
101
+ case 'create_form': {
102
+ const { form } = await client.createForm({
103
+ name: args.name,
104
+ redirectUrl: args.redirect_url,
105
+ emailNotify: args.email_notify,
106
+ webhookUrl: args.webhook_url,
107
+ });
108
+ return JSON.stringify(form, null, 2);
109
+ }
110
+ case 'get_form': {
111
+ const { form } = await client.getForm(args.form_id);
112
+ return JSON.stringify(form, null, 2);
113
+ }
114
+ case 'update_form': {
115
+ const { form_id, ...updates } = args;
116
+ const { form } = await client.updateForm(form_id, {
117
+ name: updates.name,
118
+ redirectUrl: updates.redirect_url,
119
+ emailNotify: updates.email_notify,
120
+ webhookUrl: updates.webhook_url,
121
+ });
122
+ return JSON.stringify(form, null, 2);
123
+ }
124
+ case 'delete_form': {
125
+ await client.deleteForm(args.form_id);
126
+ return 'Form deleted successfully.';
127
+ }
128
+ case 'get_submissions': {
129
+ const result = await client.getSubmissions(args.form_id, {
130
+ page: args.page,
131
+ limit: args.limit,
132
+ });
133
+ return JSON.stringify(result, null, 2);
134
+ }
135
+ case 'delete_submission': {
136
+ await client.deleteSubmission(args.submission_id);
137
+ return 'Submission deleted successfully.';
138
+ }
139
+ case 'get_stats': {
140
+ const { account } = await client.getAccount();
141
+ return JSON.stringify({
142
+ email: account.email,
143
+ plan: account.plan,
144
+ forms: account.usage.forms,
145
+ submissions_this_month: account.usage.submissions,
146
+ limits: account.limits,
147
+ }, null, 2);
148
+ }
149
+ default:
150
+ return `Unknown tool: ${name}`;
151
+ }
152
+ }
153
+ catch (error) {
154
+ if (error instanceof FormBackApiError) {
155
+ return `Error (${error.statusCode}): ${error.message}`;
156
+ }
157
+ throw error;
158
+ }
159
+ }
160
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "formback-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for FormBack \u2014 manage forms and submissions from AI assistants",
5
+ "type": "module",
6
+ "bin": {
7
+ "formback-mcp": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsx src/index.ts",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "test:live": "tsx tests/test-live.ts"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "formback",
20
+ "forms",
21
+ "ai"
22
+ ],
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "typescript": "^5.3.0",
29
+ "tsx": "^4.7.0",
30
+ "vitest": "^2.0.0",
31
+ "@types/node": "^22.0.0"
32
+ }
33
+ }
package/src/client.ts ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * FormBack API Client
3
+ */
4
+
5
+ export interface FormBackConfig {
6
+ apiKey: string;
7
+ apiUrl: string;
8
+ }
9
+
10
+ export interface Form {
11
+ id: string;
12
+ name: string;
13
+ redirectUrl: string | null;
14
+ emailNotify: boolean;
15
+ webhookUrl: string | null;
16
+ createdAt: string;
17
+ updatedAt: string;
18
+ _count?: { submissions: number };
19
+ }
20
+
21
+ export interface Submission {
22
+ id: string;
23
+ formId: string;
24
+ data: Record<string, unknown>;
25
+ meta: Record<string, unknown>;
26
+ createdAt: string;
27
+ }
28
+
29
+ export interface Pagination {
30
+ page: number;
31
+ limit: number;
32
+ total: number;
33
+ pages: number;
34
+ }
35
+
36
+ export interface Account {
37
+ email: string;
38
+ plan: string;
39
+ limits: Record<string, number>;
40
+ usage: { forms: number; submissions: number };
41
+ }
42
+
43
+ export class FormBackApiError extends Error {
44
+ constructor(
45
+ public statusCode: number,
46
+ message: string,
47
+ ) {
48
+ super(message);
49
+ this.name = 'FormBackApiError';
50
+ }
51
+ }
52
+
53
+ export class FormBackClient {
54
+ private apiKey: string;
55
+ private apiUrl: string;
56
+
57
+ constructor(config: FormBackConfig) {
58
+ this.apiKey = config.apiKey;
59
+ this.apiUrl = config.apiUrl.replace(/\/$/, '');
60
+ }
61
+
62
+ private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
63
+ const url = `${this.apiUrl}${path}`;
64
+ const headers: Record<string, string> = {
65
+ 'Authorization': `ApiKey ${this.apiKey}`,
66
+ 'Content-Type': 'application/json',
67
+ };
68
+
69
+ const res = await fetch(url, {
70
+ method,
71
+ headers,
72
+ body: body ? JSON.stringify(body) : undefined,
73
+ });
74
+
75
+ if (!res.ok) {
76
+ const data = await res.json().catch(() => ({ error: res.statusText }));
77
+ throw new FormBackApiError(
78
+ res.status,
79
+ (data as { error?: string }).error || `HTTP ${res.status}`,
80
+ );
81
+ }
82
+
83
+ return res.json() as Promise<T>;
84
+ }
85
+
86
+ async listForms(): Promise<{ forms: Form[] }> {
87
+ return this.request('GET', '/api/forms');
88
+ }
89
+
90
+ async createForm(data: {
91
+ name: string;
92
+ redirectUrl?: string;
93
+ emailNotify?: boolean;
94
+ webhookUrl?: string;
95
+ }): Promise<{ form: Form }> {
96
+ return this.request('POST', '/api/forms', {
97
+ name: data.name,
98
+ redirectUrl: data.redirectUrl,
99
+ emailNotify: data.emailNotify,
100
+ webhookUrl: data.webhookUrl,
101
+ });
102
+ }
103
+
104
+ async getForm(id: string): Promise<{ form: Form }> {
105
+ return this.request('GET', `/api/forms/${id}`);
106
+ }
107
+
108
+ async updateForm(
109
+ id: string,
110
+ data: { name?: string; redirectUrl?: string; emailNotify?: boolean; webhookUrl?: string },
111
+ ): Promise<{ form: Form }> {
112
+ return this.request('PATCH', `/api/forms/${id}`, data);
113
+ }
114
+
115
+ async deleteForm(id: string): Promise<{ ok: boolean }> {
116
+ return this.request('DELETE', `/api/forms/${id}`);
117
+ }
118
+
119
+ async getSubmissions(
120
+ formId: string,
121
+ params?: { page?: number; limit?: number },
122
+ ): Promise<{ submissions: Submission[]; pagination: Pagination }> {
123
+ const query = new URLSearchParams();
124
+ if (params?.page) query.set('page', String(params.page));
125
+ if (params?.limit) query.set('limit', String(params.limit));
126
+ const qs = query.toString();
127
+ return this.request('GET', `/api/forms/${formId}/submissions${qs ? `?${qs}` : ''}`);
128
+ }
129
+
130
+ async deleteSubmission(id: string): Promise<{ ok: boolean }> {
131
+ return this.request('DELETE', `/api/submissions/${id}`);
132
+ }
133
+
134
+ async getAccount(): Promise<{ account: Account }> {
135
+ return this.request('GET', '/api/account');
136
+ }
137
+ }
package/src/index.ts ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * FormBack MCP Server
5
+ *
6
+ * Provides AI assistants with tools to manage forms and submissions via FormBack API.
7
+ */
8
+
9
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
+ import {
12
+ CallToolRequestSchema,
13
+ ListToolsRequestSchema,
14
+ } from '@modelcontextprotocol/sdk/types.js';
15
+ import { FormBackClient } from './client.js';
16
+ import { TOOLS, handleToolCall } from './tools.js';
17
+
18
+ const API_KEY = process.env.FORMBACK_API_KEY;
19
+ const API_URL = process.env.FORMBACK_API_URL || 'https://api.formback.email';
20
+
21
+ if (!API_KEY) {
22
+ console.error('Error: FORMBACK_API_KEY environment variable is required.');
23
+ console.error('Get your API key at https://formback.email/dashboard');
24
+ process.exit(1);
25
+ }
26
+
27
+ const client = new FormBackClient({ apiKey: API_KEY, apiUrl: API_URL });
28
+
29
+ const server = new Server(
30
+ { name: 'formback', version: '1.0.0' },
31
+ { capabilities: { tools: {} } },
32
+ );
33
+
34
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
35
+ tools: TOOLS,
36
+ }));
37
+
38
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
39
+ const { name, arguments: args = {} } = request.params;
40
+ const result = await handleToolCall(client, name, args as Record<string, unknown>);
41
+ return {
42
+ content: [{ type: 'text' as const, text: result }],
43
+ };
44
+ });
45
+
46
+ async function main() {
47
+ const transport = new StdioServerTransport();
48
+ await server.connect(transport);
49
+ }
50
+
51
+ main().catch((error) => {
52
+ console.error('Fatal error:', error);
53
+ process.exit(1);
54
+ });
package/src/tools.ts ADDED
@@ -0,0 +1,188 @@
1
+ /**
2
+ * MCP Tool definitions for FormBack
3
+ */
4
+
5
+ import { FormBackClient, FormBackApiError } from './client.js';
6
+
7
+ export interface Tool {
8
+ name: string;
9
+ description: string;
10
+ inputSchema: {
11
+ type: 'object';
12
+ properties: Record<string, unknown>;
13
+ required?: string[];
14
+ };
15
+ }
16
+
17
+ export const TOOLS: Tool[] = [
18
+ {
19
+ name: 'list_forms',
20
+ description: 'List all forms for the authenticated user. Returns form names, IDs, and submission counts.',
21
+ inputSchema: { type: 'object', properties: {} },
22
+ },
23
+ {
24
+ name: 'create_form',
25
+ description: 'Create a new form. Returns the created form with its unique ID and endpoint URL.',
26
+ inputSchema: {
27
+ type: 'object',
28
+ properties: {
29
+ name: { type: 'string', description: 'Name of the form' },
30
+ redirect_url: { type: 'string', description: 'URL to redirect after submission (optional)' },
31
+ email_notify: { type: 'boolean', description: 'Send email notifications on submission (default: true)' },
32
+ webhook_url: { type: 'string', description: 'Webhook URL to call on submission (optional)' },
33
+ },
34
+ required: ['name'],
35
+ },
36
+ },
37
+ {
38
+ name: 'get_form',
39
+ description: 'Get details of a specific form by ID, including submission count.',
40
+ inputSchema: {
41
+ type: 'object',
42
+ properties: {
43
+ form_id: { type: 'string', description: 'Form ID' },
44
+ },
45
+ required: ['form_id'],
46
+ },
47
+ },
48
+ {
49
+ name: 'update_form',
50
+ description: 'Update form settings (name, redirect URL, email notifications, webhook URL).',
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ form_id: { type: 'string', description: 'Form ID' },
55
+ name: { type: 'string', description: 'New name' },
56
+ redirect_url: { type: 'string', description: 'New redirect URL' },
57
+ email_notify: { type: 'boolean', description: 'Enable/disable email notifications' },
58
+ webhook_url: { type: 'string', description: 'New webhook URL' },
59
+ },
60
+ required: ['form_id'],
61
+ },
62
+ },
63
+ {
64
+ name: 'delete_form',
65
+ description: 'Delete a form and all its submissions. This action is irreversible.',
66
+ inputSchema: {
67
+ type: 'object',
68
+ properties: {
69
+ form_id: { type: 'string', description: 'Form ID to delete' },
70
+ },
71
+ required: ['form_id'],
72
+ },
73
+ },
74
+ {
75
+ name: 'get_submissions',
76
+ description: 'Get submissions for a form with pagination. Returns submission data and metadata.',
77
+ inputSchema: {
78
+ type: 'object',
79
+ properties: {
80
+ form_id: { type: 'string', description: 'Form ID' },
81
+ page: { type: 'number', description: 'Page number (default: 1)' },
82
+ limit: { type: 'number', description: 'Items per page (default: 50, max: 100)' },
83
+ },
84
+ required: ['form_id'],
85
+ },
86
+ },
87
+ {
88
+ name: 'delete_submission',
89
+ description: 'Delete a specific submission by ID.',
90
+ inputSchema: {
91
+ type: 'object',
92
+ properties: {
93
+ submission_id: { type: 'string', description: 'Submission ID to delete' },
94
+ },
95
+ required: ['submission_id'],
96
+ },
97
+ },
98
+ {
99
+ name: 'get_stats',
100
+ description: 'Get account statistics: plan, form count, submission usage this month, and limits.',
101
+ inputSchema: { type: 'object', properties: {} },
102
+ },
103
+ ];
104
+
105
+ type ToolArgs = Record<string, unknown>;
106
+
107
+ export async function handleToolCall(
108
+ client: FormBackClient,
109
+ name: string,
110
+ args: ToolArgs,
111
+ ): Promise<string> {
112
+ try {
113
+ switch (name) {
114
+ case 'list_forms': {
115
+ const { forms } = await client.listForms();
116
+ if (forms.length === 0) return 'No forms found. Create one with create_form.';
117
+ return JSON.stringify(forms, null, 2);
118
+ }
119
+
120
+ case 'create_form': {
121
+ const { form } = await client.createForm({
122
+ name: args.name as string,
123
+ redirectUrl: args.redirect_url as string | undefined,
124
+ emailNotify: args.email_notify as boolean | undefined,
125
+ webhookUrl: args.webhook_url as string | undefined,
126
+ });
127
+ return JSON.stringify(form, null, 2);
128
+ }
129
+
130
+ case 'get_form': {
131
+ const { form } = await client.getForm(args.form_id as string);
132
+ return JSON.stringify(form, null, 2);
133
+ }
134
+
135
+ case 'update_form': {
136
+ const { form_id, ...updates } = args;
137
+ const { form } = await client.updateForm(form_id as string, {
138
+ name: updates.name as string | undefined,
139
+ redirectUrl: updates.redirect_url as string | undefined,
140
+ emailNotify: updates.email_notify as boolean | undefined,
141
+ webhookUrl: updates.webhook_url as string | undefined,
142
+ });
143
+ return JSON.stringify(form, null, 2);
144
+ }
145
+
146
+ case 'delete_form': {
147
+ await client.deleteForm(args.form_id as string);
148
+ return 'Form deleted successfully.';
149
+ }
150
+
151
+ case 'get_submissions': {
152
+ const result = await client.getSubmissions(args.form_id as string, {
153
+ page: args.page as number | undefined,
154
+ limit: args.limit as number | undefined,
155
+ });
156
+ return JSON.stringify(result, null, 2);
157
+ }
158
+
159
+ case 'delete_submission': {
160
+ await client.deleteSubmission(args.submission_id as string);
161
+ return 'Submission deleted successfully.';
162
+ }
163
+
164
+ case 'get_stats': {
165
+ const { account } = await client.getAccount();
166
+ return JSON.stringify(
167
+ {
168
+ email: account.email,
169
+ plan: account.plan,
170
+ forms: account.usage.forms,
171
+ submissions_this_month: account.usage.submissions,
172
+ limits: account.limits,
173
+ },
174
+ null,
175
+ 2,
176
+ );
177
+ }
178
+
179
+ default:
180
+ return `Unknown tool: ${name}`;
181
+ }
182
+ } catch (error) {
183
+ if (error instanceof FormBackApiError) {
184
+ return `Error (${error.statusCode}): ${error.message}`;
185
+ }
186
+ throw error;
187
+ }
188
+ }
@@ -0,0 +1,175 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { FormBackClient, FormBackApiError } from '../src/client.js';
3
+ import { handleToolCall } from '../src/tools.js';
4
+
5
+ // Mock fetch globally
6
+ const mockFetch = vi.fn();
7
+ vi.stubGlobal('fetch', mockFetch);
8
+
9
+ function mockResponse(data: unknown, status = 200) {
10
+ return {
11
+ ok: status >= 200 && status < 300,
12
+ status,
13
+ statusText: status === 200 ? 'OK' : 'Error',
14
+ json: () => Promise.resolve(data),
15
+ };
16
+ }
17
+
18
+ const client = new FormBackClient({
19
+ apiKey: 'fb_test123',
20
+ apiUrl: 'https://api.formback.email',
21
+ });
22
+
23
+ beforeEach(() => {
24
+ mockFetch.mockReset();
25
+ });
26
+
27
+ describe('FormBackClient', () => {
28
+ it('sends correct auth header', async () => {
29
+ mockFetch.mockResolvedValue(mockResponse({ forms: [] }));
30
+ await client.listForms();
31
+ expect(mockFetch).toHaveBeenCalledWith(
32
+ 'https://api.formback.email/api/forms',
33
+ expect.objectContaining({
34
+ headers: expect.objectContaining({
35
+ Authorization: 'ApiKey fb_test123',
36
+ }),
37
+ }),
38
+ );
39
+ });
40
+
41
+ it('throws FormBackApiError on non-ok response', async () => {
42
+ mockFetch.mockResolvedValue(mockResponse({ error: 'Invalid API key' }, 401));
43
+ await expect(client.listForms()).rejects.toThrow(FormBackApiError);
44
+ await expect(client.listForms()).rejects.toThrow('Invalid API key');
45
+ });
46
+
47
+ it('creates form with correct body', async () => {
48
+ mockFetch.mockResolvedValue(mockResponse({ form: { id: '1', name: 'Test' } }, 201));
49
+ await client.createForm({ name: 'Test', webhookUrl: 'https://hook.io' });
50
+ const call = mockFetch.mock.calls[0];
51
+ expect(call[0]).toBe('https://api.formback.email/api/forms');
52
+ expect(call[1].method).toBe('POST');
53
+ expect(JSON.parse(call[1].body)).toEqual({
54
+ name: 'Test',
55
+ redirectUrl: undefined,
56
+ emailNotify: undefined,
57
+ webhookUrl: 'https://hook.io',
58
+ });
59
+ });
60
+
61
+ it('gets submissions with pagination', async () => {
62
+ mockFetch.mockResolvedValue(
63
+ mockResponse({ submissions: [], pagination: { page: 2, limit: 10, total: 0, pages: 0 } }),
64
+ );
65
+ await client.getSubmissions('form1', { page: 2, limit: 10 });
66
+ expect(mockFetch.mock.calls[0][0]).toBe(
67
+ 'https://api.formback.email/api/forms/form1/submissions?page=2&limit=10',
68
+ );
69
+ });
70
+ });
71
+
72
+ describe('handleToolCall', () => {
73
+ it('list_forms returns forms', async () => {
74
+ mockFetch.mockResolvedValue(
75
+ mockResponse({
76
+ forms: [{ id: '1', name: 'Contact', _count: { submissions: 5 } }],
77
+ }),
78
+ );
79
+ const result = await handleToolCall(client, 'list_forms', {});
80
+ const parsed = JSON.parse(result);
81
+ expect(parsed).toHaveLength(1);
82
+ expect(parsed[0].name).toBe('Contact');
83
+ });
84
+
85
+ it('list_forms empty returns message', async () => {
86
+ mockFetch.mockResolvedValue(mockResponse({ forms: [] }));
87
+ const result = await handleToolCall(client, 'list_forms', {});
88
+ expect(result).toContain('No forms found');
89
+ });
90
+
91
+ it('create_form passes args correctly', async () => {
92
+ mockFetch.mockResolvedValue(mockResponse({ form: { id: '2', name: 'New' } }));
93
+ const result = await handleToolCall(client, 'create_form', {
94
+ name: 'New',
95
+ redirect_url: 'https://thanks.com',
96
+ });
97
+ expect(JSON.parse(result).name).toBe('New');
98
+ });
99
+
100
+ it('get_form returns form', async () => {
101
+ mockFetch.mockResolvedValue(mockResponse({ form: { id: '1', name: 'Test' } }));
102
+ const result = await handleToolCall(client, 'get_form', { form_id: '1' });
103
+ expect(JSON.parse(result).id).toBe('1');
104
+ });
105
+
106
+ it('update_form sends updates', async () => {
107
+ mockFetch.mockResolvedValue(mockResponse({ form: { id: '1', name: 'Updated' } }));
108
+ const result = await handleToolCall(client, 'update_form', {
109
+ form_id: '1',
110
+ name: 'Updated',
111
+ });
112
+ expect(JSON.parse(result).name).toBe('Updated');
113
+ });
114
+
115
+ it('delete_form returns success', async () => {
116
+ mockFetch.mockResolvedValue(mockResponse({ ok: true }));
117
+ const result = await handleToolCall(client, 'delete_form', { form_id: '1' });
118
+ expect(result).toContain('deleted');
119
+ });
120
+
121
+ it('get_submissions with pagination', async () => {
122
+ mockFetch.mockResolvedValue(
123
+ mockResponse({
124
+ submissions: [{ id: 's1', data: { email: 'test@test.com' } }],
125
+ pagination: { page: 1, limit: 50, total: 1, pages: 1 },
126
+ }),
127
+ );
128
+ const result = await handleToolCall(client, 'get_submissions', { form_id: '1' });
129
+ const parsed = JSON.parse(result);
130
+ expect(parsed.submissions).toHaveLength(1);
131
+ expect(parsed.pagination.total).toBe(1);
132
+ });
133
+
134
+ it('delete_submission returns success', async () => {
135
+ mockFetch.mockResolvedValue(mockResponse({ ok: true }));
136
+ const result = await handleToolCall(client, 'delete_submission', { submission_id: 's1' });
137
+ expect(result).toContain('deleted');
138
+ });
139
+
140
+ it('get_stats returns account info', async () => {
141
+ mockFetch.mockResolvedValue(
142
+ mockResponse({
143
+ account: {
144
+ email: 'test@test.com',
145
+ plan: 'free',
146
+ limits: { forms: 3, submissions: 100 },
147
+ usage: { forms: 1, submissions: 42 },
148
+ },
149
+ }),
150
+ );
151
+ const result = await handleToolCall(client, 'get_stats', {});
152
+ const parsed = JSON.parse(result);
153
+ expect(parsed.plan).toBe('free');
154
+ expect(parsed.submissions_this_month).toBe(42);
155
+ });
156
+
157
+ it('handles API errors gracefully', async () => {
158
+ mockFetch.mockResolvedValue(mockResponse({ error: 'Form not found' }, 404));
159
+ const result = await handleToolCall(client, 'get_form', { form_id: 'bad' });
160
+ expect(result).toContain('404');
161
+ expect(result).toContain('Form not found');
162
+ });
163
+
164
+ it('handles auth errors', async () => {
165
+ mockFetch.mockResolvedValue(mockResponse({ error: 'Invalid API key' }, 401));
166
+ const result = await handleToolCall(client, 'list_forms', {});
167
+ expect(result).toContain('401');
168
+ expect(result).toContain('Invalid API key');
169
+ });
170
+
171
+ it('unknown tool returns error message', async () => {
172
+ const result = await handleToolCall(client, 'nonexistent', {});
173
+ expect(result).toContain('Unknown tool');
174
+ });
175
+ });
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ /**
4
+ * Live integration test for FormBack MCP tools against production API.
5
+ *
6
+ * Usage: FORMBACK_API_KEY=fb_xxx tsx tests/test-live.ts
7
+ */
8
+
9
+ import { FormBackClient } from '../src/client.js';
10
+ import { handleToolCall } from '../src/tools.js';
11
+
12
+ const API_KEY = process.env.FORMBACK_API_KEY;
13
+ const API_URL = process.env.FORMBACK_API_URL || 'https://api.formback.email';
14
+
15
+ if (!API_KEY) {
16
+ console.error('Set FORMBACK_API_KEY to run live tests');
17
+ process.exit(1);
18
+ }
19
+
20
+ const client = new FormBackClient({ apiKey: API_KEY, apiUrl: API_URL });
21
+
22
+ let passed = 0;
23
+ let failed = 0;
24
+
25
+ function assert(condition: boolean, msg: string) {
26
+ if (condition) {
27
+ console.log(` ✅ ${msg}`);
28
+ passed++;
29
+ } else {
30
+ console.error(` ❌ ${msg}`);
31
+ failed++;
32
+ }
33
+ }
34
+
35
+ async function run() {
36
+ console.log('🧪 FormBack MCP Live Tests\n');
37
+ console.log(`API: ${API_URL}\n`);
38
+
39
+ // 1. Get stats
40
+ console.log('📊 get_stats');
41
+ const stats = await handleToolCall(client, 'get_stats', {});
42
+ const statsObj = JSON.parse(stats);
43
+ assert(!!statsObj.email, `Account email: ${statsObj.email}`);
44
+ assert(!!statsObj.plan, `Plan: ${statsObj.plan}`);
45
+
46
+ // 2. Create form
47
+ console.log('\n📝 create_form');
48
+ const createResult = await handleToolCall(client, 'create_form', {
49
+ name: `MCP Test ${Date.now()}`,
50
+ redirect_url: 'https://example.com/thanks',
51
+ email_notify: false,
52
+ });
53
+ const form = JSON.parse(createResult);
54
+ assert(!!form.id, `Created form: ${form.id}`);
55
+ assert(form.name.startsWith('MCP Test'), `Name: ${form.name}`);
56
+ const formId = form.id;
57
+
58
+ // 3. List forms
59
+ console.log('\n📋 list_forms');
60
+ const listResult = await handleToolCall(client, 'list_forms', {});
61
+ const forms = JSON.parse(listResult);
62
+ assert(Array.isArray(forms), `Got ${forms.length} forms`);
63
+ assert(forms.some((f: { id: string }) => f.id === formId), 'New form in list');
64
+
65
+ // 4. Get form
66
+ console.log('\n🔍 get_form');
67
+ const getResult = await handleToolCall(client, 'get_form', { form_id: formId });
68
+ const fetched = JSON.parse(getResult);
69
+ assert(fetched.id === formId, 'Fetched correct form');
70
+
71
+ // 5. Update form
72
+ console.log('\n✏️ update_form');
73
+ const updateResult = await handleToolCall(client, 'update_form', {
74
+ form_id: formId,
75
+ name: 'MCP Updated',
76
+ webhook_url: 'https://hook.example.com',
77
+ });
78
+ const updated = JSON.parse(updateResult);
79
+ assert(updated.name === 'MCP Updated', 'Name updated');
80
+
81
+ // 6. Get submissions (should be empty)
82
+ console.log('\n📨 get_submissions');
83
+ const subsResult = await handleToolCall(client, 'get_submissions', { form_id: formId });
84
+ const subs = JSON.parse(subsResult);
85
+ assert(subs.pagination.total === 0, 'No submissions yet');
86
+
87
+ // 7. Submit data via form endpoint
88
+ console.log('\n📤 Submit test data');
89
+ const submitRes = await fetch(`${API_URL}/f/${formId}`, {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/json' },
92
+ body: JSON.stringify({ name: 'Test User', email: 'test@mcp.io', message: 'Hello from MCP test' }),
93
+ });
94
+ assert(submitRes.ok || submitRes.status === 302 || submitRes.status === 303, `Submit status: ${submitRes.status}`);
95
+
96
+ // 8. Get submissions again
97
+ console.log('\n📨 get_submissions (after submit)');
98
+ const subsResult2 = await handleToolCall(client, 'get_submissions', { form_id: formId });
99
+ const subs2 = JSON.parse(subsResult2);
100
+ assert(subs2.pagination.total >= 1, `Submissions: ${subs2.pagination.total}`);
101
+
102
+ // 9. Delete submission if exists
103
+ if (subs2.submissions.length > 0) {
104
+ console.log('\n🗑️ delete_submission');
105
+ const delSubResult = await handleToolCall(client, 'delete_submission', {
106
+ submission_id: subs2.submissions[0].id,
107
+ });
108
+ assert(delSubResult.includes('deleted'), 'Submission deleted');
109
+ }
110
+
111
+ // 10. Delete form
112
+ console.log('\n🗑️ delete_form');
113
+ const delResult = await handleToolCall(client, 'delete_form', { form_id: formId });
114
+ assert(delResult.includes('deleted'), 'Form deleted');
115
+
116
+ // 11. Verify deleted
117
+ console.log('\n🔍 get_form (deleted — expect error)');
118
+ const gone = await handleToolCall(client, 'get_form', { form_id: formId });
119
+ assert(gone.includes('404') || gone.includes('not found'), 'Form gone');
120
+
121
+ // Summary
122
+ console.log(`\n${'='.repeat(40)}`);
123
+ console.log(`Results: ${passed} passed, ${failed} failed`);
124
+ process.exit(failed > 0 ? 1 : 0);
125
+ }
126
+
127
+ run().catch((err) => {
128
+ console.error('Fatal:', err);
129
+ process.exit(1);
130
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true,
12
+ "sourceMap": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist", "tests"]
16
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['tests/**/*.test.ts'],
6
+ },
7
+ });