{"name":"io.github.vitorrocha-panda/foxform","slug":"vitorrocha-panda-foxform","title":"FoxForm","description":"Build, publish and read scored forms and quizzes where the score picks the next screen.","url":"https://mcp.market/server/vitorrocha-panda-foxform","rating":null,"grade":"C","score":61,"certified":false,"status":"active","category":"other","tags":[],"presence":{"score":8,"stars":null,"forks":null,"downloads_week":null,"last_push_at":null,"license":null},"uptime":{"percent":100,"checks":6,"ok":6,"last_checked_at":"2026-09-21T00:20:50.083Z","last_ok_at":"2026-09-21T00:20:50.083Z","latency_ms":2135},"claimed":false,"transport":"remote","callable_via_gateway":true,"default_price_micros":0,"repository":null,"website":"https://foxform.app","version":"1.0.0","remotes":[{"type":"streamable-http","url":"https://mcp.foxform.app/mcp","headers":[{"description":"FoxForm API key (ff_live_...), generated in the dashboard under MCP & API.","isRequired":true,"isSecret":true,"name":"x-api-key"}]}],"packages":[],"tools":[{"name":"foxform_create_form","description":"Create a new form, including per-screen conditional logic. Requires a WRITE-scoped API key.\n\nArgs:\n  - title (string): form title (required)\n  - description (string, optional)\n  - theme (string, optional): one of midnight|ocean|sunset|forest|lavender|minimal (default sunset = Ember)\n  - questions (array, optional): array of screen objects ({ id, type, title, required, variableName?, choices?, logic?, ... }); omit to start empty\n  - thank_you_message (string, optional)\n\nReturns: { form } with the created form (including its id and slug). The form starts as a draft — call foxform_publish_form to make it live.\n\nScreen fields are validated: unknown fields are REJECTED instead of being stored and ignored, and branching rules are cross-checked against the screen ids in the same payload.\n\nCONDITIONAL LOGIC (branching), per screen — stored in `questions[].logic`:\n\n  logic.conditionalNavigationV2 = {\n    enabled: true,\n    groups: [                       // groups are OR-joined; FIRST matching group wins\n      {\n        id: \"grp-1\",\n        conditions: [               // conditions inside a group are AND-joined\n          { id: \"cond-1\", left: \"{{quer_testar}}\", operator: \"equal_to\", right: \"Ainda não\" }\n        ],\n        then: { type: \"specific_screen\", targetScreenId: \"s-motivos\" }\n      }\n    ]\n  }\n\n- `then.type`: 'next_screen' | 'previous_screen' | 'specific_screen' (needs targetScreenId = another screen's `id`) | 'end_form'.\n  Add `then.url` (+ optional `openNewTab`) to redirect to an external URL instead.\n- `operator`: 'equal_to' | 'not_equal_to' | 'greater_than' | 'greater_or_equal_than' | 'less_than' | 'less_or_equal_than' | 'contains'.\n- `left`/`right` are EXPRESSION strings: a literal (\"10\", \"Ainda não\"), a variable (\"{{score}}\", \"{{minha_var}}\" = the screen's `variableName`), or arithmetic (\"calc({{peso}}/(({{altura}}/100)*({{altura}}/100)))\").\n- Comparing an ANSWER: use `left: \"{{<variableName of the deciding screen>}}\"` and `right` = the option's `label` OR its `value` (both match).\n- `{{score}}` is the running sum of `points` on the options picked so far (`choices[].points`, `images[].points`) — that is how score-based branching works.\n- A navigation group with no conditions NEVER matches. `enabled: false` stores the rules but disables them.\n- Screen-level conditional display uses the same group shape: `logic.display = { enabled: true, groups: [...], showAfterSeconds?: n }` (`then` is ignored — THEN means \"show\").\n- Other logic keys: `logic.autoAdvance = { enabled, delaySeconds? }`, `logic.navigationBehavior = { onButtonClick?, onAutoAdvance?, targetScreenId? }`.\n- `logic.conditionalNavigation` (legacy, pre-DEVF-161) is still read and migrated on load — don't author new rules with it.\n\nUnknown fields are REJECTED (they used to be stored and silently ignored): `logic` as an array, or `rules`/`branching`/`conditions`/`goto`/`jump`/`nextScreen` anywhere, are not read by any renderer.","write_action":true,"price_micros":0,"input_schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":255,"description":"Form title"},"description":{"type":"string","maxLength":2000,"description":"Optional description"},"theme":{"type":"string","description":"Theme key (default sunset/Ember)"},"questions":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Screen objects; omit for an empty form. Conditional logic goes in each screen's `logic` (see the tool description)."},"thank_you_message":{"type":"string","maxLength":2000}},"required":["title"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"foxform_export_responses","description":"Export all responses for a form as CSV text (one row per response, columns = questions). Large exports are truncated — use foxform_list_responses with pagination for very large datasets.\n\nArgs:\n  - form_id (string)\n\nReturns: raw CSV text.","write_action":false,"price_micros":0,"input_schema":{"type":"object","properties":{"form_id":{"type":"string","minLength":1,"description":"Form ID"}},"required":["form_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"foxform_get_form","description":"Fetch a single form by ID, including its full question list, per-screen conditional logic and settings.\n\nArgs:\n  - form_id (string): the form's ID (from foxform_list_forms)\n  - response_format ('markdown' | 'json')\n\nReturns { form, public_url }: the full form object (id, title, description, slug, status, theme, questions[], thank_you_message, timestamps) plus `public_url` — the public link (`https://forms.foxform.app/<slug>`), which is live only when status is 'published'. Use `public_url` verbatim; never build the URL yourself (the public domain is forms.foxform.app, not foxform.app).\nEach screen in `questions[]` carries its own `logic` (branching / conditional display) and, for choice screens, `choices[]`/`images[]` with their `points` and `value`. The markdown output summarises every rule; use response_format 'json' to get the exact stored objects (that's the shape foxform_update_form expects back).\n\nCONDITIONAL LOGIC (branching), per screen — stored in `questions[].logic`:\n\n  logic.conditionalNavigationV2 = {\n    enabled: true,\n    groups: [                       // groups are OR-joined; FIRST matching group wins\n      {\n        id: \"grp-1\",\n        conditions: [               // conditions inside a group are AND-joined\n          { id: \"cond-1\", left: \"{{quer_testar}}\", operator: \"equal_to\", right: \"Ainda não\" }\n        ],\n        then: { type: \"specific_screen\", targetScreenId: \"s-motivos\" }\n      }\n    ]\n  }\n\n- `then.type`: 'next_screen' | 'previous_screen' | 'specific_screen' (needs targetScreenId = another screen's `id`) | 'end_form'.\n  Add `then.url` (+ optional `openNewTab`) to redirect to an external URL instead.\n- `operator`: 'equal_to' | 'not_equal_to' | 'greater_than' | 'greater_or_equal_than' | 'less_than' | 'less_or_equal_than' | 'contains'.\n- `left`/`right` are EXPRESSION strings: a literal (\"10\", \"Ainda não\"), a variable (\"{{score}}\", \"{{minha_var}}\" = the screen's `variableName`), or arithmetic (\"calc({{peso}}/(({{altura}}/100)*({{altura}}/100)))\").\n- Comparing an ANSWER: use `left: \"{{<variableName of the deciding screen>}}\"` and `right` = the option's `label` OR its `value` (both match).\n- `{{score}}` is the running sum of `points` on the options picked so far (`choices[].points`, `images[].points`) — that is how score-based branching works.\n- A navigation group with no conditions NEVER matches. `enabled: false` stores the rules but disables them.\n- Screen-level conditional display uses the same group shape: `logic.display = { enabled: true, groups: [...], showAfterSeconds?: n }` (`then` is ignored — THEN means \"show\").\n- Other logic keys: `logic.autoAdvance = { enabled, delaySeconds? }`, `logic.navigationBehavior = { onButtonClick?, onAutoAdvance?, targetScreenId? }`.\n- `logic.conditionalNavigation` (legacy, pre-DEVF-161) is still read and migrated on load — don't author new rules with it.\n\nUnknown fields are REJECTED (they used to be stored and silently ignored): `logic` as an array, or `rules`/`branching`/`conditions`/`goto`/`jump`/`nextScreen` anywhere, are not read by any renderer.","write_action":false,"price_micros":0,"input_schema":{"type":"object","properties":{"form_id":{"type":"string","minLength":1,"description":"Form ID"},"response_format":{"type":"string","enum":["markdown","json"],"default":"markdown","description":"Output format: 'markdown' (human-readable) or 'json' (machine-readable)"}},"required":["form_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"foxform_get_form_analytics","description":"Aggregated analytics for a form: overview KPIs (total responses, form views, response rate, avg response time), the responses-over-time timeline, and per-question stats.\n\nArgs:\n  - form_id (string)\n  - response_format ('markdown' | 'json')\n\nReturns: { overview, timeline: [{date,count}], questions: [...], response_time_distribution?, views_timeline? }.\nNote: form_views / response_rate are null until the form has tracked views (forward-only).","write_action":false,"price_micros":0,"input_schema":{"type":"object","properties":{"form_id":{"type":"string","minLength":1,"description":"Form ID"},"response_format":{"type":"string","enum":["markdown","json"],"default":"markdown","description":"Output format: 'markdown' (human-readable) or 'json' (machine-readable)"}},"required":["form_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"foxform_get_response","description":"Fetch one response by its ID (from foxform_list_responses).\n\nArgs:\n  - response_id (string)\n  - response_format ('markdown' | 'json')\n\nReturns the full response object (id, form_id, answers, submitted_at, metadata).","write_action":false,"price_micros":0,"input_schema":{"type":"object","properties":{"response_id":{"type":"string","minLength":1,"description":"Response ID"},"response_format":{"type":"string","enum":["markdown","json"],"default":"markdown","description":"Output format: 'markdown' (human-readable) or 'json' (machine-readable)"}},"required":["response_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"foxform_list_forms","description":"List the forms owned by the authenticated FoxForm account, newest first.\n\nArgs:\n  - page (number): 1-based page number (default 1)\n  - limit (number): page size, 1-100 (default 20)\n  - response_format ('markdown' | 'json'): output format (default markdown)\n\nReturns: { total, page, limit, count, forms: [{ id, title, status, slug, public_url, questions_count, updated_at }] }\nThe public link of a form is `public_url` (`https://forms.foxform.app/<slug>`) — always use this exact value, do NOT build the URL yourself (the public domain is forms.foxform.app, not foxform.app). `public_url` is only live when `status` is 'published'.\nUse this first to discover form IDs, then call foxform_get_form / foxform_get_form_analytics.","write_action":false,"price_micros":0,"input_schema":{"type":"object","properties":{"page":{"type":"integer","minimum":1,"default":1,"description":"1-based page number"},"limit":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Page size (1-100)"},"response_format":{"type":"string","enum":["markdown","json"],"default":"markdown","description":"Output format: 'markdown' (human-readable) or 'json' (machine-readable)"}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"foxform_list_responses","description":"List submitted responses for a form, newest first.\n\nArgs:\n  - form_id (string): the form's ID\n  - page (number): 1-based page (default 1)\n  - limit (number): page size, 1-100 (default 20)\n  - response_format ('markdown' | 'json')\n\nReturns: { total, page, limit, count, responses: [{ id, submitted_at, answers }] }.\nFor aggregate metrics use foxform_get_form_analytics; for a full dump use foxform_export_responses.","write_action":false,"price_micros":0,"input_schema":{"type":"object","properties":{"form_id":{"type":"string","minLength":1,"description":"Form ID"},"page":{"type":"integer","minimum":1,"default":1},"limit":{"type":"integer","minimum":1,"maximum":100,"default":20},"response_format":{"type":"string","enum":["markdown","json"],"default":"markdown","description":"Output format: 'markdown' (human-readable) or 'json' (machine-readable)"}},"required":["form_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"foxform_publish_form","description":"Publish a form so it's live at its public URL and can accept responses. Requires a WRITE-scoped API key.\n\nArgs:\n  - form_id (string)\n\nReturns: { form, public_url } — the published form (status 'published') and its live public link `https://forms.foxform.app/<slug>`. When sharing the link with the user, use `public_url` EXACTLY as returned; do NOT build the URL yourself (the public domain is forms.foxform.app, NOT foxform.app or the API/app domain). (May fail with a plan-limit error on Free accounts.)","write_action":true,"price_micros":0,"input_schema":{"type":"object","properties":{"form_id":{"type":"string","minLength":1,"description":"Form ID"}},"required":["form_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"foxform_unpublish_form","description":"Unpublish a form (takes it offline; stops accepting responses). Requires a WRITE-scoped API key.\n\nArgs:\n  - form_id (string)\n\nReturns: a confirmation message.","write_action":false,"price_micros":0,"input_schema":{"type":"object","properties":{"form_id":{"type":"string","minLength":1,"description":"Form ID"}},"required":["form_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"foxform_update_form","description":"Update an existing form's fields, including each screen's conditional logic (branching). Requires a WRITE-scoped API key. Only the fields you pass are changed.\n\nArgs:\n  - form_id (string): the form to update (required)\n  - title (string, optional)\n  - description (string, optional)\n  - theme (string, optional)\n  - questions (array, optional): replaces the FULL screen list — there is no per-screen patch. To add logic to one screen: call foxform_get_form with response_format 'json', edit that screen's `logic`, and send the whole array back.\n  - thank_you_message (string, optional)\n\nReturns: { form } with the updated form.\n\nScreen fields are validated: unknown fields are REJECTED instead of being stored and ignored (the API accepts arbitrary keys but no renderer reads them), `then.targetScreenId` must be the id of a screen in the same payload, and `{{variables}}` that no screen exposes come back as warnings.\n\nCONDITIONAL LOGIC (branching), per screen — stored in `questions[].logic`:\n\n  logic.conditionalNavigationV2 = {\n    enabled: true,\n    groups: [                       // groups are OR-joined; FIRST matching group wins\n      {\n        id: \"grp-1\",\n        conditions: [               // conditions inside a group are AND-joined\n          { id: \"cond-1\", left: \"{{quer_testar}}\", operator: \"equal_to\", right: \"Ainda não\" }\n        ],\n        then: { type: \"specific_screen\", targetScreenId: \"s-motivos\" }\n      }\n    ]\n  }\n\n- `then.type`: 'next_screen' | 'previous_screen' | 'specific_screen' (needs targetScreenId = another screen's `id`) | 'end_form'.\n  Add `then.url` (+ optional `openNewTab`) to redirect to an external URL instead.\n- `operator`: 'equal_to' | 'not_equal_to' | 'greater_than' | 'greater_or_equal_than' | 'less_than' | 'less_or_equal_than' | 'contains'.\n- `left`/`right` are EXPRESSION strings: a literal (\"10\", \"Ainda não\"), a variable (\"{{score}}\", \"{{minha_var}}\" = the screen's `variableName`), or arithmetic (\"calc({{peso}}/(({{altura}}/100)*({{altura}}/100)))\").\n- Comparing an ANSWER: use `left: \"{{<variableName of the deciding screen>}}\"` and `right` = the option's `label` OR its `value` (both match).\n- `{{score}}` is the running sum of `points` on the options picked so far (`choices[].points`, `images[].points`) — that is how score-based branching works.\n- A navigation group with no conditions NEVER matches. `enabled: false` stores the rules but disables them.\n- Screen-level conditional display uses the same group shape: `logic.display = { enabled: true, groups: [...], showAfterSeconds?: n }` (`then` is ignored — THEN means \"show\").\n- Other logic keys: `logic.autoAdvance = { enabled, delaySeconds? }`, `logic.navigationBehavior = { onButtonClick?, onAutoAdvance?, targetScreenId? }`.\n- `logic.conditionalNavigation` (legacy, pre-DEVF-161) is still read and migrated on load — don't author new rules with it.\n\nUnknown fields are REJECTED (they used to be stored and silently ignored): `logic` as an array, or `rules`/`branching`/`conditions`/`goto`/`jump`/`nextScreen` anywhere, are not read by any renderer.","write_action":true,"price_micros":0,"input_schema":{"type":"object","properties":{"form_id":{"type":"string","minLength":1,"description":"Form ID"},"title":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string","maxLength":2000},"theme":{"type":"string"},"questions":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Replaces the full screen list. Conditional logic goes in each screen's `logic` (see the tool description)."},"thank_you_message":{"type":"string","maxLength":2000}},"required":["form_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}],"scan":{"score":61,"grade":"C","scanned_at":"2026-09-20T20:59:05.216Z","report":{"scannerVersion":"0.1.9","scannedAt":"2026-09-20T20:59:05.218Z","components":{"code":{"score":-1,"max":25,"notes":["remote-only server, no package to scan"]},"reliability":{"score":20,"max":20,"notes":["remote reachable in 1094ms"]},"poisoning":{"score":13,"max":15,"notes":["10 tool descriptions checked"]},"auth":{"score":8,"max":15,"notes":["API key sent as a header"]},"maintenance":{"score":3,"max":15,"notes":["no repository listed"]},"identity":{"score":2,"max":10,"notes":["no repository or website to verify"]}},"findings":[{"id":"poison.long-description","severity":"low","component":"poisoning","title":"Unusually long tool description (over 2,000 characters)","evidence":"tool foxform_get_form: …Fetch a single form by ID, including its full question list, per-screen conditional logic and settings. Args: - form_id (string): the form's ID (from foxform_list_forms) - response_format ('markdown' | 'json') Returns { form, public_url }: the full form object (id, title, description, slug, status, theme, questions[], thank_you_message, timestamps) plus `public_url` — the public link (`https://forms.foxform.app/<slug>`), which is live only when status is 'published'. Use `public_url` verbatim; never build the URL yourself (the public domain is forms.foxform.app, not foxform.app). Each screen in `questions[]` carries its own `logic` (branching / conditional display) and, for choice screens, `choices[]`/`images[]` with their `points` and `value`. The markdown output summarises every rule; use response_format 'json' to get the exact stored objects (that's the shape foxform_update_form expects back). CONDITIONAL LOGIC (branching), per screen — stored in `questions[].logic`: logic.conditionalNavigationV2 = { enabled: true, groups: [ // groups are OR-joined; FIRST matching group wins { id: \"grp-1\", conditions: [ // conditions inside a group are AND-joined { id: \"cond-1\", left: \"{{quer_testar}}\", operator: \"equal_to\", right: \"Ainda não\" } ], then: { type: \"specific_screen\", targetScreenId: \"s-motivos\" } } ] } - `then.type`: 'next_screen' | 'previous_screen' | 'specific_screen' (needs targetScreenId = another screen's `id`) | 'end_form'. Add `then.url` (+ optional `openNewTab`) to redirect to an external URL instead. - `operator`: 'equal_to' | 'not_equal_to' | 'greater_than' | 'greater_or_equal_than' | 'less_than' | 'less_or_equal_than' | 'contains'. - `left`/`right` are EXPRESSION strings: a literal (\"10\", \"Ainda não\"), a variable (\"{{score}}\", \"{{minha_var}}\" = the screen's `variableName`), or arithmetic (\"calc({{peso}}/(({{altura}}/100)*({{altura}}/100)))\"). - Comparing an ANSWER: use `left: \"{{<variableName of the deciding screen>}}\"` and `right` = the option's `label` OR its `value` (both match). - `{{score}}` is the running sum of `points` on the options picked so far (`choices[].points`, `images[].points`) — that is how score-based branching works. - A navigation group with no conditions NEVER matches. `enabled: false` stores the rules but disables them. - Screen-level conditional display uses the same group shape: `logic.display = { enabled: true, groups: [...], showAfterSeconds?: n }` (`then` is ignored — THEN means \"show\"). - Other logic keys: `logic.autoAdvance = { enabled, delaySeconds? }`, `logic.navigationBehavior = { onButtonClick?, onAutoAdvance?, targetScreenId? }`. - `logic.conditionalNavigation` (legacy, pre-DEVF-161) is still read and migrated on load — don't author new rules with it. Unknown fields are REJECTED (they used to be stored and silently ignored): `logic` as an array, or `rules`/`branching`/`conditions`/`goto`/`jump`/`nextScreen` anywhere, are not read by any renderer.…"},{"id":"maint.no-repo","severity":"low","component":"maintenance","title":"No source repository listed"}],"inputs":{"probes":[{"url":"https://mcp.foxform.app/mcp","reachable":true,"authRequired":false,"latencyMs":1094,"serverInfo":{"name":"foxform-mcp-server","version":"1.0.0"}}],"packages":[],"repo":{"found":false},"icon":{"url":"https://foxform.app/apple-touch-icon.png","source":"site","width":180,"height":180},"presence":{"stars":null,"forks":null,"downloadsWeek":null,"license":null,"lastPushAt":null,"score":8}}}},"grade_history":[],"reviews":[]}