@typeroll/mcp-server 0.7.7 → 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 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 Code (or any MCP-compatible client) manage a
5
- Typeroll site through the same tool surface a human agency would use:
6
- read and write pages, partials, collections, media, redirects, versions;
7
- trigger deploys; mint preview links.
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
- a site-scoped key; the MCP just carries the bearer through.
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
- ## Quick start
13
+ ## Two ways to connect
14
14
 
15
- 1. **Create an API key** in your Typeroll portal at
16
- `/app/sites/{siteId}/settings/api-keys`. The key is shown once at
17
- creation copy it somewhere safe.
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 from the API keys page. |
55
- | `TYPEROLL_SITE_ID` | no | Pin the server to a specific site. Defaults to autodetect (calls `GET /v1/sites` per-site keys return exactly one). |
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,49 +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 /app/sites/{id}/settings/api-keys
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 (the v1 keys are
11
- // per-site so that endpoint always returns exactly one).
12
- //
13
- // Each tool wraps a single REST endpoint. The MCP server itself does no
14
- // business logic that all lives in the portal. This keeps the package
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
17
  import { runInstallSkillsCli } from './install-skills.js';
21
- import { pageTools } from './tools/pages.js';
22
- import { partialTools } from './tools/partials.js';
23
- import { collectionTools } from './tools/collections.js';
24
- import { mediaTools } from './tools/media.js';
25
- import { redirectTools } from './tools/redirects.js';
26
- import { formTools } from './tools/forms.js';
27
- import { searchTools } from './tools/search.js';
28
- import { bulkTools } from './tools/bulk.js';
29
- import { versionTools } from './tools/versions.js';
30
- import { deployTools } from './tools/deploy.js';
31
- import { previewTools } from './tools/preview.js';
32
- import { blockTypeTools } from './tools/block-types.js';
33
- import { pageBlockTools } from './tools/page-blocks.js';
34
- import { settingsTools } from './tools/settings.js';
35
- import { siteTools } from './tools/sites.js';
36
- const VERSION = '0.7.7';
18
+ import { buildServer } from './server.js';
19
+ const VERSION = '0.7.8';
37
20
  async function resolveSiteId(client) {
38
21
  const fromEnv = process.env.TYPEROLL_SITE_ID?.trim();
39
22
  if (fromEnv)
40
23
  return fromEnv;
41
- // Per-site keys → exactly one site. We fetch it so the agent doesn't
42
- // have to know its own site id ahead of time.
43
24
  const res = await client.rootGet('sites');
44
25
  if (!res.sites || res.sites.length === 0) {
45
26
  throw new Error('No sites returned for this API key. Check the key is valid.');
46
27
  }
47
28
  if (res.sites.length > 1) {
48
- 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.');
49
31
  }
50
32
  return res.sites[0].id;
51
33
  }
@@ -54,8 +36,6 @@ function bail(message) {
54
36
  process.exit(1);
55
37
  }
56
38
  async function main() {
57
- // Subcommand dispatch. These run without any of the MCP-server env-var
58
- // validation below, since they don't talk to the portal.
59
39
  const argv = process.argv.slice(2);
60
40
  if (argv[0] === 'install-skills') {
61
41
  const code = await runInstallSkillsCli(argv.slice(1));
@@ -85,41 +65,11 @@ async function main() {
85
65
  catch (e) {
86
66
  bail(`Failed to discover site: ${e instanceof Error ? e.message : String(e)}`);
87
67
  }
88
- const deps = { client, siteId };
89
- const server = new McpServer({ name: 'typeroll', version: VERSION }, { capabilities: { tools: {} } });
90
- const allTools = [
91
- ...siteTools,
92
- ...pageTools,
93
- ...partialTools,
94
- ...blockTypeTools,
95
- ...pageBlockTools,
96
- ...collectionTools,
97
- ...mediaTools,
98
- ...redirectTools,
99
- ...formTools,
100
- ...settingsTools,
101
- ...searchTools,
102
- ...bulkTools,
103
- ...versionTools,
104
- ...deployTools,
105
- ...previewTools,
106
- ];
107
- // The SDK's registerTool signature has very deep generic constraints
108
- // (ZodRawShape × AnySchema × annotations) that TypeScript can't unify
109
- // when we iterate heterogeneous tools at runtime. We cast the bag once
110
- // here and rely on our own ToolDef contract for type safety.
111
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
112
- const register = server.registerTool.bind(server);
113
- for (const tool of allTools) {
114
- register(tool.name, {
115
- description: tool.description,
116
- ...(tool.inputSchema ? { inputSchema: tool.inputSchema } : {}),
117
- },
118
- // The SDK's callback gets the args object (when inputSchema is set) or
119
- // no args (when it isn't). Both are valid for our handler signature.
120
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
121
- async (args) => tool.handler(args ?? {}, deps));
122
- }
68
+ const server = buildServer({
69
+ client,
70
+ fixedSiteId: siteId,
71
+ info: { name: 'typeroll', version: VERSION },
72
+ });
123
73
  const transport = new StdioServerTransport();
124
74
  await server.connect(transport);
125
75
  }
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeroll/mcp-server",
3
- "version": "0.7.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
@@ -41,17 +41,20 @@ ln -s "$PWD/skills/tr-migrate-wp.md" ~/.claude/skills/
41
41
 
42
42
  | File | When it triggers | What it does |
43
43
  |---|---|---|
44
- | `tr-blog.md` | "add a blog", "set up news", "article section" | Collection schema → seed items → listing page → per-article pages nav update deploy. |
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-seo.md` | "SEO", "meta descriptions", "structured data" | Audit fix titles/descriptions OG images JSON-LD robots.txt deploy. |
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. |
48
50
 
49
51
  ### Importera innehåll
50
52
 
51
53
  | File | When it triggers | What it does |
52
54
  |---|---|---|
53
- | `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. |
54
- | `tr-import-url.md` | "import from Squarespace/Wix/Webflow", any non-WP URL| Fetchcleanadapt to target designmedia transfer draft pages redirects deploy. |
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 bodyrichtext 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. |
55
58
 
56
59
  ## Prerequisites for every skill
57
60