@typeroll/mcp-server 0.45.22 → 0.45.23

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,183 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { z } from 'zod';
5
+ import { VERSION } from './version.js';
6
+ import { ApiError, TyperollClient } from './client.js';
7
+ export const workspaceSchema = z.object({
8
+ schema_version: z.literal(1),
9
+ portal: z.string().url().refine(value => {
10
+ const url = new URL(value);
11
+ return !url.username && !url.password && !url.search && !url.hash && url.pathname === '/' &&
12
+ (url.protocol === 'https:' || (url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)));
13
+ }, 'Use an HTTPS portal origin (HTTP is allowed only on loopback).'),
14
+ organization_id: z.string().min(1).nullable(), site_id: z.string().min(1).nullable(),
15
+ version: z.string().min(1), tool_mode: z.enum(['compact', 'full']),
16
+ }).strict();
17
+ export const defaultWorkspace = { schema_version: 1, portal: 'https://app.typeroll.com', organization_id: null, site_id: null, version: 'main', tool_mode: 'compact' };
18
+ const lockSchema = z.object({ schema_version: z.literal(1), generator_version: z.string(), files: z.record(z.string().regex(/^[a-f0-9]{64}$/)) }).strict();
19
+ const hash = (value) => createHash('sha256').update(value).digest('hex');
20
+ export const ignores = ['.typeroll/', '.env', '.env.*', '!.env.example', '.mcp.json', '.cursor/mcp.json', '.vscode/mcp.json', 'qa/reports/', 'sources/private/'];
21
+ export const agentInstructions = `# Working on this website
22
+
23
+ Read typeroll.json, brief/ and decisions.md. Confirm the portal, Organization,
24
+ Site and working Version before writes. This workspace is not the generated
25
+ publication repository. The CMS owns current content and configuration; local
26
+ sources are proposals unless the user explicitly adopts them.
27
+
28
+ ## Discover only what the task needs
29
+
30
+ With compact MCP, search_tools then describe_tool; call_read_tool,
31
+ call_write_tool and call_admin_tool preserve the operation's effect. In full
32
+ mode use the named tools directly. Start with get_site/get_site_capabilities.
33
+ Use list_skills and read the relevant recipe. Request read_guide sections_only
34
+ before loading specific sections, not the complete manual at every connection.
35
+ Call read_app_documentation for enabled apps when needed. Provider docs are
36
+ reference material, not permission to deploy or send messages. Never commit
37
+ private app guides or cache them as permanent project instructions.
38
+ Public documentation: https://typeroll.com/docs/llms.txt
39
+
40
+ ## Edit and verify
41
+
42
+ - Read current Pages, Content types, templates, partials and relevant block schemas.
43
+ - Use native editable blocks; document necessary exceptions and design decisions.
44
+ - Keep the same Version for reads, writes, references and previews. Larger work
45
+ on an existing site starts with create_branch; record its returned ID in
46
+ typeroll.json and reconnect the local MCP process. Hosted calls pass version
47
+ explicitly; the hosted server cannot read this workspace file.
48
+ - Distinguish working copies, saved CMS content and deployed output. Save within
49
+ the user's scope, review previews and deploy only when explicitly authorized.
50
+ - Check get_import_readiness before any import. For new uploads use signed direct
51
+ upload/finalization. Never put image bytes or customer secrets in Git.
52
+ - Inspect desktop/mobile layouts and interaction states. Read changes back.
53
+ Record exact version, tested pages, evidence and unresolved issues in qa/.
54
+ - Never edit generated publishing source to change CMS content. Do not overwrite
55
+ current CMS or owner answers from an old local snapshot.
56
+
57
+ Store credentials in the client's private settings or injected environment.
58
+ Run the workspace doctor after setup and after changing the binding.
59
+ `;
60
+ export const workspaceTemplates = {
61
+ 'AGENTS.md': agentInstructions,
62
+ 'README.md': `# Website workspace\n\nKeep project intent here; edit the website through Typeroll CMS.\n\n1. Fill in brief/site.md and brief/brand.md.\n2. Connect hosted MCP at https://app.typeroll.com/api/mcp?tools=compact.\n3. Use an Organization key to create a site, or select an existing one.\n4. Record its IDs and working version in typeroll.json. No secrets belong there.\n5. Run npx @typeroll/mcp-server doctor . (inject TYPEROLL_API_KEY privately).\n\nStart guide: https://typeroll.com/docs/getting-started/agent-workspace/\n\nUpdate the generated scaffold with npx @typeroll/mcp-server init . --update.\nEdited files are preserved and reported for manual reconciliation. Optional local\nrecipes: init . --recipes. They are reference Markdown, not automatically loaded.\n`,
63
+ 'brief/site.md': '# Site brief\n\n## Purpose and audience\n\n## Languages and scope\n\n## Primary user journeys\n\n## Content authority and sources\n\n## Editorial constraints and forbidden internal markers\n',
64
+ 'brief/brand.md': '# Brand\n\n## Voice and tone\n\n## Visual direction and references\n\n## Typography, colors and image policy\n\n## Desktop and mobile navigation\n\nRecord intent here; current design tokens belong in CMS settings.\n',
65
+ 'brief/content-model.md': '# Content model\n\n## Page types and routes\n\n## Fields and references\n\n## Shared templates and partials\n\n## Taxonomy and navigation\n\nUse the current CMS schema. This document describes decisions, not a second schema store.\n',
66
+ 'decisions.md': '# Decisions\n\nRecord date, decision, reason, approved scope and alternatives when useful.\n',
67
+ 'sources/README.md': '# Sources\n\nStore research references and copy proposals. Record source URLs and dates.\nPrivate extracts belong in ignored private/ or an approved external store.\nDo not treat local drafts as current CMS content or commit media libraries.\n',
68
+ 'scripts/README.md': '# Project automation\n\nKeep repeatable imports and QA scripts here. Use the authenticated REST API\nfor scripted work; keep credentials outside files and inject them at runtime.\nMCP remains the interactive interface. Typeroll currently has no general site CRUD CLI.\n',
69
+ 'qa/checklist.md': '# Verification\n\n- Confirm Site, Version and deployed/source identity.\n- Read back saved content and inspect desktop/mobile previews.\n- Exercise menus, keyboard focus, links and enabled app states.\n- Verify routes, metadata, media and publishing readiness before launch.\n- Record tested pages, viewports, results and unresolved issues.\n- Store screenshots and private runtime reports in ignored reports/.\n- Publishing validation does not prove visual or editorial completeness.\n',
70
+ };
71
+ export async function readWorkspace(dir) {
72
+ try {
73
+ return workspaceSchema.parse(JSON.parse(await fs.readFile(path.join(dir, 'typeroll.json'), 'utf8')));
74
+ }
75
+ catch {
76
+ throw new Error('Invalid or missing typeroll.json. Run init and use its documented non-secret fields.');
77
+ }
78
+ }
79
+ /** Reject linked destinations, including linked ancestor folders inside the workspace. */
80
+ export async function safeDestination(dir, relative) {
81
+ if (path.isAbsolute(relative) || relative.split(/[\\/]/).some(part => part === '..'))
82
+ throw new Error('Unsafe workspace path');
83
+ let target = dir;
84
+ for (const part of relative.split('/')) {
85
+ target = path.join(target, part);
86
+ const stat = await fs.lstat(target).catch(error => { if (error.code !== 'ENOENT')
87
+ throw error; return null; });
88
+ if (stat?.isSymbolicLink())
89
+ throw new Error(`Refusing linked workspace path: ${relative}`);
90
+ }
91
+ return target;
92
+ }
93
+ export async function writeWorkspaceFiles(dir, templates, update) {
94
+ await fs.mkdir(dir, { recursive: true });
95
+ const root = await fs.realpath(dir);
96
+ const lockFile = await safeDestination(root, 'typeroll.lock.json');
97
+ let previous = { schema_version: 1, generator_version: VERSION, files: {} };
98
+ try {
99
+ previous = lockSchema.parse(JSON.parse(await fs.readFile(lockFile, 'utf8')));
100
+ }
101
+ catch (error) {
102
+ if (error.code !== 'ENOENT')
103
+ throw new Error('Invalid typeroll.lock.json; restore it before updating.');
104
+ }
105
+ // Validate every destination before performing any writes.
106
+ for (const name of [...Object.keys(templates), '.gitignore'])
107
+ await safeDestination(root, name);
108
+ const results = [];
109
+ const files = { ...previous.files };
110
+ for (const [name, content] of Object.entries(templates)) {
111
+ const target = path.join(root, name);
112
+ const existing = await fs.readFile(target, 'utf8').catch(error => { if (error.code !== 'ENOENT')
113
+ throw error; return null; });
114
+ if (existing !== null && existing !== content && (!update || hash(existing) !== previous.files[name])) {
115
+ results.push({ path: name, action: 'modified' });
116
+ continue;
117
+ }
118
+ if (existing !== content) {
119
+ await fs.mkdir(path.dirname(target), { recursive: true });
120
+ await fs.writeFile(target, content, { mode: 0o600 });
121
+ }
122
+ files[name] = hash(content);
123
+ results.push({ path: name, action: existing === content ? 'kept' : existing === null ? 'created' : 'updated' });
124
+ }
125
+ const ignoreFile = path.join(root, '.gitignore');
126
+ const current = await fs.readFile(ignoreFile, 'utf8').catch(error => { if (error.code !== 'ENOENT')
127
+ throw error; return ''; });
128
+ const missing = ignores.filter(rule => !current.split(/\r?\n/).includes(rule));
129
+ if (missing.length)
130
+ await fs.writeFile(ignoreFile, current + (current && !current.endsWith('\n') ? '\n' : '') + '\n# Typeroll workspace private files\n' + missing.join('\n') + '\n');
131
+ // Config is always user-owned, never replaced by an update.
132
+ delete files['typeroll.json'];
133
+ await fs.writeFile(lockFile, JSON.stringify({ schema_version: 1, generator_version: VERSION, files }, null, 2) + '\n');
134
+ return results;
135
+ }
136
+ export function workspaceClient(config, env, fetchImpl) {
137
+ if (!env.TYPEROLL_API_KEY)
138
+ throw new Error('Inject TYPEROLL_API_KEY through private client settings or the environment.');
139
+ if (env.TYPEROLL_API_URL && env.TYPEROLL_API_URL.replace(/\/$/, '') !== config.portal.replace(/\/$/, ''))
140
+ throw new Error('TYPEROLL_API_URL conflicts with typeroll.json.');
141
+ if (env.TYPEROLL_SITE_ID && env.TYPEROLL_SITE_ID !== config.site_id)
142
+ throw new Error('TYPEROLL_SITE_ID conflicts with typeroll.json.');
143
+ return new TyperollClient({ baseUrl: config.portal, apiKey: env.TYPEROLL_API_KEY, defaultVersion: config.version, fetchImpl });
144
+ }
145
+ export async function verifyWorkspaceBinding(config, client) {
146
+ if (!config.site_id || !config.organization_id)
147
+ throw new Error('Set site_id and organization_id before starting a bound workspace connection.');
148
+ const site = await client.get(config.site_id, '');
149
+ if (site.id !== config.site_id || site.version_id !== config.version || site.organization_id !== config.organization_id)
150
+ throw new Error('Portal Site, Organization or Version does not match typeroll.json. Run doctor before editing.');
151
+ }
152
+ export async function doctor(dir, offline, env = process.env, fetchImpl) {
153
+ const checks = [];
154
+ const config = await readWorkspace(dir);
155
+ checks.push({ check: 'workspace', status: 'passed', message: 'Valid non-secret workspace configuration.' });
156
+ const ignore = await fs.readFile(path.join(dir, '.gitignore'), 'utf8').catch(() => '');
157
+ checks.push({ check: 'ignore_entries', status: ignores.every(rule => ignore.split(/\r?\n/).includes(rule)) ? 'passed' : 'required', message: 'Checks required .gitignore entries only. Run init if missing; inspect overriding rules and already tracked files separately.' });
158
+ if (!config.site_id)
159
+ checks.push({ check: 'site', status: 'required', message: 'Create/select a Site through hosted MCP or the portal, then set site_id in typeroll.json.' });
160
+ if (offline)
161
+ checks.push({ check: 'connection', status: 'not_checked', message: 'Offline check: authentication, permissions, Version and readiness were not checked.' });
162
+ else {
163
+ try {
164
+ const client = workspaceClient(config, env, fetchImpl);
165
+ await client.rootGet('sites');
166
+ checks.push({ check: 'authentication', status: 'passed', message: 'The portal accepted the credential.' });
167
+ if (config.site_id) {
168
+ const site = await client.get(config.site_id, '');
169
+ if (site.id !== config.site_id || site.version_id !== config.version)
170
+ throw new Error('Site or Version differs from the workspace binding.');
171
+ checks.push({ check: 'site_version', status: 'passed', message: 'Site and Version match the workspace.' });
172
+ checks.push({ check: 'organization', status: config.organization_id && config.organization_id === site.organization_id ? 'passed' : 'required', message: site.organization_id ? `Confirm organization_id matches ${site.organization_id}.` : 'This portal does not return the Site Organization ID; verify it before writes.' });
173
+ await client.get(config.site_id, 'capabilities');
174
+ checks.push({ check: 'capabilities', status: 'passed', message: 'Capabilities are readable. Read relevant block schemas and app documentation on demand.' });
175
+ }
176
+ checks.push({ check: 'writes_and_publishing', status: 'not_checked', message: 'Read-only doctor. No edits, media transfer, app activation or publication was attempted; check publishing/import readiness for those tasks.' });
177
+ }
178
+ catch (error) {
179
+ checks.push({ check: 'connection', status: 'required', message: error instanceof ApiError ? `Portal HTTP ${error.status}. Check credential scope, portal and Site/Version.` : 'Could not verify the connection. Check injected credentials, matching environment, network and Site/Version.' });
180
+ }
181
+ }
182
+ return { passed: !checks.some(check => check.status === 'required'), generator_version: VERSION, checks };
183
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@typeroll/mcp-server",
3
- "version": "0.45.22",
4
- "description": "Typeroll CMS MCP server \u2013 connect AI agents to the REST API to manage content and publish static websites.",
3
+ "version": "0.45.23",
4
+ "description": "Typeroll CMS MCP server – connect AI agents to the REST API to manage content and publish static websites.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -42,7 +42,8 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@modelcontextprotocol/sdk": "^1.0.0",
45
- "zod": "^3.23.0"
45
+ "zod": "^3.23.0",
46
+ "zod-to-json-schema": "3.25.2"
46
47
  },
47
48
  "devDependencies": {
48
49
  "typescript": "^5.6.0",
package/skills/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Typeroll CMS skills for AI agents
2
2
 
3
- Boilerplate skills that pair with [`@typeroll/mcp-server`](../packages/mcp-server/README.md).
3
+ Boilerplate skills that pair with [`@typeroll/mcp-server`](../README.md).
4
4
  Each one is a self-contained markdown file the agent reads when its
5
5
  description matches the user's request. Recipes call MCP tools; the agent
6
6
  adapts them to the specific job.
@@ -12,27 +12,21 @@ save first.
12
12
 
13
13
  ## Installation
14
14
 
15
- Copy whichever skills you need into your Claude Code skills directory.
16
- Either:
15
+ Prefer `list_skills` and `read_skill` over loading all recipes into context.
16
+ For optional local reference copies in an agent-neutral workspace:
17
17
 
18
- ```bash
19
- # project-scoped (recommended)
20
- mkdir -p .claude/skills
21
- cp skills/*.md .claude/skills/
22
-
23
- # or user-scoped (available in every project)
24
- cp skills/*.md ~/.claude/skills/
18
+ ```sh
19
+ npx @typeroll/mcp-server@0.45.23 init ./my-site --recipes
20
+ npx @typeroll/mcp-server@0.45.23 init ./my-site --recipes --update
25
21
  ```
26
22
 
27
- Symlinks work too, so you can stay in sync with the upstream:
28
-
29
- ```bash
30
- ln -s "$PWD/skills/tr-migrate-wp.md" ~/.claude/skills/
31
- ```
23
+ The files live in `typeroll-skills/`. Updates preserve user edits. Copying Markdown
24
+ is not a guarantee of automatic skill loading in any client; follow its current
25
+ instructions. Read the relevant recipe when the task calls for it.
32
26
 
33
27
  ## The skills
34
28
 
35
- ### Bygga och designa
29
+ ### Build and design
36
30
 
37
31
  | File | When it triggers | What it does |
38
32
  |---|---|---|
@@ -42,7 +36,7 @@ ln -s "$PWD/skills/tr-migrate-wp.md" ~/.claude/skills/
42
36
  | `tr-content-write.md` | "write a page about…", "draft copy for…" | Discovery first (settings + sample pages), then drafts in the site's voice, previews, iterates. |
43
37
  | `tr-images.md` | "make an image / hero / illustration", media uploads | Generates locally → signed upload URL → metadata patch → embed. |
44
38
 
45
- ### Funktioner
39
+ ### Features
46
40
 
47
41
  | File | When it triggers | What it does |
48
42
  |---|---|---|
@@ -51,7 +45,7 @@ ln -s "$PWD/skills/tr-migrate-wp.md" ~/.claude/skills/
51
45
  | `tr-page-template.md` | Reusable Page templates: audio players, chapter lists, guest cards, image galleries — anything needing loops/nested data | Compose native blocks around `template_content_slot`; bind Page metadata, structured fields and references. |
52
46
  | `tr-seo.md` | "SEO", "meta descriptions", "structured data" | Audit → fix titles/descriptions → OG images → JSON-LD → robots.txt → deploy. |
53
47
 
54
- ### Importera innehåll
48
+ ### Import content
55
49
 
56
50
  | File | When it triggers | What it does |
57
51
  |---|---|---|
@@ -63,14 +57,13 @@ ln -s "$PWD/skills/tr-migrate-wp.md" ~/.claude/skills/
63
57
 
64
58
  ## Prerequisites for every skill
65
59
 
66
- 1. `@typeroll/mcp-server` configured in the agent’s MCP settings with a valid
67
- `TYPEROLL_API_KEY` and `TYPEROLL_API_URL`.
68
- 2. The agent has read `AGENTS.md` (ships with the MCP package — see
69
- `node_modules/@typeroll/mcp-server/AGENTS.md` after install, or
70
- reference it directly).
60
+ 1. A working hosted or local Typeroll MCP connection for API operations. Local
61
+ stdio uses an injected API key; hosted clients may use OAuth or bearer headers.
62
+ 2. The relevant guide sections and the selected Site/Version's capabilities.
63
+ Use `read_guide sections_only=true` before requesting individual sections.
71
64
 
72
- If those are missing, every skill will fail at the first MCP call with
73
- "Missing bearer token" or "Invalid or revoked token".
65
+ Recipe discovery and reads are local reference operations; they do not require a
66
+ Site. API operations enforce authentication, scope and setup independently.
74
67
 
75
68
  ## Authoring more
76
69
 
@@ -23,6 +23,21 @@ structured, per-field editing, native full-bleed sections, responsive
23
23
  breakpoints, and templates. HTML mode is the secondary path for
24
24
  hand-crafted one-offs and migrated content (section at the end).
25
25
 
26
+ ## Workspace and context
27
+
28
+ An optional agent-neutral workspace stores the brief, decisions and QA. It does
29
+ not create a Site or replace CMS content. See the public Agent workspace guide.
30
+ For compact MCP: search_tools, describe_tool, then the appropriate read/write/admin
31
+ wrapper. Load only the relevant guide section and recipe. Discover site apps with
32
+ read_app_documentation when needed; do not bundle private app guides locally.
33
+
34
+ No Site yet? Use hosted MCP with an Organization key and create_site, or create it
35
+ in the portal. Read back its ID before editing. Use one explicit Site/Version for
36
+ related operations. Larger changes to an existing site should use a branch.
37
+
38
+ Before importing content or a media library, require get_import_readiness to pass.
39
+ Publishing accounts may remain unconfigured while authoring new drafts and previews.
40
+
26
41
  ## Preconditions
27
42
 
28
43
  - `@typeroll/mcp-server` configured with a valid `TYPEROLL_API_KEY`.
@@ -196,9 +211,9 @@ truth):
196
211
  - **`core/heading`** decouples `level` (h1–h6, semantics) from `size`
197
212
  (visual) — exactly one `level: h1` per page.
198
213
  - **Images:** `core/image` with an uploaded media URL. The build pipeline
199
- automatically emits responsive `<picture>` with AVIF/WebP variants
200
- (run `generate_image_variants` after upload) — the in-portal preview
201
- shows a plain `<img>`, the deployed site gets the upgrade. Use the
214
+ emits responsive output from prepared media. Upload through a signed direct
215
+ grant, finalize it, and use returned IDs/URLs. Follow media preparation status;
216
+ do not routinely regenerate unchanged images or use legacy maintenance tools. Use the
202
217
  `radius` field for rounded corners.
203
218
  - **Repeaters/listings:** `core/page_list`, `gallery`,
204
219
  `feature_grid` etc. — alias blocks over `core/repeater`. Use these for
@@ -302,7 +317,8 @@ edits, not full-page rewrites.
302
317
 
303
318
  ### 7. Deploy
304
319
 
305
- When the user approves:
320
+ When the user approves, verify read_publishing_readiness and save the intended
321
+ Version before deploying. A published CMS status is not a live deployment.
306
322
  ```
307
323
  trigger_deploy
308
324
  get_deploy_status job_id=<id>