@typeroll/mcp-server 0.33.2 → 0.35.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.
@@ -1,312 +1,145 @@
1
1
  ---
2
2
  name: tr-forms
3
- description: Use when the user wants to add a contact form, booking form, or any web form to their Typeroll site. Triggers on "lägg till formulär", "contact form", "add a form", "let visitors message us", "booking form", "formulär", or similar. Covers both creating the form definition and embedding the HTML widget on a page.
3
+ description: Use when the user wants to add a contact form, newsletter signup, booking form, or other web form to a Typeroll site.
4
4
  ---
5
5
 
6
6
  # Add a form to a Typeroll site
7
7
 
8
- Typeroll forms are server-backed: submissions go to
9
- `/api/forms/submit` (HMAC-signed, rate-limited, honeypot-protected)
10
- and are stored in Firestore. The user sees them in the portal's
11
- Submissions inbox. No third-party service needed.
8
+ Typeroll forms are server-backed. The platform renders the form shell, accepts
9
+ signed submissions, validates declared fields, stores submissions, and can run
10
+ admin-configured email or webhook actions.
12
11
 
13
- ## Preconditions
12
+ ## Choose the placement
14
13
 
15
- - Site exists and MCP is configured.
16
- - You know what fields the form needs.
14
+ - Block-mode page: add `{ type: 'core/form', data: { form_id } }`.
15
+ - HTML-mode page: add `<x-form id="form-id" />` to `html_content`.
17
16
 
18
- ## Forms 2.0 steps mode (template_capabilities_version 0.18.0)
17
+ Both are authoring references to the same renderer. Preview and static
18
+ generation expand them to the complete form HTML, signed token, honeypot,
19
+ initial step state, styles, and shared runtime. Never hand-write the `<form>`
20
+ shell or paste a token into page HTML.
19
21
 
20
- Prefer this over the hand-built core/html embed when the portal supports
21
- it. A form's `steps[]` are Block[] trees of `form/*` field blocks mixed
22
- with content blocks; place `{ type: 'core/form', data: { form_id } }` on
23
- the page and the build renders everything (token, honeypot, runtime,
24
- proof-of-work) — you never hand-write the <form> markup.
22
+ ## Create a simple form
25
23
 
26
- ```
27
- update_form form_id=ansokan patch={ steps: [
28
- { id: 'steg1', title: 'Om företaget', blocks: [
29
- { type: 'form/text', data: { name: 'foretag', label: 'Företag', required: true } },
30
- { type: 'form/email', data: { name: 'epost', label: 'E-post', required: true } } ] },
31
- { id: 'steg2', title: 'Detaljer', blocks: [
32
- { type: 'form/textarea', data: { name: 'meddelande', label: 'Meddelande' } },
33
- { type: 'form/consent', data: { name: 'gdpr', text: '<p>Jag godkänner …</p>' } } ] } ] }
34
- add_block target={kind:'page', id:'kontakt'} block={ type: 'core/form', data: { form_id: 'ansokan' } }
35
- ```
36
-
37
- Validation derives from the field blocks (required/pattern/min/max) —
38
- there is no separate field list to maintain. Partial submissions persist
39
- per step (TTL `partial_ttl_days`, default 30). Form-scoped CSS goes in
40
- `styles`; field look is themable via `--form-field-*` tokens in the
41
- site's custom_css. On older portals, use the legacy recipe below.
42
-
43
- **Simple forms don't need hand-built steps** (template_capabilities_version
44
- ≥ 0.29.0, `forms_steps_only` capability): steps are the ONLY stored model,
45
- and a plain `create_form` with `fields[]` is authoring sugar — the server
46
- converts the list to a single static step (read_form shows the resulting
47
- steps). Every form renders through the `core/form` block (styled inputs,
48
- client validation, token + honeypot all handled). So for any single-step
49
- form: `create_form` with `fields`, then
50
- `add_block { type: 'core/form', data: { form_id } }`. Reach for the
51
- raw-HTML embed (below) only on older portals or HTML-mode pages — and note
52
- it supports single-step forms only.
24
+ `fields[]` is authoring sugar for one static step:
53
25
 
54
- ## Recipe
55
-
56
- ### 1. Create the form definition
57
-
58
- ```
59
- create_form {
60
- "id": "kontakt",
61
- "name": "Kontaktformulär",
26
+ ```json
27
+ {
28
+ "id": "newsletter",
29
+ "name": "Newsletter",
62
30
  "fields": [
63
- {"name":"name", "label":"Namn", "type":"text", "required":true},
64
- {"name":"email", "label":"E-post", "type":"email", "required":true},
65
- {"name":"phone", "label":"Telefon", "type":"text"},
66
- {"name":"message", "label":"Meddelande", "type":"textarea", "required":true},
67
- {"name":"subject", "label":"Ämne", "type":"select",
68
- "options":["Prisförfrågan","Samarbete","Övrigt"]}
31
+ {"name":"email", "type":"email", "label":"E-postadress", "required":true}
69
32
  ],
70
- "success_message": "Tack! Vi återkommer inom 24 timmar."
33
+ "submit_text": "Prenumerera",
34
+ "success_message": "Tack! Du är anmäld."
71
35
  }
72
36
  ```
73
37
 
74
- There is no `recipient_email` field submissions land in the portal's
75
- Submissions inbox (`/app/sites/{siteId}/forms/{formId}/submissions`).
76
- Email notification is a form *action* (not yet wired platform-side);
77
- tell the customer to check the inbox.
38
+ Field names must match `[a-z][a-z0-9_-]*`. Labels may be localized. Supported
39
+ simple types include `text`, `email`, `tel`, `url`, `number`, `textarea`,
40
+ `select`, `radio`, `checkbox`, `hidden`, and `gdpr_consent`.
78
41
 
79
- **Field types:** `text`, `email`, `tel`, `url`, `number`, `textarea`,
80
- `select`, `radio`, `checkbox`.
42
+ ## Place the form
81
43
 
82
- **Field name rules:** Lowercase ASCII only: `[a-z][a-z0-9_-]*`.
83
- - `besokt` not `besökt` (`ö→o`)
84
- - `foretag` not `företag` (`ö→o`, `ä→a`)
85
- - `meddelande` not `Meddelande` (the `name` must be lowercase; `label` can be anything)
86
-
87
- ### 2. Get the signed embed token
88
-
89
- The submit endpoint only accepts requests carrying a platform-signed
90
- HMAC token. `create_form` returns it directly; you can also fetch it any
91
- time with:
44
+ On a block-mode page:
92
45
 
93
46
  ```
94
- read_form form_id="kontakt"
47
+ add_block target={kind:'page', id:'start'}
48
+ block={type:'core/form', data:{form_id:'newsletter'}}
95
49
  ```
96
50
 
97
- The response includes `submit_token` (put it in the hidden `_token`
98
- input) and `submit_url` (use it as the form's `action`). The token is
99
- stable — it only stops working if the platform rotates its signing
100
- secret — so baking it into static page HTML is correct. If
101
- `submit_token` comes back `null`, the server has no signing secret
102
- configured (dev setups); the form cannot accept submissions until that's
103
- fixed — tell the user instead of embedding a broken form.
104
-
105
- ### 3. Embed the form on a page
106
-
107
- A plain HTML form POST is the default and needs **no JavaScript**: the
108
- endpoint answers a normal form-encoded POST with a small confirmation
109
- page (the form's `success_message` + a link back to the page the
110
- visitor came from). Validation errors get the same treatment. The HTML:
51
+ On an HTML-mode page:
111
52
 
112
53
  ```html
113
- <section class="contact-section">
114
- <div class="contact-container">
115
- <h2>Kontakta oss</h2>
116
- <p>Fyll i formuläret så återkommer vi inom 24 timmar.</p>
117
-
118
- <form class="contact-form"
119
- action="SUBMIT_URL"
120
- method="POST">
121
- <!-- The signed token is the only hidden field the platform needs;
122
- it encodes org + site + form identity. -->
123
- <input type="hidden" name="_token" value="SIGNED_TOKEN">
124
- <!-- Honeypot — must stay empty, bots fill it -->
125
- <input type="text" name="_hp" style="display:none" tabindex="-1" autocomplete="off">
126
-
127
- <div class="form-group">
128
- <label for="name">Namn *</label>
129
- <input type="text" id="name" name="name" required>
130
- </div>
131
-
132
- <div class="form-group">
133
- <label for="email">E-post *</label>
134
- <input type="email" id="email" name="email" required>
135
- </div>
136
-
137
- <div class="form-group">
138
- <label for="message">Meddelande *</label>
139
- <textarea id="message" name="message" rows="5" required></textarea>
140
- </div>
141
-
142
- <div class="form-group">
143
- <label for="subject">Ämne</label>
144
- <select id="subject" name="subject">
145
- <option value="Prisförfrågan">Prisförfrågan</option>
146
- <option value="Samarbete">Samarbete</option>
147
- <option value="Övrigt">Övrigt</option>
148
- </select>
149
- </div>
150
-
151
- <button type="submit" class="btn-primary">Skicka meddelande</button>
152
- </form>
153
- </div>
54
+ <section class="newsletter-signup">
55
+ <h2>Få våra nyheter</h2>
56
+ <x-form id="newsletter" />
154
57
  </section>
155
-
156
- <style>
157
- .contact-section{padding:4rem 2rem}
158
- .contact-container{max-width:600px;margin:0 auto}
159
- .form-group{margin-bottom:1.5rem}
160
- .form-group label{display:block;font-weight:600;margin-bottom:0.4rem;font-size:0.9rem}
161
- .form-group input,.form-group textarea,.form-group select{
162
- width:100%;padding:0.75rem 1rem;border:1px solid var(--color-surface);
163
- border-radius:0.375rem;font-family:inherit;font-size:1rem;
164
- background:var(--color-surface);color:var(--color-text)
165
- }
166
- .form-group textarea{resize:vertical}
167
- .btn-primary{
168
- background:var(--color-primary);color:#fff;border:none;
169
- padding:0.875rem 2rem;border-radius:0.375rem;font-size:1rem;
170
- font-weight:600;cursor:pointer;width:100%
171
- }
172
- .btn-primary:hover{opacity:0.9}
173
- </style>
174
58
  ```
175
59
 
176
- **Replace:**
177
- - `SUBMIT_URL` `submit_url` from `read_form` / `create_form`
178
- - `SIGNED_TOKEN` → `submit_token` from the same response
60
+ `<x-form>` is not a browser-side shortcode. It is replaced during server
61
+ preview/build, so it also supports multi-step forms and never needs inline JS.
179
62
 
180
- ### 4. Inline feedback (optional — requires user action)
63
+ ## Multi-step form
181
64
 
182
- **The page sanitizer strips inline `<script>` from page and partial
183
- HTML**, so you cannot ship fetch-based submit handling yourself — a
184
- `<script>` you put in `html_content` is silently removed. The plain
185
- POST above is the reliable path; use it.
65
+ For a funnel, write `steps[]` directly. Each step is a block tree containing
66
+ `form/*` field blocks and optional content blocks:
186
67
 
187
- If the customer wants inline feedback without a page reload, the script
188
- must go into the site's `scripts_body_end` setting, which only a human
189
- can edit (Settings → Custom code — it's deliberately excluded from the
190
- AI tool surface). Hand them this snippet for that box; it intercepts the
191
- form and POSTs the JSON shape `{ token, data }`:
192
-
193
- ```html
194
- <script>
195
- (function(){
196
- const form = document.querySelector('.contact-form');
197
- if(!form) return;
198
- form.addEventListener('submit', async (e) => {
199
- e.preventDefault();
200
- const btn = form.querySelector('[type=submit]');
201
- btn.disabled = true;
202
- btn.textContent = 'Skickar…';
203
- // The endpoint's fetch contract is JSON: { token, data }.
204
- const fd = new FormData(form);
205
- const token = fd.get('_token');
206
- const data = {};
207
- fd.forEach((v, k) => { if (k !== '_token') data[k] = v; });
208
- try {
209
- const res = await fetch(form.action, {
210
- method: 'POST',
211
- headers: { 'Content-Type': 'application/json' },
212
- body: JSON.stringify({ token, data }),
213
- });
214
- const json = await res.json();
215
- if (res.ok && json.success) {
216
- form.innerHTML = '<p class="form-success">' + (json.message || 'Tack!') + '</p>';
217
- } else {
218
- btn.disabled = false;
219
- btn.textContent = 'Skicka meddelande';
220
- alert('Något gick fel: ' + ((json.errors && json.errors.join(', ')) || json.error || 'okänt fel'));
221
- }
222
- } catch {
223
- btn.disabled = false;
224
- btn.textContent = 'Skicka meddelande';
225
- alert('Nätverksfel — försök igen.');
226
- }
227
- });
228
- })();
229
- </script>
68
+ ```
69
+ update_form form_id=ansokan patch={steps:[
70
+ {id:'company', title:'Företag', blocks:[
71
+ {type:'form/text', data:{name:'company', label:'Företag', required:true}},
72
+ {type:'form/email', data:{name:'email', label:'E-post', required:true}}
73
+ ]},
74
+ {id:'details', title:'Detaljer', blocks:[
75
+ {type:'form/textarea', data:{name:'message', label:'Meddelande'}},
76
+ {type:'form/consent', data:{name:'consent', text:'<p>Jag godkänner …</p>'}}
77
+ ]}
78
+ ]}
230
79
  ```
231
80
 
232
- ### 5. Update the contact page with the form HTML
81
+ Submissions accumulate in one partial record and become complete on the final
82
+ step. Abandoned partials use `partial_ttl_days` (default 30).
233
83
 
234
- ```
235
- update_page page_id="kontakt" patch={
236
- "html_content": "<full page HTML including the form section>"
237
- }
238
- ```
84
+ ## Storage and integrations
239
85
 
240
- ### 6. Verify
86
+ Completed submissions appear in Forms → Submissions. Admins can configure
87
+ actions in the form editor:
241
88
 
242
- ```
243
- read_form form_id="kontakt"
244
- list_forms
245
- ```
89
+ - Email notification or autoresponder through the site's email connector.
90
+ - Generic webhook to a public HTTPS endpoint. The admin chooses an explicit
91
+ field allowlist and signing secret. Typeroll signs the exact request body in
92
+ `X-Typeroll-Signature`, sends an idempotency key, retries transient failures,
93
+ and stores delivery status.
246
94
 
247
- Confirm the form appears and fields match what you embedded.
95
+ Actions are deliberately excluded from MCP/API-key writes and reads because
96
+ they can exfiltrate submitted data. Direct the user to the portal form editor
97
+ to configure them.
248
98
 
249
- ### 7. Deploy
99
+ ## Verify
250
100
 
251
- ```
252
- trigger_deploy
253
- get_deploy_status job_id=<id>
254
- ```
101
+ 1. `read_form form_id="newsletter"` and confirm the steps/fields.
102
+ 2. Preview the page and confirm the authoring reference has expanded to a form
103
+ with `data-tr-form-el`, a signed token, and the platform runtime.
104
+ 3. Submit a test entry and confirm it appears in Forms → Submissions.
105
+ 4. If a webhook is configured, confirm its delivery status and the receiving
106
+ system's idempotency key before deploying.
255
107
 
256
- After deploy, test by submitting the live form. Submissions appear in the
257
- portal at `/app/sites/{siteId}/forms/kontakt/submissions`.
108
+ ## Common patterns
258
109
 
259
- ## Common form patterns
110
+ Newsletter:
260
111
 
261
- ### Booking / appointment request
262
112
  ```json
263
- {"fields": [
264
- {"name":"name", "type":"text", "label":"Namn", "required":true},
265
- {"name":"email", "type":"email", "label":"E-post", "required":true},
266
- {"name":"date", "type":"text", "label":"Önskat datum (YYYY-MM-DD)"},
267
- {"name":"time", "type":"select","label":"Tid", "options":["09:00","10:00","11:00","14:00","15:00"]},
268
- {"name":"notes", "type":"textarea","label":"Kommentar"}
269
- ]}
113
+ {"fields":[{"name":"email","type":"email","label":"E-postadress","required":true}]}
270
114
  ```
271
115
 
272
- ### Newsletter signup (minimal)
116
+ Contact:
117
+
273
118
  ```json
274
- {"fields": [
275
- {"name":"email", "type":"email", "label":"E-postadress", "required":true}
119
+ {"fields":[
120
+ {"name":"name","type":"text","label":"Namn","required":true},
121
+ {"name":"email","type":"email","label":"E-post","required":true},
122
+ {"name":"message","type":"textarea","label":"Meddelande","required":true}
276
123
  ]}
277
124
  ```
278
125
 
279
- ### Job application
126
+ Booking request:
127
+
280
128
  ```json
281
- {"fields": [
282
- {"name":"name", "type":"text", "label":"Namn", "required":true},
283
- {"name":"email", "type":"email", "label":"E-post", "required":true},
284
- {"name":"role", "type":"select","label":"Roll", "options":["Designer","Projektledare","Övrigt"]},
285
- {"name":"experience", "type":"textarea","label":"Berätta om dig själv"},
286
- {"name":"portfolio", "type":"url", "label":"Portfolio-URL"}
129
+ {"fields":[
130
+ {"name":"name","type":"text","label":"Namn","required":true},
131
+ {"name":"email","type":"email","label":"E-post","required":true},
132
+ {"name":"time","type":"select","label":"Tid","options":["09:00","10:00","14:00"]},
133
+ {"name":"notes","type":"textarea","label":"Kommentar"}
287
134
  ]}
288
135
  ```
289
136
 
290
137
  ## Pitfalls
291
138
 
292
- - **Tokens are stable, not expiring.** The `submit_token` stays valid
293
- until the platform rotates its signing secret (rare, operator-driven).
294
- Bake it into the static HTML; no refresh logic needed. If submissions
295
- suddenly 403 after working, re-fetch via `read_form` and republish.
296
- - **Inline `<script>` does not survive.** The page sanitizer strips it
297
- from `html_content` never rely on client JS you embed yourself. The
298
- plain form POST works without it (section 3).
299
- - **Field names must be lowercase ASCII** (`[a-z][a-z0-9_-]*`):
300
- `foretag` not `företag`, `amne` not `ämne`. Labels can be anything.
301
- - **Don't use the same form_id on two different forms.** IDs must be
302
- unique per site — use descriptive names: `kontakt`, `boka`, `nyhetsbrev`.
303
- - **Honeypot must be invisible.** `_hp` field must have `display:none`.
304
- If it's visible and a real user fills it, their submission is rejected.
305
- - **Email notifications are admin-only — not settable via MCP.** Submissions
306
- are stored and visible in the portal's Forms → Submissions inbox. A site
307
- admin can also configure post-submission emails (admin notification +
308
- autoresponder) under **Forms → <form> → Email**, after setting up an email
309
- connector under **Settings → Email & notifications**. These carry recipient
310
- addresses + templates over submission data, so they're deliberately off the
311
- agent surface (`create_form`/`update_form` ignore `actions`). Point the
312
- customer at those screens; you can't set them up for them.
139
+ - Do not hand-write a form, token, honeypot, or submit script.
140
+ - Do not put a raw `<script>` in page HTML; the sanitizer removes it.
141
+ - Do not expose action configuration through agent surfaces.
142
+ - Do not send every submitted field to a webhook by default; choose the
143
+ smallest allowlist the external register needs.
144
+ - `submit_token` is stable until the platform rotates its form-signing secret;
145
+ a rebuild refreshes it after rotation.