@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.
- package/AGENTS.md +13 -6
- package/README.md +26 -4
- package/dist/bundled-content.js +3 -3
- package/dist/extension-cli.js +188 -0
- package/dist/index.js +6 -0
- package/dist/server.js +7 -1
- package/dist/tools/apps.js +36 -0
- package/dist/tools/forms.js +5 -7
- package/dist/version.js +1 -1
- package/package.json +6 -5
- package/skills/tr-forms.md +90 -257
package/skills/tr-forms.md
CHANGED
|
@@ -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
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
##
|
|
12
|
+
## Choose the placement
|
|
14
13
|
|
|
15
|
-
-
|
|
16
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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":"
|
|
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
|
-
"
|
|
33
|
+
"submit_text": "Prenumerera",
|
|
34
|
+
"success_message": "Tack! Du är anmäld."
|
|
71
35
|
}
|
|
72
36
|
```
|
|
73
37
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
`select`, `radio`, `checkbox`.
|
|
42
|
+
## Place the form
|
|
81
43
|
|
|
82
|
-
|
|
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
|
-
|
|
47
|
+
add_block target={kind:'page', id:'start'}
|
|
48
|
+
block={type:'core/form', data:{form_id:'newsletter'}}
|
|
95
49
|
```
|
|
96
50
|
|
|
97
|
-
|
|
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="
|
|
114
|
-
<
|
|
115
|
-
|
|
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
|
-
|
|
177
|
-
-
|
|
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
|
-
|
|
63
|
+
## Multi-step form
|
|
181
64
|
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
form
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
+
Completed submissions appear in Forms → Submissions. Admins can configure
|
|
87
|
+
actions in the form editor:
|
|
241
88
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
+
## Verify
|
|
250
100
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
257
|
-
portal at `/app/sites/{siteId}/forms/kontakt/submissions`.
|
|
108
|
+
## Common patterns
|
|
258
109
|
|
|
259
|
-
|
|
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
|
-
|
|
116
|
+
Contact:
|
|
117
|
+
|
|
273
118
|
```json
|
|
274
|
-
{"fields":
|
|
275
|
-
{"name":"
|
|
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
|
-
|
|
126
|
+
Booking request:
|
|
127
|
+
|
|
280
128
|
```json
|
|
281
|
-
{"fields":
|
|
282
|
-
{"name":"name",
|
|
283
|
-
{"name":"email",
|
|
284
|
-
{"name":"
|
|
285
|
-
{"name":"
|
|
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
|
-
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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.
|