@typeroll/mcp-server 0.7.5 → 0.7.8

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.
@@ -0,0 +1,243 @@
1
+ ---
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.
4
+ ---
5
+
6
+ # Add a form to a Typeroll site
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.
12
+
13
+ ## Preconditions
14
+
15
+ - Site exists and MCP is configured.
16
+ - You know what fields the form needs.
17
+
18
+ ## Recipe
19
+
20
+ ### 1. Create the form definition
21
+
22
+ ```
23
+ create_form {
24
+ "id": "kontakt",
25
+ "name": "Kontaktformulär",
26
+ "fields": [
27
+ {"name":"name", "label":"Namn", "type":"text", "required":true},
28
+ {"name":"email", "label":"E-post", "type":"email", "required":true},
29
+ {"name":"phone", "label":"Telefon", "type":"text"},
30
+ {"name":"message", "label":"Meddelande", "type":"textarea", "required":true},
31
+ {"name":"subject", "label":"Ämne", "type":"select",
32
+ "options":["Prisförfrågan","Samarbete","Övrigt"]}
33
+ ],
34
+ "success_message": "Tack! Vi återkommer inom 24 timmar.",
35
+ "recipient_email": "hej@acme.se"
36
+ }
37
+ ```
38
+
39
+ **Field types:** `text`, `email`, `tel`, `url`, `number`, `textarea`,
40
+ `select`, `radio`, `checkbox`.
41
+
42
+ **Field name rules:** Lowercase ASCII only: `[a-z][a-z0-9_-]*`.
43
+ - `besokt` not `besökt` (`ö→o`)
44
+ - `foretag` not `företag` (`ö→o`, `ä→a`)
45
+ - `meddelande` not `Meddelande` (the `name` must be lowercase; `label` can be anything)
46
+
47
+ ### 2. Get the signed embed token
48
+
49
+ The form's submit button needs a signed token that proves it was
50
+ generated by the platform. Fetch it:
51
+
52
+ ```
53
+ read_form form_id="kontakt"
54
+ ```
55
+
56
+ The response includes `submit_token` (a short-lived HMAC). Use this in
57
+ the HTML.
58
+
59
+ Actually, the embed HTML is best generated by reading the form and
60
+ building it manually — the form's `id` is all you need for the action URL.
61
+
62
+ ### 3. Embed the form on a page
63
+
64
+ Typeroll forms submit to the platform's endpoint. The HTML:
65
+
66
+ ```html
67
+ <section class="contact-section">
68
+ <div class="contact-container">
69
+ <h2>Kontakta oss</h2>
70
+ <p>Fyll i formuläret så återkommer vi inom 24 timmar.</p>
71
+
72
+ <form class="contact-form"
73
+ action="https://app.typeroll.com/api/forms/submit"
74
+ method="POST">
75
+ <!-- Hidden fields — required by the platform -->
76
+ <input type="hidden" name="_form_id" value="kontakt">
77
+ <input type="hidden" name="_site_id" value="YOUR_SITE_ID">
78
+ <input type="hidden" name="_token" value="SIGNED_TOKEN">
79
+ <!-- Honeypot — must stay empty, bots fill it -->
80
+ <input type="text" name="_hp" style="display:none" tabindex="-1" autocomplete="off">
81
+
82
+ <div class="form-group">
83
+ <label for="name">Namn *</label>
84
+ <input type="text" id="name" name="name" required>
85
+ </div>
86
+
87
+ <div class="form-group">
88
+ <label for="email">E-post *</label>
89
+ <input type="email" id="email" name="email" required>
90
+ </div>
91
+
92
+ <div class="form-group">
93
+ <label for="message">Meddelande *</label>
94
+ <textarea id="message" name="message" rows="5" required></textarea>
95
+ </div>
96
+
97
+ <div class="form-group">
98
+ <label for="subject">Ämne</label>
99
+ <select id="subject" name="subject">
100
+ <option value="Prisförfrågan">Prisförfrågan</option>
101
+ <option value="Samarbete">Samarbete</option>
102
+ <option value="Övrigt">Övrigt</option>
103
+ </select>
104
+ </div>
105
+
106
+ <button type="submit" class="btn-primary">Skicka meddelande</button>
107
+ </form>
108
+ </div>
109
+ </section>
110
+
111
+ <style>
112
+ .contact-section{padding:4rem 2rem}
113
+ .contact-container{max-width:600px;margin:0 auto}
114
+ .form-group{margin-bottom:1.5rem}
115
+ .form-group label{display:block;font-weight:600;margin-bottom:0.4rem;font-size:0.9rem}
116
+ .form-group input,.form-group textarea,.form-group select{
117
+ width:100%;padding:0.75rem 1rem;border:1px solid var(--color-surface);
118
+ border-radius:0.375rem;font-family:inherit;font-size:1rem;
119
+ background:var(--color-surface);color:var(--color-text)
120
+ }
121
+ .form-group textarea{resize:vertical}
122
+ .btn-primary{
123
+ background:var(--color-primary);color:#fff;border:none;
124
+ padding:0.875rem 2rem;border-radius:0.375rem;font-size:1rem;
125
+ font-weight:600;cursor:pointer;width:100%
126
+ }
127
+ .btn-primary:hover{opacity:0.9}
128
+ </style>
129
+ ```
130
+
131
+ **Replace:**
132
+ - `YOUR_SITE_ID` → the site's actual id (from `get_site`)
133
+ - `SIGNED_TOKEN` → fetch via `read_form form_id="kontakt"` → `submit_token`
134
+
135
+ ### 4. Add success/error handling (optional JS)
136
+
137
+ To show inline feedback without a page reload:
138
+
139
+ ```html
140
+ <script>
141
+ (function(){
142
+ const form = document.querySelector('.contact-form');
143
+ if(!form) return;
144
+ form.addEventListener('submit', async(e) => {
145
+ e.preventDefault();
146
+ const btn = form.querySelector('[type=submit]');
147
+ btn.disabled = true;
148
+ btn.textContent = 'Skickar…';
149
+ try {
150
+ const res = await fetch(form.action, {method:'POST', body: new FormData(form)});
151
+ const data = await res.json();
152
+ if(data.ok) {
153
+ form.innerHTML = '<p class="form-success">Tack! Vi återkommer inom 24 timmar.</p>';
154
+ } else {
155
+ btn.disabled = false;
156
+ btn.textContent = 'Skicka meddelande';
157
+ alert('Något gick fel: ' + (data.error || 'okänt fel'));
158
+ }
159
+ } catch {
160
+ btn.disabled = false;
161
+ btn.textContent = 'Skicka meddelande';
162
+ alert('Nätverksfel — försök igen.');
163
+ }
164
+ });
165
+ })();
166
+ </script>
167
+ ```
168
+
169
+ ### 5. Update the contact page with the form HTML
170
+
171
+ ```
172
+ update_page page_id="kontakt" patch={
173
+ "html_content": "<full page HTML including the form section>"
174
+ }
175
+ ```
176
+
177
+ ### 6. Verify
178
+
179
+ ```
180
+ read_form form_id="kontakt"
181
+ list_forms
182
+ ```
183
+
184
+ Confirm the form appears and fields match what you embedded.
185
+
186
+ ### 7. Deploy
187
+
188
+ ```
189
+ trigger_deploy
190
+ get_deploy_status job_id=<id>
191
+ ```
192
+
193
+ After deploy, test by submitting the live form. Submissions appear in the
194
+ portal at `/app/sites/{siteId}/forms/kontakt/submissions`.
195
+
196
+ ## Common form patterns
197
+
198
+ ### Booking / appointment request
199
+ ```json
200
+ {"fields": [
201
+ {"name":"name", "type":"text", "label":"Namn", "required":true},
202
+ {"name":"email", "type":"email", "label":"E-post", "required":true},
203
+ {"name":"date", "type":"text", "label":"Önskat datum (YYYY-MM-DD)"},
204
+ {"name":"time", "type":"select","label":"Tid", "options":["09:00","10:00","11:00","14:00","15:00"]},
205
+ {"name":"notes", "type":"textarea","label":"Kommentar"}
206
+ ]}
207
+ ```
208
+
209
+ ### Newsletter signup (minimal)
210
+ ```json
211
+ {"fields": [
212
+ {"name":"email", "type":"email", "label":"E-postadress", "required":true}
213
+ ]}
214
+ ```
215
+
216
+ ### Job application
217
+ ```json
218
+ {"fields": [
219
+ {"name":"name", "type":"text", "label":"Namn", "required":true},
220
+ {"name":"email", "type":"email", "label":"E-post", "required":true},
221
+ {"name":"role", "type":"select","label":"Roll", "options":["Designer","Projektledare","Övrigt"]},
222
+ {"name":"experience", "type":"textarea","label":"Berätta om dig själv"},
223
+ {"name":"portfolio", "type":"url", "label":"Portfolio-URL"}
224
+ ]}
225
+ ```
226
+
227
+ ## Pitfalls
228
+
229
+ - **Tokens expire.** The `submit_token` in the form embed is short-lived
230
+ (24h by default). For forms on long-cached static pages, fetch a fresh
231
+ token via `read_form` and rebuild the page HTML, then redeploy.
232
+ Alternatively, fetch the token client-side via a small `<script>` that
233
+ calls the portal's token endpoint before submit.
234
+ - **Field names must be ASCII.** `message` not `meddelande`... wait,
235
+ `meddelande` is ASCII. `foretag` not `företag`. Only Swedish/Nordic
236
+ special characters (å, ä, ö) cause problems.
237
+ - **Don't use the same form_id on two different forms.** IDs must be
238
+ unique per site — use descriptive names: `kontakt`, `boka`, `nyhetsbrev`.
239
+ - **Honeypot must be invisible.** `_hp` field must have `display:none`.
240
+ If it's visible and a real user fills it, their submission is rejected.
241
+ - **Recipient email is display-only in phase 1.** The portal stores
242
+ submissions; email delivery to `recipient_email` depends on the platform's
243
+ email configuration. Confirm with the customer that they'll check the inbox.
@@ -0,0 +1,173 @@
1
+ ---
2
+ name: tr-import-url
3
+ description: Use when the user wants to import or migrate content from a non-WordPress website — a Squarespace site, a Wix site, a static HTML site, a Webflow export, or any URL the user points at. Also triggers on "copy content from", "rebuild this site", "import from Squarespace/Wix/Webflow", or "make it look like this site". For WordPress sources use tr-migrate-wp instead.
4
+ ---
5
+
6
+ # Import content from a non-WordPress site
7
+
8
+ ## When to use this vs tr-migrate-wp
9
+
10
+ | Source | Use |
11
+ |---|---|
12
+ | WordPress with `/wp-json` accessible | `tr-migrate-wp` |
13
+ | WordPress with REST disabled | This skill (scrape HTML) |
14
+ | Squarespace, Wix, Webflow, static HTML | This skill |
15
+ | CSV / spreadsheet data | This skill (skip scraping, just parse) |
16
+ | Any URL the user points at | This skill |
17
+
18
+ ## Preconditions
19
+
20
+ - Target Typeroll site exists with working header/footer.
21
+ - Source URL(s) accessible (check with a quick `fetch`; if blocked, mention
22
+ it and ask the user for an HTML export or screenshot).
23
+
24
+ ## Recipe
25
+
26
+ ### 1. Inventory the source site
27
+
28
+ Fetch the homepage and build a URL list:
29
+
30
+ ```
31
+ fetch <source-url> # root HTML
32
+ fetch <source-url>/sitemap.xml # XML sitemap if it exists
33
+ ```
34
+
35
+ Parse `<a href>` links to discover internal pages. Build a list:
36
+ - Homepage
37
+ - Top-level pages (About, Services, Contact, etc.)
38
+ - Any sub-pages that look important
39
+
40
+ Avoid: pagination URLs, session URLs, `/wp-admin`, `/cdn-cgi/`, query strings.
41
+
42
+ ### 2. Learn the target's design
43
+
44
+ ```
45
+ read_site_settings
46
+ read_partial partial_id="header"
47
+ list_pages limit=5
48
+ ```
49
+
50
+ The goal is to understand what CSS variables, class names, and structural
51
+ conventions the target site uses so the imported content looks native.
52
+
53
+ ### 3. Fetch and clean each source page
54
+
55
+ For each URL:
56
+
57
+ **a. Fetch the HTML.**
58
+ ```
59
+ fetch <page-url>
60
+ ```
61
+
62
+ If the site returns a bot-block (Cloudflare, 403, or clearly JS-only
63
+ SPA output), note it. Tell the user: "This page blocked direct fetching.
64
+ Can you provide the page source or an HTML export?"
65
+
66
+ **b. Extract the main content.**
67
+
68
+ Discard: nav, header, footer, cookie banners, chat widgets, scripts.
69
+ Keep: `<main>`, `<article>`, the largest content region.
70
+
71
+ Clean the HTML:
72
+ - Strip platform-specific classes: `sqsrte-*`, `wf-*`, `et_*`,
73
+ `elementor-*`, `fl-*`, `divi-*`, `vc_*`
74
+ - Remove empty `<div>`, `<span>`, `<section>` wrappers (no class, no content)
75
+ - Unwrap redundant nesting: `<div><p>text</p></div>` → `<p>text</p>`
76
+ - Keep: `<h1>`–`<h6>`, `<p>`, `<ul>`, `<ol>`, `<img>`, `<a>`, `<table>`,
77
+ `<blockquote>`, `<figure>`, `<figcaption>`, `<strong>`, `<em>`
78
+ - Fix headings: ensure exactly one `<h1>` per page (the page title)
79
+
80
+ **c. Transfer images.** For each `<img src>`:
81
+ ```
82
+ upload_media_from_url url="<source-img-url>" alt="..."
83
+ ```
84
+ Replace the src with the returned CDN URL. Skip tracking pixels
85
+ (1×1 images), decorative SVGs that are just icons, and anything
86
+ that 404s.
87
+
88
+ **d. Adapt to the target's design.**
89
+ Replace source-specific CSS classes with target conventions.
90
+ Use `var(--color-*)` for colors, `var(--font-*)` for type.
91
+
92
+ ### 4. Create pages as drafts
93
+
94
+ ```
95
+ create_page title="Om oss" slug="om-oss"
96
+ html_content="<cleaned, adapted HTML>"
97
+ content_mode="html" status="draft"
98
+ seo_title="Om oss — Acme"
99
+ seo_description="..."
100
+ ```
101
+
102
+ Always draft first. The user signs off before publishing.
103
+
104
+ ### 5. Handle redirects
105
+
106
+ If the source URLs differ from the target slugs, create redirects:
107
+
108
+ ```
109
+ create_redirect from_path="/about" to_path="/om-oss"
110
+ create_redirect from_path="/services.html" to_path="/tjanster"
111
+ ```
112
+
113
+ ### 6. Preview with the user
114
+
115
+ ```
116
+ get_preview_link
117
+ ```
118
+
119
+ Walk through every imported page with the user. Common issues:
120
+ - Heading hierarchy wrong (two H1s, or H3 used where H2 belongs)
121
+ - Images missing alt text
122
+ - Squarespace column layouts that don't work without their grid system
123
+ - Embedded forms or maps that need re-setup
124
+
125
+ ### 7. Publish + deploy
126
+
127
+ After approval:
128
+ ```
129
+ batch_update_pages updates=[
130
+ {page_id: "om-oss", patch: {status: "published"}},
131
+ {page_id: "tjanster", patch: {status: "published"}}
132
+ ]
133
+ trigger_deploy
134
+ get_deploy_status job_id=<id>
135
+ ```
136
+
137
+ ## Platform-specific notes
138
+
139
+ ### Squarespace
140
+ - Main content is inside `.content-wrapper` or `[data-section-theme]` blocks
141
+ - Portfolio images are usually high-resolution originals in `/universal/images/`
142
+ - JSON-LD is Squarespace's own schema — strip it
143
+ - Gallery blocks → convert to CSS grid with inline `<img>` tags
144
+
145
+ ### Wix
146
+ - Wix sites are React SPAs — `fetch` returns an empty shell
147
+ - Ask the user for the Wix site's "Export to HTML" (available in some plans)
148
+ or take screenshots for reference
149
+ - Best path: get content from the user (text + image files), rebuild clean
150
+
151
+ ### Webflow
152
+ - Usually fetchable; clean output
153
+ - Classes like `w-container`, `w-row`, `w-col-*` can be stripped
154
+ - Webflow CMS items are server-rendered — they appear in the HTML
155
+
156
+ ### Static HTML / old sites
157
+ - Often the cleanest import. Fetch, strip nav/footer, keep body.
158
+ - Watch for table-based layouts (pre-2010 sites) — convert to CSS grid
159
+
160
+ ## Pitfalls
161
+
162
+ - **Don't import `<style>` blocks from the source site.** They reference
163
+ external fonts, resets, and classes that don't exist in the target.
164
+ Strip all `<style>` tags from source HTML and rewrite styles in the
165
+ target's conventions.
166
+ - **Don't break the single-H1 rule.** Many source sites have no H1 or
167
+ several. Fix it.
168
+ - **Squarespace/Wix forms.** They won't work after import — the backend
169
+ is vendor-locked. Create a Typeroll form instead: `create_form`.
170
+ - **Analytics/tracking code.** If the source has GA4 or similar, don't
171
+ copy it into pages. Set it via `update_site_settings scripts_head="..."`.
172
+ - **Videos.** YouTube/Vimeo embeds are fine (`<iframe>` is allowed).
173
+ Hosted MP4s need re-uploading if the source URL won't persist.