@typeroll/mcp-server 0.10.0 → 0.11.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 +1 -1
- package/dist/tools/forms.js +7 -5
- package/package.json +1 -1
- package/skills/tr-forms.md +63 -35
package/AGENTS.md
CHANGED
|
@@ -466,7 +466,7 @@ stakeholder review.
|
|
|
466
466
|
| **Collections** | `create_collection`, `update_collection_schema`, `delete_collection`, `list_collections`, `read_collection`, `list_collection_items` (richtext hidden by default), `read_collection_item`, `batch_read_collection_items`, `create_collection_item`, `update_collection_item`, `delete_collection_item`, `regenerate_collection_listing` |
|
|
467
467
|
| **Media** | `list_media`, `read_media`, `create_upload_url`, `upload_media_from_url`, `upload_media_inline`, `update_media`, `delete_media`, `finalize_media`, `finalize_all_media`, `generate_image_variants`, `suggest_alt_text_context` |
|
|
468
468
|
| **Redirects** | `list_redirects`, `create_redirect`, `delete_redirect` |
|
|
469
|
-
| **Forms** | `list_forms`, `read_form`, `create_form`, `update_form`, `delete_form`, `list_form_submissions` |
|
|
469
|
+
| **Forms** | `list_forms`, `read_form`, `create_form`, `update_form`, `delete_form`, `list_form_submissions`. read/create return `submit_token` + `submit_url` — embed as a plain `<form method="POST">` with a hidden `_token` input + empty honeypot `_hp`; no client JS (the sanitizer strips inline `<script>`; the endpoint answers form posts with an HTML confirmation page) |
|
|
470
470
|
| **Settings** | `update_site_settings` (whitelist) |
|
|
471
471
|
| **Search + bulk** | `search_pages`, `bulk_replace_text` |
|
|
472
472
|
| **Branches** | `create_branch`, `read_version`, `delete_branch`, `merge_branch` |
|
package/dist/tools/forms.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Forms — agents can create/update/delete forms, and read submissions.
|
|
2
2
|
// The customer-facing submit endpoint is HMAC-signed and lives at
|
|
3
|
-
// /api/forms/submit
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// /api/forms/submit. read_form/create_form return `submit_token` +
|
|
4
|
+
// `submit_url`, which is everything an agent needs to embed a working
|
|
5
|
+
// plain-POST form (no client JS — the page sanitizer strips inline
|
|
6
|
+
// <script>, and the endpoint answers form-encoded posts with an HTML
|
|
7
|
+
// confirmation page).
|
|
6
8
|
import { z } from 'zod';
|
|
7
9
|
import { ok, withErrorBoundary } from './helpers.js';
|
|
8
10
|
const fieldSchema = z.object({
|
|
@@ -28,7 +30,7 @@ export const formTools = [
|
|
|
28
30
|
},
|
|
29
31
|
{
|
|
30
32
|
name: 'read_form',
|
|
31
|
-
description: 'Read one form by id, including fields, submit_text, and
|
|
33
|
+
description: 'Read one form by id, including fields, submit_text, success_message — plus submit_token and submit_url for embedding: a plain <form method="POST" action={submit_url}> with a hidden _token input and an empty honeypot _hp is a fully working no-JS embed.',
|
|
32
34
|
inputSchema: { form_id: z.string() },
|
|
33
35
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
34
36
|
const res = await client.get(siteId, `forms/${encodeURIComponent(args.form_id)}`);
|
|
@@ -37,7 +39,7 @@ export const formTools = [
|
|
|
37
39
|
},
|
|
38
40
|
{
|
|
39
41
|
name: 'create_form',
|
|
40
|
-
description: 'Create a form. Each field has { name, type, label, required?, placeholder?, options? }. Allowed types: text, email, tel, url, number, textarea, select, checkbox, radio, hidden, gdpr_consent. The form
|
|
42
|
+
description: 'Create a form. Each field has { name, type, label, required?, placeholder?, options? }. Allowed types: text, email, tel, url, number, textarea, select, checkbox, radio, hidden, gdpr_consent. The response includes submit_token + submit_url — embed with a plain <form method="POST" action={submit_url}>, a hidden _token input, and an empty honeypot _hp. Do NOT add inline <script> (the page sanitizer strips it); the endpoint answers plain form posts with an HTML confirmation page.',
|
|
41
43
|
inputSchema: {
|
|
42
44
|
id: z.string().regex(/^[a-z][a-z0-9_-]{0,62}$/),
|
|
43
45
|
name: z.string().min(1),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typeroll/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Model Context Protocol server for the Typeroll public API. Use with Claude Code or any MCP-compatible client to manage a Typeroll site.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/skills/tr-forms.md
CHANGED
|
@@ -31,11 +31,15 @@ create_form {
|
|
|
31
31
|
{"name":"subject", "label":"Ämne", "type":"select",
|
|
32
32
|
"options":["Prisförfrågan","Samarbete","Övrigt"]}
|
|
33
33
|
],
|
|
34
|
-
"success_message": "Tack! Vi återkommer inom 24 timmar."
|
|
35
|
-
"recipient_email": "hej@acme.se"
|
|
34
|
+
"success_message": "Tack! Vi återkommer inom 24 timmar."
|
|
36
35
|
}
|
|
37
36
|
```
|
|
38
37
|
|
|
38
|
+
There is no `recipient_email` field — submissions land in the portal's
|
|
39
|
+
Submissions inbox (`/app/sites/{siteId}/forms/{formId}/submissions`).
|
|
40
|
+
Email notification is a form *action* (not yet wired platform-side);
|
|
41
|
+
tell the customer to check the inbox.
|
|
42
|
+
|
|
39
43
|
**Field types:** `text`, `email`, `tel`, `url`, `number`, `textarea`,
|
|
40
44
|
`select`, `radio`, `checkbox`.
|
|
41
45
|
|
|
@@ -46,22 +50,28 @@ create_form {
|
|
|
46
50
|
|
|
47
51
|
### 2. Get the signed embed token
|
|
48
52
|
|
|
49
|
-
The
|
|
50
|
-
|
|
53
|
+
The submit endpoint only accepts requests carrying a platform-signed
|
|
54
|
+
HMAC token. `create_form` returns it directly; you can also fetch it any
|
|
55
|
+
time with:
|
|
51
56
|
|
|
52
57
|
```
|
|
53
58
|
read_form form_id="kontakt"
|
|
54
59
|
```
|
|
55
60
|
|
|
56
|
-
The response includes `submit_token` (
|
|
57
|
-
the
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
The response includes `submit_token` (put it in the hidden `_token`
|
|
62
|
+
input) and `submit_url` (use it as the form's `action`). The token is
|
|
63
|
+
stable — it only stops working if the platform rotates its signing
|
|
64
|
+
secret — so baking it into static page HTML is correct. If
|
|
65
|
+
`submit_token` comes back `null`, the server has no signing secret
|
|
66
|
+
configured (dev setups); the form cannot accept submissions until that's
|
|
67
|
+
fixed — tell the user instead of embedding a broken form.
|
|
61
68
|
|
|
62
69
|
### 3. Embed the form on a page
|
|
63
70
|
|
|
64
|
-
|
|
71
|
+
A plain HTML form POST is the default and needs **no JavaScript**: the
|
|
72
|
+
endpoint answers a normal form-encoded POST with a small confirmation
|
|
73
|
+
page (the form's `success_message` + a link back to the page the
|
|
74
|
+
visitor came from). Validation errors get the same treatment. The HTML:
|
|
65
75
|
|
|
66
76
|
```html
|
|
67
77
|
<section class="contact-section">
|
|
@@ -70,11 +80,10 @@ Typeroll forms submit to the platform's endpoint. The HTML:
|
|
|
70
80
|
<p>Fyll i formuläret så återkommer vi inom 24 timmar.</p>
|
|
71
81
|
|
|
72
82
|
<form class="contact-form"
|
|
73
|
-
action="
|
|
83
|
+
action="SUBMIT_URL"
|
|
74
84
|
method="POST">
|
|
75
|
-
<!--
|
|
76
|
-
|
|
77
|
-
<input type="hidden" name="_site_id" value="YOUR_SITE_ID">
|
|
85
|
+
<!-- The signed token is the only hidden field the platform needs;
|
|
86
|
+
it encodes org + site + form identity. -->
|
|
78
87
|
<input type="hidden" name="_token" value="SIGNED_TOKEN">
|
|
79
88
|
<!-- Honeypot — must stay empty, bots fill it -->
|
|
80
89
|
<input type="text" name="_hp" style="display:none" tabindex="-1" autocomplete="off">
|
|
@@ -129,32 +138,50 @@ Typeroll forms submit to the platform's endpoint. The HTML:
|
|
|
129
138
|
```
|
|
130
139
|
|
|
131
140
|
**Replace:**
|
|
132
|
-
- `
|
|
133
|
-
- `SIGNED_TOKEN` →
|
|
141
|
+
- `SUBMIT_URL` → `submit_url` from `read_form` / `create_form`
|
|
142
|
+
- `SIGNED_TOKEN` → `submit_token` from the same response
|
|
143
|
+
|
|
144
|
+
### 4. Inline feedback (optional — requires user action)
|
|
134
145
|
|
|
135
|
-
|
|
146
|
+
**The page sanitizer strips inline `<script>` from page and partial
|
|
147
|
+
HTML**, so you cannot ship fetch-based submit handling yourself — a
|
|
148
|
+
`<script>` you put in `html_content` is silently removed. The plain
|
|
149
|
+
POST above is the reliable path; use it.
|
|
136
150
|
|
|
137
|
-
|
|
151
|
+
If the customer wants inline feedback without a page reload, the script
|
|
152
|
+
must go into the site's `scripts_body_end` setting, which only a human
|
|
153
|
+
can edit (Settings → Custom code — it's deliberately excluded from the
|
|
154
|
+
AI tool surface). Hand them this snippet for that box; it intercepts the
|
|
155
|
+
form and POSTs the JSON shape `{ token, data }`:
|
|
138
156
|
|
|
139
157
|
```html
|
|
140
158
|
<script>
|
|
141
159
|
(function(){
|
|
142
160
|
const form = document.querySelector('.contact-form');
|
|
143
161
|
if(!form) return;
|
|
144
|
-
form.addEventListener('submit', async(e) => {
|
|
162
|
+
form.addEventListener('submit', async (e) => {
|
|
145
163
|
e.preventDefault();
|
|
146
164
|
const btn = form.querySelector('[type=submit]');
|
|
147
165
|
btn.disabled = true;
|
|
148
166
|
btn.textContent = 'Skickar…';
|
|
167
|
+
// The endpoint's fetch contract is JSON: { token, data }.
|
|
168
|
+
const fd = new FormData(form);
|
|
169
|
+
const token = fd.get('_token');
|
|
170
|
+
const data = {};
|
|
171
|
+
fd.forEach((v, k) => { if (k !== '_token') data[k] = v; });
|
|
149
172
|
try {
|
|
150
|
-
const res = await fetch(form.action, {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
173
|
+
const res = await fetch(form.action, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: { 'Content-Type': 'application/json' },
|
|
176
|
+
body: JSON.stringify({ token, data }),
|
|
177
|
+
});
|
|
178
|
+
const json = await res.json();
|
|
179
|
+
if (res.ok && json.success) {
|
|
180
|
+
form.innerHTML = '<p class="form-success">' + (json.message || 'Tack!') + '</p>';
|
|
154
181
|
} else {
|
|
155
182
|
btn.disabled = false;
|
|
156
183
|
btn.textContent = 'Skicka meddelande';
|
|
157
|
-
alert('Något gick fel: ' + (
|
|
184
|
+
alert('Något gick fel: ' + ((json.errors && json.errors.join(', ')) || json.error || 'okänt fel'));
|
|
158
185
|
}
|
|
159
186
|
} catch {
|
|
160
187
|
btn.disabled = false;
|
|
@@ -226,18 +253,19 @@ portal at `/app/sites/{siteId}/forms/kontakt/submissions`.
|
|
|
226
253
|
|
|
227
254
|
## Pitfalls
|
|
228
255
|
|
|
229
|
-
- **Tokens
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
256
|
+
- **Tokens are stable, not expiring.** The `submit_token` stays valid
|
|
257
|
+
until the platform rotates its signing secret (rare, operator-driven).
|
|
258
|
+
Bake it into the static HTML; no refresh logic needed. If submissions
|
|
259
|
+
suddenly 403 after working, re-fetch via `read_form` and republish.
|
|
260
|
+
- **Inline `<script>` does not survive.** The page sanitizer strips it
|
|
261
|
+
from `html_content` — never rely on client JS you embed yourself. The
|
|
262
|
+
plain form POST works without it (section 3).
|
|
263
|
+
- **Field names must be lowercase ASCII** (`[a-z][a-z0-9_-]*`):
|
|
264
|
+
`foretag` not `företag`, `amne` not `ämne`. Labels can be anything.
|
|
237
265
|
- **Don't use the same form_id on two different forms.** IDs must be
|
|
238
266
|
unique per site — use descriptive names: `kontakt`, `boka`, `nyhetsbrev`.
|
|
239
267
|
- **Honeypot must be invisible.** `_hp` field must have `display:none`.
|
|
240
268
|
If it's visible and a real user fills it, their submission is rejected.
|
|
241
|
-
- **
|
|
242
|
-
|
|
243
|
-
|
|
269
|
+
- **No email notifications yet.** Submissions are stored and visible in
|
|
270
|
+
the portal's Submissions inbox; the email action handler isn't wired
|
|
271
|
+
platform-side. Confirm with the customer that they'll check the inbox.
|