@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.
- package/README.md +39 -13
- package/dist/index.js +28 -63
- package/dist/install-skills.js +130 -0
- package/dist/server.js +129 -0
- package/dist/tools/pages.js +11 -2
- package/dist/tools/partials.js +9 -4
- package/dist/tools/search.js +6 -3
- package/package.json +7 -1
- package/skills/README.md +26 -5
- package/skills/tr-blog.md +171 -0
- package/skills/tr-brand.md +169 -0
- package/skills/tr-collection-template.md +262 -0
- package/skills/tr-forms.md +243 -0
- package/skills/tr-import-url.md +173 -0
- package/skills/tr-migrate-astro.md +278 -0
- package/skills/tr-new-site.md +198 -0
- package/skills/tr-page-template.md +168 -0
- package/skills/tr-seo.md +179 -0
package/README.md
CHANGED
|
@@ -1,20 +1,46 @@
|
|
|
1
1
|
# @typeroll/mcp-server
|
|
2
2
|
|
|
3
3
|
Model Context Protocol server for the [Typeroll](https://typeroll.com)
|
|
4
|
-
public API. Lets Claude
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
public API. Lets Claude (Desktop / claude.ai / Code) manage a Typeroll
|
|
5
|
+
site through the same tool surface a human agency would use: read and
|
|
6
|
+
write pages, partials, collections, media, redirects, versions; trigger
|
|
7
|
+
deploys; mint preview links.
|
|
8
8
|
|
|
9
9
|
The server is a **thin transport adapter** — every tool wraps one HTTP
|
|
10
|
-
endpoint of the Typeroll REST API. Auth happens at the API layer with
|
|
11
|
-
|
|
10
|
+
endpoint of the Typeroll REST API. Auth happens at the API layer with a
|
|
11
|
+
site- or org-scoped key; the MCP just carries the bearer through.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Two ways to connect
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
- **Hosted (Claude Desktop / claude.ai) — paste a URL.** No CLI, no
|
|
16
|
+
Node.js install. In Claude open **Settings → Connectors → Add custom
|
|
17
|
+
connector** and paste `https://app.typeroll.com/api/mcp`
|
|
18
|
+
(or `https://<your-self-hosted-portal>/mcp`). Claude opens a consent
|
|
19
|
+
page; paste your Typeroll API key there.
|
|
20
|
+
- **Stdio (Claude Code) — one `claude mcp add` command.** Best for local
|
|
21
|
+
dev / agency staff already in a terminal. Instructions below.
|
|
22
|
+
|
|
23
|
+
This npm package is the stdio transport. The hosted endpoint ships as
|
|
24
|
+
part of the Typeroll portal itself — same tool surface, same package
|
|
25
|
+
under the hood.
|
|
26
|
+
|
|
27
|
+
## Key scopes
|
|
28
|
+
|
|
29
|
+
- **Org-scoped key** (created at `/app/settings/api-keys`) — one
|
|
30
|
+
credential covers every site in your org *and* every site shared into
|
|
31
|
+
your org. The default for the hosted Claude connector. Stdio works too
|
|
32
|
+
if you set `TYPEROLL_SITE_ID` so the install binds to one site.
|
|
33
|
+
- **Site-scoped key** (created at `/app/sites/{siteId}/settings/api-keys`) —
|
|
34
|
+
tighter blast radius for a single-site credential, e.g. one you'd
|
|
35
|
+
hand to a customer for a self-managed site.
|
|
36
|
+
|
|
37
|
+
Both look like `typeroll_live_…`; revoke either from the portal and any
|
|
38
|
+
client using it stops working immediately.
|
|
39
|
+
|
|
40
|
+
## Stdio quick start (Claude Code)
|
|
41
|
+
|
|
42
|
+
1. **Create an API key** in your Typeroll portal — see the two scope
|
|
43
|
+
options above. Org-scoped is the right default.
|
|
18
44
|
|
|
19
45
|
2. **Add the server to Claude Code.** Drop this into `~/.claude.json`
|
|
20
46
|
(or your local `.claude/config.json`):
|
|
@@ -46,13 +72,13 @@ a site-scoped key; the MCP just carries the bearer through.
|
|
|
46
72
|
Claude will call `get_site`, `list_pages`, `list_partials`,
|
|
47
73
|
`list_collections` in sequence and report back.
|
|
48
74
|
|
|
49
|
-
## Environment variables
|
|
75
|
+
## Environment variables (stdio)
|
|
50
76
|
|
|
51
77
|
| Var | Required | Description |
|
|
52
78
|
|-----------------|----------|-------------|
|
|
53
79
|
| `TYPEROLL_API_URL` | yes | Base URL of your Typeroll portal. |
|
|
54
|
-
| `TYPEROLL_API_KEY` | yes | A `typeroll_live_…` bearer token
|
|
55
|
-
| `TYPEROLL_SITE_ID` |
|
|
80
|
+
| `TYPEROLL_API_KEY` | yes | A `typeroll_live_…` bearer token. |
|
|
81
|
+
| `TYPEROLL_SITE_ID` | sometimes | Pin to a specific site. Required when using an org-scoped key over stdio (the install can only target one site at a time); auto-detected for site-scoped keys. |
|
|
56
82
|
|
|
57
83
|
## What the agent should read first
|
|
58
84
|
|
package/dist/index.js
CHANGED
|
@@ -3,48 +3,31 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Reads two env vars at startup:
|
|
5
5
|
// TYPEROLL_API_URL — base URL of the portal (e.g. https://app.typeroll.com)
|
|
6
|
-
// TYPEROLL_API_KEY — a typeroll_live_... bearer token from
|
|
6
|
+
// TYPEROLL_API_KEY — a typeroll_live_... bearer token from the portal
|
|
7
7
|
//
|
|
8
8
|
// Optionally:
|
|
9
9
|
// TYPEROLL_SITE_ID — pre-set the site id. If omitted we discover it by
|
|
10
|
-
// calling GET /v1/sites at startup
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// boring to maintain and lets the customer's self-hosted portal serve the
|
|
16
|
-
// same MCP without needing two implementations to stay in sync.
|
|
17
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
// calling GET /v1/sites at startup. For site-scoped keys
|
|
11
|
+
// that endpoint always returns exactly one. For org-scoped
|
|
12
|
+
// keys (introduced for the hosted MCP connector) it can
|
|
13
|
+
// return many — in that case TYPEROLL_SITE_ID is required
|
|
14
|
+
// so this stdio invocation maps onto one specific site.
|
|
18
15
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
19
16
|
import { TyperollClient } from './client.js';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
import { mediaTools } from './tools/media.js';
|
|
24
|
-
import { redirectTools } from './tools/redirects.js';
|
|
25
|
-
import { formTools } from './tools/forms.js';
|
|
26
|
-
import { searchTools } from './tools/search.js';
|
|
27
|
-
import { bulkTools } from './tools/bulk.js';
|
|
28
|
-
import { versionTools } from './tools/versions.js';
|
|
29
|
-
import { deployTools } from './tools/deploy.js';
|
|
30
|
-
import { previewTools } from './tools/preview.js';
|
|
31
|
-
import { blockTypeTools } from './tools/block-types.js';
|
|
32
|
-
import { pageBlockTools } from './tools/page-blocks.js';
|
|
33
|
-
import { settingsTools } from './tools/settings.js';
|
|
34
|
-
import { siteTools } from './tools/sites.js';
|
|
35
|
-
const VERSION = '0.1.0';
|
|
17
|
+
import { runInstallSkillsCli } from './install-skills.js';
|
|
18
|
+
import { buildServer } from './server.js';
|
|
19
|
+
const VERSION = '0.7.8';
|
|
36
20
|
async function resolveSiteId(client) {
|
|
37
21
|
const fromEnv = process.env.TYPEROLL_SITE_ID?.trim();
|
|
38
22
|
if (fromEnv)
|
|
39
23
|
return fromEnv;
|
|
40
|
-
// Per-site keys → exactly one site. We fetch it so the agent doesn't
|
|
41
|
-
// have to know its own site id ahead of time.
|
|
42
24
|
const res = await client.rootGet('sites');
|
|
43
25
|
if (!res.sites || res.sites.length === 0) {
|
|
44
26
|
throw new Error('No sites returned for this API key. Check the key is valid.');
|
|
45
27
|
}
|
|
46
28
|
if (res.sites.length > 1) {
|
|
47
|
-
throw new Error(`This key authorises ${res.sites.length} sites; set TYPEROLL_SITE_ID to pick one
|
|
29
|
+
throw new Error(`This key authorises ${res.sites.length} sites; set TYPEROLL_SITE_ID to pick one. ` +
|
|
30
|
+
'Org-scoped keys span multiple sites — stdio currently binds to one at a time.');
|
|
48
31
|
}
|
|
49
32
|
return res.sites[0].id;
|
|
50
33
|
}
|
|
@@ -53,6 +36,18 @@ function bail(message) {
|
|
|
53
36
|
process.exit(1);
|
|
54
37
|
}
|
|
55
38
|
async function main() {
|
|
39
|
+
const argv = process.argv.slice(2);
|
|
40
|
+
if (argv[0] === 'install-skills') {
|
|
41
|
+
const code = await runInstallSkillsCli(argv.slice(1));
|
|
42
|
+
process.exit(code);
|
|
43
|
+
}
|
|
44
|
+
if (argv[0] === '--help' || argv[0] === '-h' || argv[0] === 'help') {
|
|
45
|
+
console.error('Usage:');
|
|
46
|
+
console.error(' typeroll-mcp Start the MCP server (reads TYPEROLL_API_URL and TYPEROLL_API_KEY)');
|
|
47
|
+
console.error(' typeroll-mcp install-skills <dir> [-f] Copy bundled skill files to <dir>');
|
|
48
|
+
console.error(' typeroll-mcp --help Show this help');
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
56
51
|
const apiUrl = process.env.TYPEROLL_API_URL?.trim();
|
|
57
52
|
const apiKey = process.env.TYPEROLL_API_KEY?.trim();
|
|
58
53
|
if (!apiUrl)
|
|
@@ -70,41 +65,11 @@ async function main() {
|
|
|
70
65
|
catch (e) {
|
|
71
66
|
bail(`Failed to discover site: ${e instanceof Error ? e.message : String(e)}`);
|
|
72
67
|
}
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
...partialTools,
|
|
79
|
-
...blockTypeTools,
|
|
80
|
-
...pageBlockTools,
|
|
81
|
-
...collectionTools,
|
|
82
|
-
...mediaTools,
|
|
83
|
-
...redirectTools,
|
|
84
|
-
...formTools,
|
|
85
|
-
...settingsTools,
|
|
86
|
-
...searchTools,
|
|
87
|
-
...bulkTools,
|
|
88
|
-
...versionTools,
|
|
89
|
-
...deployTools,
|
|
90
|
-
...previewTools,
|
|
91
|
-
];
|
|
92
|
-
// The SDK's registerTool signature has very deep generic constraints
|
|
93
|
-
// (ZodRawShape × AnySchema × annotations) that TypeScript can't unify
|
|
94
|
-
// when we iterate heterogeneous tools at runtime. We cast the bag once
|
|
95
|
-
// here and rely on our own ToolDef contract for type safety.
|
|
96
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
97
|
-
const register = server.registerTool.bind(server);
|
|
98
|
-
for (const tool of allTools) {
|
|
99
|
-
register(tool.name, {
|
|
100
|
-
description: tool.description,
|
|
101
|
-
...(tool.inputSchema ? { inputSchema: tool.inputSchema } : {}),
|
|
102
|
-
},
|
|
103
|
-
// The SDK's callback gets the args object (when inputSchema is set) or
|
|
104
|
-
// no args (when it isn't). Both are valid for our handler signature.
|
|
105
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
-
async (args) => tool.handler(args ?? {}, deps));
|
|
107
|
-
}
|
|
68
|
+
const server = buildServer({
|
|
69
|
+
client,
|
|
70
|
+
fixedSiteId: siteId,
|
|
71
|
+
info: { name: 'typeroll', version: VERSION },
|
|
72
|
+
});
|
|
108
73
|
const transport = new StdioServerTransport();
|
|
109
74
|
await server.connect(transport);
|
|
110
75
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// install-skills subcommand for the typeroll-mcp CLI.
|
|
2
|
+
//
|
|
3
|
+
// Copies the bundled skill markdown files from this package's `skills/`
|
|
4
|
+
// directory into a destination directory the user provides. Used to seed
|
|
5
|
+
// `.claude/skills/` (project scope) or `~/.claude/skills/` (user scope) so
|
|
6
|
+
// Claude Code picks up Typeroll-specific skill recipes.
|
|
7
|
+
//
|
|
8
|
+
// This subcommand does NOT need TYPEROLL_API_URL / TYPEROLL_API_KEY — it's a
|
|
9
|
+
// pure file copy. Putting the dispatch in index.ts before the env-var
|
|
10
|
+
// validation is the only thing that matters for that.
|
|
11
|
+
import { promises as fs } from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
// Resolve the bundled skills directory. At runtime this file lives at
|
|
15
|
+
// <package>/dist/install-skills.js (after tsc); skills/ is a sibling of dist/.
|
|
16
|
+
// fileURLToPath gives us a real OS path that works on every platform.
|
|
17
|
+
function skillsDir() {
|
|
18
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
return path.resolve(here, '..', 'skills');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Copy all `tr-*.md` files from the bundled skills directory to `dest`.
|
|
23
|
+
* Returns a summary so callers can render output. Throws on filesystem errors
|
|
24
|
+
* the caller didn't ask for.
|
|
25
|
+
*/
|
|
26
|
+
export async function installSkills(opts) {
|
|
27
|
+
const source = opts.sourceDir ?? skillsDir();
|
|
28
|
+
const force = opts.force ?? false;
|
|
29
|
+
// Validate the source dir exists. If this fails the package install is
|
|
30
|
+
// broken (skills/ missing from the tarball) — surface clearly.
|
|
31
|
+
try {
|
|
32
|
+
const stat = await fs.stat(source);
|
|
33
|
+
if (!stat.isDirectory()) {
|
|
34
|
+
throw new Error(`bundled skills path is not a directory: ${source}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
39
|
+
throw new Error(`could not find bundled skills directory at ${source}: ${reason}`);
|
|
40
|
+
}
|
|
41
|
+
// Resolve destination. Create it if missing (mkdir -p semantics).
|
|
42
|
+
const dest = path.resolve(opts.dest);
|
|
43
|
+
await fs.mkdir(dest, { recursive: true });
|
|
44
|
+
// Only copy tr-*.md — README.md in skills/ is package-internal, not a skill.
|
|
45
|
+
const entries = await fs.readdir(source);
|
|
46
|
+
const skillFiles = entries.filter((f) => f.startsWith('tr-') && f.endsWith('.md')).sort();
|
|
47
|
+
const copied = [];
|
|
48
|
+
const skipped = [];
|
|
49
|
+
for (const file of skillFiles) {
|
|
50
|
+
const target = path.join(dest, file);
|
|
51
|
+
if (!force) {
|
|
52
|
+
try {
|
|
53
|
+
await fs.access(target);
|
|
54
|
+
skipped.push(file);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// File doesn't exist — proceed to copy.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
await fs.copyFile(path.join(source, file), target);
|
|
62
|
+
copied.push(file);
|
|
63
|
+
}
|
|
64
|
+
return { destination: dest, copied, skipped };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* CLI entry point — parses argv (after the subcommand token has been
|
|
68
|
+
* removed) and prints human-readable output. Returns exit code.
|
|
69
|
+
*/
|
|
70
|
+
export async function runInstallSkillsCli(args) {
|
|
71
|
+
let dest;
|
|
72
|
+
let force = false;
|
|
73
|
+
for (const arg of args) {
|
|
74
|
+
if (arg === '--force' || arg === '-f') {
|
|
75
|
+
force = true;
|
|
76
|
+
}
|
|
77
|
+
else if (arg === '--help' || arg === '-h') {
|
|
78
|
+
printHelp();
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
else if (!dest && !arg.startsWith('-')) {
|
|
82
|
+
dest = arg;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
console.error(`typeroll-mcp install-skills: unrecognised argument: ${arg}`);
|
|
86
|
+
printHelp();
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!dest) {
|
|
91
|
+
console.error('typeroll-mcp install-skills: missing destination directory.');
|
|
92
|
+
printHelp();
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const result = await installSkills({ dest, force });
|
|
97
|
+
const total = result.copied.length + result.skipped.length;
|
|
98
|
+
console.log(`typeroll-mcp: installed skills to ${result.destination}`);
|
|
99
|
+
console.log(` copied: ${result.copied.length}/${total}`);
|
|
100
|
+
if (result.skipped.length > 0) {
|
|
101
|
+
console.log(` skipped: ${result.skipped.length} (already exist; rerun with --force to overwrite)`);
|
|
102
|
+
for (const f of result.skipped)
|
|
103
|
+
console.log(` - ${f}`);
|
|
104
|
+
}
|
|
105
|
+
if (result.copied.length > 0) {
|
|
106
|
+
for (const f of result.copied)
|
|
107
|
+
console.log(` + ${f}`);
|
|
108
|
+
}
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
113
|
+
console.error(`typeroll-mcp install-skills: ${reason}`);
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function printHelp() {
|
|
118
|
+
console.error('');
|
|
119
|
+
console.error('Usage: npx @typeroll/mcp-server install-skills <destination> [--force]');
|
|
120
|
+
console.error('');
|
|
121
|
+
console.error('Copies bundled Typeroll skill markdown files to the destination directory.');
|
|
122
|
+
console.error('');
|
|
123
|
+
console.error('Common destinations:');
|
|
124
|
+
console.error(' .claude/skills Project-scoped (recommended)');
|
|
125
|
+
console.error(' ~/.claude/skills User-scoped (available in every project)');
|
|
126
|
+
console.error('');
|
|
127
|
+
console.error('Options:');
|
|
128
|
+
console.error(' --force, -f Overwrite files that already exist');
|
|
129
|
+
console.error(' --help, -h Show this help');
|
|
130
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Transport-agnostic MCP server builder. Wraps an McpServer instance with
|
|
2
|
+
// the full Typeroll tool surface and either binds it to a single fixed site
|
|
3
|
+
// (stdio, site-scoped HTTP) or makes it multi-site (org-scoped HTTP).
|
|
4
|
+
//
|
|
5
|
+
// In multi-site mode every tool's input schema gains a required `site_id`
|
|
6
|
+
// argument; the handler wrapper validates that the id is in the allowed
|
|
7
|
+
// list AND that the share's permission level covers the operation (read
|
|
8
|
+
// vs write). This is the plumbing the hosted MCP plan calls out as
|
|
9
|
+
// safety-critical when one connector can touch many sites.
|
|
10
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { pageTools } from './tools/pages.js';
|
|
13
|
+
import { partialTools } from './tools/partials.js';
|
|
14
|
+
import { collectionTools } from './tools/collections.js';
|
|
15
|
+
import { mediaTools } from './tools/media.js';
|
|
16
|
+
import { redirectTools } from './tools/redirects.js';
|
|
17
|
+
import { formTools } from './tools/forms.js';
|
|
18
|
+
import { searchTools } from './tools/search.js';
|
|
19
|
+
import { bulkTools } from './tools/bulk.js';
|
|
20
|
+
import { versionTools } from './tools/versions.js';
|
|
21
|
+
import { deployTools } from './tools/deploy.js';
|
|
22
|
+
import { previewTools } from './tools/preview.js';
|
|
23
|
+
import { blockTypeTools } from './tools/block-types.js';
|
|
24
|
+
import { pageBlockTools } from './tools/page-blocks.js';
|
|
25
|
+
import { settingsTools } from './tools/settings.js';
|
|
26
|
+
import { siteTools } from './tools/sites.js';
|
|
27
|
+
import { fail } from './tools/helpers.js';
|
|
28
|
+
const PERM_RANK = { read: 0, write: 1, admin: 2 };
|
|
29
|
+
/**
|
|
30
|
+
* Classify a tool by name into the minimum permission needed. We keep this
|
|
31
|
+
* conservative — anything that mutates is `write`. The MCP route's per-call
|
|
32
|
+
* gate then checks `effect <= sitePermission`.
|
|
33
|
+
*
|
|
34
|
+
* Naming convention is followed by all 16 tool files: read paths are
|
|
35
|
+
* `list_*` / `read_*` / `get_*` / `preview_*` / `search_*`; everything else
|
|
36
|
+
* mutates.
|
|
37
|
+
*/
|
|
38
|
+
function effectFor(name) {
|
|
39
|
+
if (name.startsWith('list_') ||
|
|
40
|
+
name.startsWith('read_') ||
|
|
41
|
+
name.startsWith('get_') ||
|
|
42
|
+
name.startsWith('batch_read_') ||
|
|
43
|
+
name.startsWith('preview_') ||
|
|
44
|
+
name.startsWith('search_')) {
|
|
45
|
+
return 'read';
|
|
46
|
+
}
|
|
47
|
+
return 'write';
|
|
48
|
+
}
|
|
49
|
+
const DEFAULT_INFO = { name: 'typeroll', version: '0.7.8' };
|
|
50
|
+
export function buildServer(options) {
|
|
51
|
+
if (!options.fixedSiteId && !options.allowedSites) {
|
|
52
|
+
throw new Error('buildServer: either fixedSiteId or allowedSites must be provided');
|
|
53
|
+
}
|
|
54
|
+
if (options.fixedSiteId && options.allowedSites) {
|
|
55
|
+
throw new Error('buildServer: provide only one of fixedSiteId / allowedSites');
|
|
56
|
+
}
|
|
57
|
+
const server = new McpServer(options.info ?? DEFAULT_INFO, {
|
|
58
|
+
capabilities: { tools: {} },
|
|
59
|
+
});
|
|
60
|
+
const allTools = [
|
|
61
|
+
...siteTools,
|
|
62
|
+
...pageTools,
|
|
63
|
+
...partialTools,
|
|
64
|
+
...blockTypeTools,
|
|
65
|
+
...pageBlockTools,
|
|
66
|
+
...collectionTools,
|
|
67
|
+
...mediaTools,
|
|
68
|
+
...redirectTools,
|
|
69
|
+
...formTools,
|
|
70
|
+
...settingsTools,
|
|
71
|
+
...searchTools,
|
|
72
|
+
...bulkTools,
|
|
73
|
+
...versionTools,
|
|
74
|
+
...deployTools,
|
|
75
|
+
...previewTools,
|
|
76
|
+
];
|
|
77
|
+
// SDK's registerTool has deeply-nested generics we can't unify across a
|
|
78
|
+
// heterogeneous tool list at compile time — same shrug as stdio's index.ts.
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
80
|
+
const register = server.registerTool.bind(server);
|
|
81
|
+
const isMultiSite = !!options.allowedSites;
|
|
82
|
+
const allowedById = new Map((options.allowedSites ?? []).map((s) => [s.siteId, s]));
|
|
83
|
+
for (const tool of allTools) {
|
|
84
|
+
const effect = effectFor(tool.name);
|
|
85
|
+
let schema = tool.inputSchema;
|
|
86
|
+
if (isMultiSite) {
|
|
87
|
+
// Append site_id to the tool's existing input schema. We mutate a copy
|
|
88
|
+
// so we don't pollute the imported ToolDef.
|
|
89
|
+
const siteIdField = {
|
|
90
|
+
site_id: z
|
|
91
|
+
.string()
|
|
92
|
+
.describe(`Required. The id of the site this call targets. Use list_sites to discover ids; available sites for this connection: ${(options.allowedSites ?? []).map((s) => s.siteId).join(', ') || '(none)'}.`),
|
|
93
|
+
};
|
|
94
|
+
schema = { ...(tool.inputSchema ?? {}), ...siteIdField };
|
|
95
|
+
}
|
|
96
|
+
register(tool.name, {
|
|
97
|
+
description: tool.description,
|
|
98
|
+
...(schema ? { inputSchema: schema } : {}),
|
|
99
|
+
},
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
+
async (args) => {
|
|
102
|
+
const rawArgs = args ?? {};
|
|
103
|
+
let siteId;
|
|
104
|
+
if (isMultiSite) {
|
|
105
|
+
const provided = typeof rawArgs.site_id === 'string' ? rawArgs.site_id.trim() : '';
|
|
106
|
+
if (!provided) {
|
|
107
|
+
return fail(new Error('site_id is required. This connector covers multiple sites; pick one from list_sites.'));
|
|
108
|
+
}
|
|
109
|
+
const match = allowedById.get(provided);
|
|
110
|
+
if (!match) {
|
|
111
|
+
return fail(new Error(`site_id "${provided}" is not accessible by this connection. Use list_sites to see allowed ids.`));
|
|
112
|
+
}
|
|
113
|
+
if (PERM_RANK[match.permission] < PERM_RANK[effect]) {
|
|
114
|
+
return fail(new Error(`Tool "${tool.name}" requires ${effect} permission on site "${provided}"; this connection has ${match.permission}.`));
|
|
115
|
+
}
|
|
116
|
+
siteId = provided;
|
|
117
|
+
// Strip site_id from the args we pass downstream so individual
|
|
118
|
+
// tool schemas (which don't declare it) don't reject the call.
|
|
119
|
+
delete rawArgs.site_id;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
siteId = options.fixedSiteId;
|
|
123
|
+
}
|
|
124
|
+
const deps = { client: options.client, siteId };
|
|
125
|
+
return tool.handler(rawArgs, deps);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return server;
|
|
129
|
+
}
|
package/dist/tools/pages.js
CHANGED
|
@@ -40,14 +40,23 @@ export const pageTools = [
|
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
name: 'batch_read_pages',
|
|
43
|
-
description: 'Read up to 200 pages in a single call. Use to bulk-load context before a redesign sweep.',
|
|
43
|
+
description: 'Read up to 200 pages in a single call. Returns pages_by_id — a map keyed by page_id for easy lookup — plus a not_found list. Use to bulk-load context before a redesign sweep.',
|
|
44
44
|
inputSchema: {
|
|
45
45
|
page_ids: z.array(z.string()).min(1).max(200),
|
|
46
46
|
version: versionParam,
|
|
47
47
|
},
|
|
48
48
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
49
49
|
const res = await client.post(siteId, 'pages/batch-read', { page_ids: args.page_ids }, v(args.version));
|
|
50
|
-
|
|
50
|
+
// Transform array → map keyed by page_id for ergonomic access.
|
|
51
|
+
const pages_by_id = {};
|
|
52
|
+
const not_found = [];
|
|
53
|
+
for (const entry of res.pages ?? []) {
|
|
54
|
+
if (entry.found && entry.page)
|
|
55
|
+
pages_by_id[entry.page_id] = entry.page;
|
|
56
|
+
else
|
|
57
|
+
not_found.push(entry.page_id);
|
|
58
|
+
}
|
|
59
|
+
return ok({ pages_by_id, not_found });
|
|
51
60
|
}),
|
|
52
61
|
},
|
|
53
62
|
{
|
package/dist/tools/partials.js
CHANGED
|
@@ -54,14 +54,19 @@ export const partialTools = [
|
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
56
|
name: 'replace_partial',
|
|
57
|
-
description: 'Full replace of a global block (PUT).
|
|
57
|
+
description: 'Full replace of a global block (PUT). Pass html_content (and optionally name/status) directly — no wrapper object needed. ' +
|
|
58
|
+
'kind is auto-inferred from partial_id ("header" → header, "footer" → footer, anything else → free). ' +
|
|
59
|
+
'For incremental edits (changing one field without replacing the whole block) use update_partial instead.',
|
|
58
60
|
inputSchema: {
|
|
59
|
-
partial_id: z.string(),
|
|
60
|
-
|
|
61
|
+
partial_id: z.string().describe('"header", "footer", or a free-block kebab-id.'),
|
|
62
|
+
html_content: z.string().describe('Full HTML for the block. Replaces any existing content.'),
|
|
63
|
+
name: z.string().optional().describe('Human-readable label shown in the UI.'),
|
|
64
|
+
status: z.enum(['draft', 'published']).optional(),
|
|
61
65
|
version: versionParam,
|
|
62
66
|
},
|
|
63
67
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
64
|
-
const
|
|
68
|
+
const { version, partial_id, ...fields } = args;
|
|
69
|
+
const res = await client.put(siteId, `partials/${encodeURIComponent(partial_id)}`, fields, v(version));
|
|
65
70
|
return ok(res);
|
|
66
71
|
}),
|
|
67
72
|
},
|
package/dist/tools/search.js
CHANGED
|
@@ -6,10 +6,13 @@ function v(version) {
|
|
|
6
6
|
export const searchTools = [
|
|
7
7
|
{
|
|
8
8
|
name: 'search_pages',
|
|
9
|
-
description: 'Search page bodies by literal substring or regex. Returns up to 500 matches each with an excerpt around the first hit. Use this to scope a redesign or a bulk replacement before running it.'
|
|
9
|
+
description: 'Search page bodies by literal substring or regex. Returns up to 500 matches each with an excerpt around the first hit. Use this to scope a redesign or a bulk replacement before running it. ' +
|
|
10
|
+
'Pass either "contains" (case-insensitive literal) or "regex" (JS regex source without slashes) — not both. ' +
|
|
11
|
+
'Example: search_pages({"contains": "kontakta oss"}) finds every page mentioning that phrase. ' +
|
|
12
|
+
'Example: search_pages({"regex": "\\\\d{3}-\\\\d{3}"}) finds pages with phone-number patterns.',
|
|
10
13
|
inputSchema: {
|
|
11
|
-
contains: z.string().optional().describe('Case-insensitive literal substring'),
|
|
12
|
-
regex: z.string().optional().describe('JS regex source
|
|
14
|
+
contains: z.string().optional().describe('Case-insensitive literal substring. Example: "kontakta oss"'),
|
|
15
|
+
regex: z.string().optional().describe('JS regex source without surrounding slashes, case-insensitive. Example: "\\\\d{3}-\\\\d{3}" matches phone patterns.'),
|
|
13
16
|
limit: z.number().int().min(1).max(500).optional(),
|
|
14
17
|
version: versionParam,
|
|
15
18
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typeroll/mcp-server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
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": {
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
"typeroll-mcp": "dist/index.js"
|
|
16
16
|
},
|
|
17
17
|
"main": "./dist/index.js",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./dist/index.js",
|
|
20
|
+
"./server": "./src/server.ts",
|
|
21
|
+
"./client": "./src/client.ts",
|
|
22
|
+
"./tools/helpers": "./src/tools/helpers.ts"
|
|
23
|
+
},
|
|
18
24
|
"files": [
|
|
19
25
|
"dist",
|
|
20
26
|
"AGENTS.md",
|
package/skills/README.md
CHANGED
|
@@ -27,13 +27,34 @@ ln -s "$PWD/skills/tr-migrate-wp.md" ~/.claude/skills/
|
|
|
27
27
|
|
|
28
28
|
## The skills
|
|
29
29
|
|
|
30
|
+
### Bygga och designa
|
|
31
|
+
|
|
32
|
+
| File | When it triggers | What it does |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `tr-new-site.md` | "create a new site", "bootstrap a site for…" | Settings → header/footer partials → homepage → inner pages → deploy. Full setup recipe. |
|
|
35
|
+
| `tr-brand.md` | "create a brand", "choose colors", "design the look"| Palette recipes by mood, typography pairings, CSS variable setup, preview. |
|
|
36
|
+
| `tr-redesign-branch.md`| "redesign", "modernize", anything site-wide-design | Branch-isolated work with preview links, merge when approved. |
|
|
37
|
+
| `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. |
|
|
38
|
+
| `tr-images.md` | "make an image / hero / illustration", media uploads | Generates locally → signed upload URL → metadata patch → embed. |
|
|
39
|
+
|
|
40
|
+
### Funktioner
|
|
41
|
+
|
|
42
|
+
| File | When it triggers | What it does |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `tr-blog.md` | "add a blog", "set up news", "article/podcast section" | Collection schema with `item_template_html` + `route_template` → seed items → listing page with marker block → deploy. **No per-article `create_page` needed** — items materialise their own URLs. |
|
|
45
|
+
| `tr-forms.md` | "contact form", "add a form", "booking form" | Form definition → embed HTML with signed token → inline JS feedback → deploy. |
|
|
46
|
+
| `tr-directory.md` | Building a directory site, importing structured data | Schema → items → per-item URLs via `route_template` → listing page → preview → deploy. |
|
|
47
|
+
| `tr-collection-template.md` | Rich per-item detail pages: audio players, chapter lists, guest cards, image galleries — anything needing loops/nested data | Pre-render HTML into `*_html` fields when Mustache's `{{field}}` / `{{#field}}` aren't enough. Concrete recipes per pattern. |
|
|
48
|
+
| `tr-page-template.md` | Several pages share structure (category landings, service-detail variants) | Partials + `<x-include>` for HTML mode; formal `PageTemplate` via `set_page_template` for block mode. Refactor existing duplication. |
|
|
49
|
+
| `tr-seo.md` | "SEO", "meta descriptions", "structured data" | Audit → fix titles/descriptions → OG images → JSON-LD → robots.txt → deploy. |
|
|
50
|
+
|
|
51
|
+
### Importera innehåll
|
|
52
|
+
|
|
30
53
|
| File | When it triggers | What it does |
|
|
31
54
|
|---|---|---|
|
|
32
|
-
| `tr-migrate-wp.md` | "migrate from WordPress", a wp-json URL is mentioned
|
|
33
|
-
| `tr-
|
|
34
|
-
| `tr-
|
|
35
|
-
| `tr-directory.md` | Building a directory site, importing structured data | Schema → items → per-item URLs via `route_template` → listing page → preview → deploy |
|
|
36
|
-
| `tr-redesign-branch.md`| "redesign", "modernize", anything site-wide-design | Branch-isolated work with preview links, merge when approved |
|
|
55
|
+
| `tr-migrate-wp.md` | "migrate from WordPress", a wp-json URL is mentioned | Walks the WP REST, rebuilds each page in the target's design, transfers media, sets redirects, leaves everything as drafts for review. |
|
|
56
|
+
| `tr-migrate-astro.md` | "migrate an Astro site", "import from src/content" | Lifts Astro Content Collections (`src/content/*`) into Typeroll collections — zod schema → field list, frontmatter → field values, markdown body → richtext field. Translates standalone `src/pages/*` into Typeroll pages, maps `src/layouts` chunks into partials. |
|
|
57
|
+
| `tr-import-url.md` | "import from Squarespace/Wix/Webflow", any non-WP URL | Fetch → clean → adapt to target design → media transfer → draft pages → redirects → deploy. |
|
|
37
58
|
|
|
38
59
|
## Prerequisites for every skill
|
|
39
60
|
|