{
  "name": "ServiceM8 Forms — MCP Server",
  "nodes": [
    {
      "id": "trigger-mcp",
      "name": "MCP Server Trigger",
      "type": "@n8n/n8n-nodes-langchain.mcpTrigger",
      "typeVersion": 2,
      "position": [240, 380],
      "parameters": { "path": "servicem8-forms" },
      "webhookId": "servicem8-forms-mcp"
    },

    {
      "id": "tool-list-forms",
      "name": "list_forms",
      "type": "n8n-nodes-base.httpRequestTool",
      "typeVersion": 4.4,
      "position": [620, 60],
      "parameters": {
        "toolDescription": "Lists every form defined in the connected ServiceM8 account. Returns an array of {uuid, name, badge_name, document_template_uuid, active, edit_date}. Use this to see what forms already exist before creating a new one (avoid duplicates) or to find a form's UUID.",
        "method": "GET",
        "url": "https://api.servicem8.com/api_1.0/form.json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            { "name": "Accept", "value": "application/json" }
          ]
        },
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": { "name": "ServiceM8 API Key" }
      }
    },

    {
      "id": "tool-list-jobs",
      "name": "list_recent_jobs",
      "type": "n8n-nodes-base.httpRequestTool",
      "typeVersion": 4.4,
      "position": [620, 220],
      "parameters": {
        "toolDescription": "Returns the 10 most recently edited jobs in the ServiceM8 account, sorted newest first. Use to pick a target job UUID when you need to attach a form or test a submission. Returns array of {uuid, generated_job_id, status, job_address, edit_date}.",
        "method": "GET",
        "url": "https://api.servicem8.com/api_1.0/job.json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            { "name": "$top", "value": "10" },
            { "name": "$orderby", "value": "edit_date desc" }
          ]
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            { "name": "Accept", "value": "application/json" }
          ]
        },
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": { "name": "ServiceM8 API Key" }
      }
    },

    {
      "id": "tool-get-form",
      "name": "get_form",
      "type": "n8n-nodes-base.httpRequestTool",
      "typeVersion": 4.4,
      "position": [620, 300],
      "parameters": {
        "toolDescription": "Retrieves one Form record by UUID. Returns {uuid, name, badge_name, badge_mandatory_state, can_be_used_independently, document_template_uuid, template_fields[], active, edit_date}. Note this does NOT return the form's questions — call list_form_fields with the same form_uuid to get them. Use this when you have a form_uuid (from list_forms) and need the metadata + the up-to-10 template_fields (small text variables that go on every render).",
        "method": "GET",
        "url": "=https://api.servicem8.com/api_1.0/form/{{ $fromAI('form_uuid', 'UUID of the form to retrieve', 'string') }}.json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            { "name": "Accept", "value": "application/json" }
          ]
        },
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": { "name": "ServiceM8 API Key" }
      }
    },

    {
      "id": "tool-list-form-fields",
      "name": "list_form_fields",
      "type": "n8n-nodes-base.httpRequestTool",
      "typeVersion": 4.4,
      "position": [620, 380],
      "parameters": {
        "toolDescription": "Reads back every FormField (question) belonging to a given form. Returns array of {uuid, form_uuid, name, field_data_json, sort_order, active, edit_date}. The field_data_json is a JSON STRING you must parse to get {fieldType, mandatory, additionalDetails?, choices?, conditions?, conditionMethod?}. Use this BEFORE calling get_docx_build_recipe on an existing form so you know its question list. Filters server-side by form_uuid eq '...'.",
        "method": "GET",
        "url": "https://api.servicem8.com/api_1.0/formfield.json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            { "name": "$filter", "value": "=form_uuid eq '{{ $fromAI('form_uuid', 'UUID of the form whose questions you want', 'string') }}'" },
            { "name": "$orderby", "value": "sort_order asc" }
          ]
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            { "name": "Accept", "value": "application/json" }
          ]
        },
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": { "name": "ServiceM8 API Key" }
      }
    },

    {
      "id": "tool-create-form",
      "name": "create_form",
      "type": "n8n-nodes-base.httpRequestTool",
      "typeVersion": 4.4,
      "position": [620, 380],
      "parameters": {
        "toolDescription": "Creates a new ServiceM8 Form record (just the form metadata). After this returns a form_uuid (in response header x-record-uuid), call add_form_field once per question to populate it. Pass formName (string) and badge (string ≤12 chars, shown as a tag in the staff app).",
        "method": "POST",
        "url": "https://api.servicem8.com/api_1.0/form.json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            { "name": "Accept", "value": "application/json" },
            { "name": "Content-Type", "value": "application/json" }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"name\": \"{{ $fromAI('formName', 'Display name for the form, shown to staff', 'string') }}\",\n  \"badge_name\": \"{{ $fromAI('badge', 'Short tag (≤12 chars) shown next to the form on a job', 'string') }}\",\n  \"can_be_used_independently\": \"0\",\n  \"badge_mandatory_state\": \"0\"\n}",
        "options": { "response": { "response": { "fullResponse": true } } }
      },
      "credentials": {
        "httpHeaderAuth": { "name": "ServiceM8 API Key" }
      }
    },

    {
      "id": "tool-add-field",
      "name": "add_form_field",
      "type": "n8n-nodes-base.httpRequestTool",
      "typeVersion": 4.4,
      "position": [620, 540],
      "parameters": {
        "toolDescription": "Adds one question to an existing ServiceM8 form. Call once per question. Returns the new field UUID in response header x-record-uuid (you'll need it if other fields have conditional branching that references this question). Inputs: form_uuid (from create_form), question_name (string), field_type (one of: 'Text', 'Text (Multi-Line)', 'Number', 'Date', 'Multiple Choice', 'Multiple Choice (Multi-Answer)', 'Signature', 'Photo'), sort_order (integer, 1-based), and field_data_json — a JSON STRING (not object) of {fieldType, mandatory, additionalDetails?, choices?, conditions?, conditionMethod?}. For Multi-Answer MC include choices as array of strings. For conditional fields, conditions = [{question: <other_field_uuid>, operator: 'EQ', value: <choice string>}].",
        "method": "POST",
        "url": "https://api.servicem8.com/api_1.0/formfield.json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            { "name": "Accept", "value": "application/json" },
            { "name": "Content-Type", "value": "application/json" }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"form_uuid\": \"{{ $fromAI('form_uuid', 'UUID of the form returned by create_form', 'string') }}\",\n  \"name\": \"{{ $fromAI('question_name', 'The question label staff will see', 'string') }}\",\n  \"field_data_json\": {{ JSON.stringify($fromAI('field_data_json', 'JSON STRING containing fieldType, mandatory, choices, conditions etc', 'string')) }},\n  \"sort_order\": {{ $fromAI('sort_order', 'Position of this question in the form (1-based)', 'number') }}\n}",
        "options": { "response": { "response": { "fullResponse": true } } }
      },
      "credentials": {
        "httpHeaderAuth": { "name": "ServiceM8 API Key" }
      }
    },

    {
      "id": "tool-docx-recipe",
      "name": "get_docx_build_recipe",
      "type": "@n8n/n8n-nodes-langchain.toolCode",
      "typeVersion": 1.3,
      "position": [620, 720],
      "parameters": {
        "name": "get_docx_build_recipe",
        "description": "Returns a complete markdown 'recipe' explaining EXACTLY how to build the .docx template that pairs with this ServiceM8 form. The recipe contains: (a) the slug-resolved MERGEFIELD code for every question, (b) the special IF/Wingdings checkbox OOXML pattern for multi-answer choices, (c) the built-in field codes (job.*, vendor.*, calculation.*) for the header/footer, (d) step-by-step build instructions. After calling this, YOU (the AI) should generate the .docx file using your own code-execution capabilities (e.g. python-docx, the docx npm package, or raw OOXML + zip). The user must then upload the file via ServiceM8 → Settings → Document Templates (the API doesn't expose template upload).",
        "language": "javaScript",
        "specifyInputSchema": true,
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"required\": [\"name\", \"questions\"],\n  \"properties\": {\n    \"name\": { \"type\": \"string\", \"description\": \"Form name; appears in the document title\" },\n    \"brandColor\": { \"type\": \"string\", \"description\": \"Hex without # (e.g. 1F2937). Optional. Defaults to neutral slate.\" },\n    \"questions\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"required\": [\"name\", \"fieldType\"],\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"fieldType\": { \"type\": \"string\", \"enum\": [\"Text\", \"Text (Multi-Line)\", \"Number\", \"Date\", \"Multiple Choice\", \"Multiple Choice (Multi-Answer)\", \"Signature\", \"Photo\"] },\n          \"choices\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n        }\n      }\n    }\n  }\n}",
        "jsCode": "// === get_docx_build_recipe ===\n// Pure spec-to-recipe transform. Returns markdown explaining how to build\n// the matching .docx. The CLIENT (Claude Desktop, etc.) does the actual\n// DOCX construction using its own tools (python-docx, code execution, etc.).\n//\n// This keeps the n8n side simple and lets the client adapt styling per request.\n\nconst spec = (typeof query === 'object' && query) ? query : (typeof query === 'string' ? JSON.parse(query) : {});\nconst brandColor = (spec.brandColor || '1F2937').replace('#','').toUpperCase();\n\n// ServiceM8 slug rules deduced from real templates (SWMS, Confined Space):\n//   - lowercase\n//   - spaces → single underscore\n//   - special chars (`:`, `/`, `&`) → double underscore\n//   - collapse runs of 3+ underscores\nfunction slug(s) {\n  const out = [];\n  for (const ch of (s || '').toString()) {\n    const c = ch.toLowerCase();\n    if (/[a-z0-9]/.test(c)) out.push(c);\n    else if (c === ' ') out.push('_');\n    else out.push('__');\n  }\n  return out.join('').replace(/_{3,}/g, '__').replace(/^_+|_+$/g, '');\n}\n\n// Build the per-question code table\nconst rows = (spec.questions || []).map((q, i) => {\n  const base = 'form_' + slug(q.name);\n  const ft = q.fieldType || 'Text';\n  let codes = [];\n  let pattern = '';\n  if (ft === 'Multiple Choice (Multi-Answer)' && Array.isArray(q.choices)) {\n    codes = q.choices.map(c => base + '_' + slug(c));\n    pattern = 'IF/Wingdings checkbox per choice';\n  } else if (ft === 'Photo' || ft === 'Signature') {\n    codes = ['image_form_' + slug(q.name)];\n    pattern = 'Image embed';\n  } else {\n    codes = [base];\n    pattern = 'Plain MERGEFIELD';\n  }\n  return { idx: i+1, name: q.name, fieldType: ft, pattern: pattern, codes: codes };\n});\n\n// Format markdown\nlet md = '';\nmd += '# DOCX Build Recipe — ' + (spec.name || 'ServiceM8 Form') + '\\n\\n';\nmd += '**Brand colour:** `#' + brandColor + '`\\n';\nmd += '**Question count:** ' + rows.length + '\\n\\n';\nmd += '---\\n\\n';\n\nmd += '## 1. Field Codes to Emit\\n\\n';\nmd += 'Each row below is a single question and the MERGEFIELD code(s) that must appear in the DOCX. Use these codes verbatim — ServiceM8 derives them from the question name using the slug rules below, and any mismatch will render blank.\\n\\n';\nmd += '| # | Question | Type | Pattern | MERGEFIELD code(s) |\\n';\nmd += '|---|---|---|---|---|\\n';\nfor (const r of rows) {\n  md += '| ' + r.idx + ' | ' + r.name + ' | ' + r.fieldType + ' | ' + r.pattern + ' | `' + r.codes.join('`, `') + '` |\\n';\n}\nmd += '\\n';\n\nmd += '## 2. Built-In Field Codes (header / footer)\\n\\n';\nmd += 'These are auto-populated by ServiceM8 at render time — no need to add corresponding form questions:\\n\\n';\nmd += '| Code | Substitutes |\\n|---|---|\\n';\nmd += '| `job.generated_job_id` | Job number (e.g. \"s319t\") |\\n';\nmd += '| `job.contact_first` / `job.contact_last` | Customer name |\\n';\nmd += '| `job.job_address` | Site address |\\n';\nmd += '| `vendor.email` | Your business email |\\n';\nmd += '| `calculation.todays_date` | Render date |\\n';\nmd += '| `calculation.current_user_fullname` | Staff who rendered |\\n\\n';\n\nmd += '## 3. The Slug Rules (verified)\\n\\n';\nmd += '```\\n';\nmd += 'lowercase, then per-character:\\n';\nmd += '  alphanumeric → keep\\n';\nmd += '  space        → \"_\"\\n';\nmd += '  other (\":\", \"/\", \"&\", \"-\", etc.) → \"__\"\\n';\nmd += 'finally: collapse runs of 3+ underscores down to \"__\", trim leading/trailing\\n';\nmd += '```\\n\\n';\nmd += 'Examples observed in real templates:\\n';\nmd += '- `Hot` → `hot`\\n';\nmd += '- `PPE Required: Eye Protection` → `ppe_required__eye_protection`\\n';\nmd += '- `Codes of Practice & Standards Consulted` → `codes_of_practice__standards_consulted`\\n\\n';\n\nmd += '## 4. OOXML Pattern: Plain MERGEFIELD\\n\\n';\nmd += 'For `Text`, `Number`, `Date`, single-choice MC, and built-in record fields. In Word: `Insert → Quick Parts → Field → MergeField → <code>`. In raw `word/document.xml`:\\n\\n';\nmd += '```xml\\n';\nmd += '<w:r><w:fldChar w:fldCharType=\"begin\"/></w:r>\\n';\nmd += '<w:r><w:instrText xml:space=\"preserve\"> MERGEFIELD form_xxx </w:instrText></w:r>\\n';\nmd += '<w:r><w:fldChar w:fldCharType=\"separate\"/></w:r>\\n';\nmd += '<w:r><w:t>«form_xxx»</w:t></w:r>\\n';\nmd += '<w:r><w:fldChar w:fldCharType=\"end\"/></w:r>\\n';\nmd += '```\\n\\n';\n\nmd += '## 5. OOXML Pattern: IF/Wingdings Checkbox (Multi-Answer Choice)\\n\\n';\nmd += 'A multi-answer MC renders as **one cell per choice**. The MERGEFIELD substitutes `\"Yes\"` if ticked, anything else if not. Wrap in a Word IF field that swaps Wingdings glyphs:\\n\\n';\nmd += '```\\n';\nmd += '{ IF \"{ MERGEFIELD form_xxx_choice }\" = \"Yes\"\\n';\nmd += '       \"☑\" (Wingdings F0FE — filled checkbox)\\n';\nmd += '       \"☐\" (Wingdings F06F — empty checkbox)\\n';\nmd += '   \\\\* MERGEFORMAT }\\n';\nmd += '```\\n\\n';\nmd += 'Raw OOXML — emit this 11-run sequence per choice cell:\\n\\n';\nmd += '```xml\\n';\nmd += '<w:r><w:fldChar w:fldCharType=\"begin\"/></w:r>\\n';\nmd += '<w:r><w:instrText xml:space=\"preserve\"> IF \"</w:instrText></w:r>\\n';\nmd += '  <w:r><w:fldChar w:fldCharType=\"begin\"/></w:r>\\n';\nmd += '  <w:r><w:instrText xml:space=\"preserve\"> MERGEFIELD form_xxx_choice </w:instrText></w:r>\\n';\nmd += '  <w:r><w:fldChar w:fldCharType=\"separate\"/></w:r>\\n';\nmd += '  <w:r><w:t>«form_xxx_choice»</w:t></w:r>\\n';\nmd += '  <w:r><w:fldChar w:fldCharType=\"end\"/></w:r>\\n';\nmd += '<w:r><w:instrText xml:space=\"preserve\">\" = \"Yes\" \"</w:instrText></w:r>\\n';\nmd += '<w:r><w:rPr><w:rFonts w:ascii=\"Wingdings\" w:hAnsi=\"Wingdings\"/></w:rPr><w:sym w:font=\"Wingdings\" w:char=\"F0FE\"/></w:r>\\n';\nmd += '<w:r><w:instrText xml:space=\"preserve\">\" \"</w:instrText></w:r>\\n';\nmd += '<w:r><w:rPr><w:rFonts w:ascii=\"Wingdings\" w:hAnsi=\"Wingdings\"/></w:rPr><w:sym w:font=\"Wingdings\" w:char=\"F06F\"/></w:r>\\n';\nmd += '<w:r><w:instrText xml:space=\"preserve\">\" \\\\\\\\* MERGEFORMAT</w:instrText></w:r>\\n';\nmd += '<w:r><w:fldChar w:fldCharType=\"end\"/></w:r>\\n';\nmd += '```\\n\\n';\n\nmd += '## 6. Bonus: What Word Fields Can Do (use sparingly)\\n\\n';\nmd += '- **Calc fields** for arithmetic with currency formatting:\\n';\nmd += '  `{ = { MERGEFIELD form_qty } * { MERGEFIELD form_rate } \\\\# \"$#,##0.00\" }`\\n';\nmd += '- **Coloured branches** with per-branch run formatting (red on \"Fail\", green on \"Pass\")\\n';\nmd += '- **Conditional sections** — wrap entire paragraphs in IF for show/hide\\n';\nmd += '- **Dot-namespace fields**: anything under `job.*`, `vendor.*`, `location.*`, `calculation.*`\\n\\n';\nmd += 'Inside an IF, both true and false branches can contain styled `<w:r>` runs with their own colour, bold, font, size — that is how the SWMS template colours its risk-rating cells.\\n\\n';\n\nmd += '## 7. Build Steps\\n\\n';\nmd += '1. **Pick a tool**: `python-docx` (Python) or `docx` (npm) — both let you drop into raw OOXML when needed. Avoid template libraries that use `{tag}` syntax (e.g. docxtemplater) — ServiceM8 does not text-substitute, it requires real Word fields.\\n';\nmd += '2. **Build the body** in this order:\\n';\nmd += '   - Header: title, then a line of built-in fields (job id, today, prepared by)\\n';\nmd += '   - Customer block: `job.contact_first`, `job.contact_last`, `job.job_address`\\n';\nmd += '   - One section per question using the codes from the table in section 1\\n';\nmd += '   - Footer: `vendor.email`\\n';\nmd += '3. **Save** as `' + slug(spec.name || 'form') + '_template.docx`.\\n';\nmd += '4. **Upload** via ServiceM8 → Settings → Document Templates → Upload Custom Template.\\n';\nmd += '5. **Link** the new template to the form (PATCH the form record\\'s `document_template_uuid`, or set it via the form\\'s settings page).\\n\\n';\n\nmd += '## 8. Verification\\n\\n';\nmd += 'Unzip the .docx and `grep MERGEFIELD word/document.xml`. The output should match this list exactly (any difference = blank fields at render time):\\n\\n';\nconst allCodes = rows.flatMap(r => r.codes);\nallCodes.push('job.generated_job_id', 'job.contact_first', 'job.contact_last', 'job.job_address', 'vendor.email', 'calculation.todays_date', 'calculation.current_user_fullname');\nmd += '```\\n';\nfor (const c of allCodes) md += 'MERGEFIELD ' + c + '\\n';\nmd += '```\\n';\n\nreturn md;\n"
      }
    },

    {
      "id": "sticky-readme",
      "name": "Sticky Note — Setup",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [-100, 60],
      "parameters": {
        "width": 300,
        "height": 580,
        "content": "## ServiceM8 Forms — MCP Server\n\n**1. Add the API credential**\nIn n8n → Credentials → New → **Header Auth**:\n- Name: `ServiceM8 API Key` (must match — every HTTP node references this exact name; rename to taste, but update all 6 HTTP nodes if you do)\n- Header Name: `X-API-Key`\n- Header Value: your ServiceM8 API key (`smk-...`)\n\nGet the key from ServiceM8 → Settings → Account → API Keys.\n\n**2. Activate the workflow**\nThe MCP Server Trigger exposes the workflow at:\n`{your-n8n-url}/mcp/servicem8-forms`\n\n**3. Connect Claude Desktop**\nAdd to `claude_desktop_config.json`:\n```\n\"servicem8-forms\": {\n  \"transport\": {\n    \"type\": \"http\",\n    \"url\": \"https://YOUR-N8N/mcp/servicem8-forms\"\n  }\n}\n```\n\n**4. Try it**\n> \"List my ServiceM8 forms\"\n> \"Create a JSA form with these 8 questions...\"\n> \"Generate the matching DOCX template\"\n\n## Tools exposed\n- `list_forms` — read all forms\n- `list_recent_jobs` — pick a target job\n- `get_form` — read one form record by UUID\n- `list_form_fields` — read every question on a given form\n- `create_form` — POST one form record\n- `add_form_field` — POST one question (call N times)\n- `get_docx_build_recipe` — return markdown instructions; the AI client builds the actual DOCX\n\n## What this does NOT do\n- Upload the DOCX (no API for it — manual web UI step)\n- Render the form on demand (`platform_produce_document` rejects templateType=Form)\n- Submit form responses (extend with a 6th tool calling /formresponse.json)\n\nFor the full feature set (calc fields, conditional sections, coloured priority banners, photos), see the Python reference implementation in this repo."
      }
    }
  ],

  "connections": {
    "list_forms":             { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] },
    "list_recent_jobs":       { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] },
    "get_form":               { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] },
    "list_form_fields":       { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] },
    "create_form":            { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] },
    "add_form_field":         { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] },
    "get_docx_build_recipe":  { "ai_tool": [[{ "node": "MCP Server Trigger", "type": "ai_tool", "index": 0 }]] }
  },

  "pinData": {},
  "settings": { "executionOrder": "v1" }
}
