@typeroll/mcp-server 0.33.2 → 0.35.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 +13 -6
- package/README.md +26 -4
- package/dist/bundled-content.js +3 -3
- package/dist/extension-cli.js +188 -0
- package/dist/index.js +6 -0
- package/dist/server.js +7 -1
- package/dist/tools/apps.js +36 -0
- package/dist/tools/forms.js +5 -7
- package/dist/version.js +1 -1
- package/package.json +6 -5
- package/skills/tr-forms.md +90 -257
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
function option(args, name) {
|
|
3
|
+
const index = args.indexOf(name);
|
|
4
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
5
|
+
}
|
|
6
|
+
async function readJson(path) {
|
|
7
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
8
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
9
|
+
throw new Error(`${path} must contain a JSON object`);
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
export function validateExtensionManifestShape(manifest) {
|
|
13
|
+
const errors = [];
|
|
14
|
+
if (manifest.schema_version !== 1)
|
|
15
|
+
errors.push('schema_version must be 1');
|
|
16
|
+
if (typeof manifest.id !== 'string' || !/^[a-z0-9]+(?:[.-][a-z0-9][a-z0-9-]*){2,}$/.test(manifest.id))
|
|
17
|
+
errors.push('id must be a lowercase namespaced identifier');
|
|
18
|
+
if (typeof manifest.name !== 'string' || !manifest.name.trim())
|
|
19
|
+
errors.push('name is required');
|
|
20
|
+
if (typeof manifest.version !== 'string' || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(manifest.version))
|
|
21
|
+
errors.push('version must be semver');
|
|
22
|
+
if (typeof manifest.runtime_compatibility !== 'string' || !manifest.runtime_compatibility.trim())
|
|
23
|
+
errors.push('runtime_compatibility is required');
|
|
24
|
+
if (!['private', 'unlisted', 'public'].includes(String(manifest.distribution)))
|
|
25
|
+
errors.push('distribution must be private, unlisted or public');
|
|
26
|
+
const developer = manifest.developer;
|
|
27
|
+
if (!developer || typeof developer.name !== 'string')
|
|
28
|
+
errors.push('developer.name is required');
|
|
29
|
+
const frontend = manifest.frontend;
|
|
30
|
+
const components = frontend?.components;
|
|
31
|
+
if (components !== undefined && !Array.isArray(components))
|
|
32
|
+
errors.push('frontend.components must be an array');
|
|
33
|
+
for (const [index, value] of (Array.isArray(components) ? components : []).entries()) {
|
|
34
|
+
const component = value;
|
|
35
|
+
const entry = component?.entry;
|
|
36
|
+
if (!component || typeof component.id !== 'string' || typeof component.label !== 'string')
|
|
37
|
+
errors.push(`frontend.components[${index}] needs id and label`);
|
|
38
|
+
if (!['bundled_component', 'embedded_app'].includes(String(component?.render_mode)))
|
|
39
|
+
errors.push(`frontend.components[${index}].render_mode is invalid`);
|
|
40
|
+
if (component?.render_mode === 'bundled_component' && (!entry || typeof entry.script_url !== 'string' || !/^[a-f0-9]{64}$/.test(String(entry.script_sha256)))) {
|
|
41
|
+
errors.push(`frontend.components[${index}] needs script_url and lowercase SHA-256`);
|
|
42
|
+
}
|
|
43
|
+
if (component?.render_mode === 'embedded_app' && (!entry || typeof entry.frame_url !== 'string'))
|
|
44
|
+
errors.push(`frontend.components[${index}] needs frame_url`);
|
|
45
|
+
}
|
|
46
|
+
return errors;
|
|
47
|
+
}
|
|
48
|
+
function executionOrigins(manifest) {
|
|
49
|
+
const urls = [];
|
|
50
|
+
const components = (manifest.frontend?.components ?? []);
|
|
51
|
+
for (const component of components) {
|
|
52
|
+
const entry = component.entry;
|
|
53
|
+
for (const key of ['script_url', 'style_url', 'frame_url'])
|
|
54
|
+
if (typeof entry?.[key] === 'string')
|
|
55
|
+
urls.push(entry[key]);
|
|
56
|
+
}
|
|
57
|
+
const pages = (manifest.admin?.pages ?? []);
|
|
58
|
+
for (const page of pages)
|
|
59
|
+
if (typeof page.launch_url === 'string')
|
|
60
|
+
urls.push(page.launch_url);
|
|
61
|
+
for (const value of [
|
|
62
|
+
manifest.gateway?.base_url,
|
|
63
|
+
manifest.events?.webhook_url,
|
|
64
|
+
manifest.auth?.pairing_url,
|
|
65
|
+
])
|
|
66
|
+
if (typeof value === 'string')
|
|
67
|
+
urls.push(value);
|
|
68
|
+
return [...new Set(urls.map((value) => new URL(value).origin))];
|
|
69
|
+
}
|
|
70
|
+
async function api(path, init = {}, allowed = []) {
|
|
71
|
+
const baseUrl = process.env.TYPEROLL_API_URL?.trim().replace(/\/$/, '');
|
|
72
|
+
const apiKey = process.env.TYPEROLL_API_KEY?.trim();
|
|
73
|
+
if (!baseUrl || !apiKey)
|
|
74
|
+
throw new Error('TYPEROLL_API_URL and TYPEROLL_API_KEY are required');
|
|
75
|
+
const headers = new Headers(init.headers);
|
|
76
|
+
headers.set('Authorization', `Bearer ${apiKey}`);
|
|
77
|
+
if (init.body)
|
|
78
|
+
headers.set('Content-Type', 'application/json');
|
|
79
|
+
const response = await fetch(`${baseUrl}${path}`, { ...init, headers });
|
|
80
|
+
const data = await response.json().catch(() => ({}));
|
|
81
|
+
if (!response.ok && !allowed.includes(response.status))
|
|
82
|
+
throw new Error(String(data.error ?? `Typeroll API returned ${response.status}`));
|
|
83
|
+
return { status: response.status, data };
|
|
84
|
+
}
|
|
85
|
+
function help() {
|
|
86
|
+
console.error('Usage:');
|
|
87
|
+
console.error(' typeroll extension validate [manifest]');
|
|
88
|
+
console.error(' typeroll extension push --draft [--manifest path]');
|
|
89
|
+
console.error(' typeroll extension install --site <site-id> [--manifest path] [--config config.json]');
|
|
90
|
+
console.error(' typeroll extension promote <version> [--manifest path]');
|
|
91
|
+
}
|
|
92
|
+
export async function runExtensionCli(args) {
|
|
93
|
+
const command = args[0];
|
|
94
|
+
if (!command || ['help', '--help', '-h'].includes(command)) {
|
|
95
|
+
help();
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
const manifestPath = option(args, '--manifest') ?? (command === 'validate' && args[1] && !args[1].startsWith('-') ? args[1] : 'typeroll-extension.json');
|
|
99
|
+
try {
|
|
100
|
+
const manifest = await readJson(manifestPath);
|
|
101
|
+
const errors = validateExtensionManifestShape(manifest);
|
|
102
|
+
if (errors.length) {
|
|
103
|
+
for (const error of errors)
|
|
104
|
+
console.error(`- ${error}`);
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
if (command === 'validate') {
|
|
108
|
+
console.log(`${manifestPath}: valid manifest v1 shape (server validation remains authoritative)`);
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
const extensionId = String(manifest.id);
|
|
112
|
+
if (command === 'push') {
|
|
113
|
+
const current = await api(`/api/developer/extensions/${encodeURIComponent(extensionId)}`, {}, [404]);
|
|
114
|
+
if (current.status === 404) {
|
|
115
|
+
const created = await api('/api/developer/extensions', {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
id: extensionId,
|
|
119
|
+
name: manifest.name,
|
|
120
|
+
distribution: manifest.distribution,
|
|
121
|
+
trusted_origins: executionOrigins(manifest),
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
if (created.data.client_secret)
|
|
125
|
+
console.error('Extension client secret (shown once):', created.data.client_secret);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const registered = current.data.extension;
|
|
129
|
+
const trusted = Array.isArray(registered?.trusted_origins) ? registered.trusted_origins.map(String) : [];
|
|
130
|
+
await api(`/api/developer/extensions/${encodeURIComponent(extensionId)}`, {
|
|
131
|
+
method: 'PATCH',
|
|
132
|
+
body: JSON.stringify({
|
|
133
|
+
name: manifest.name,
|
|
134
|
+
trusted_origins: [...new Set([...trusted, ...executionOrigins(manifest)])],
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const pushed = await api(`/api/developer/extensions/${encodeURIComponent(extensionId)}/versions`, {
|
|
139
|
+
method: 'POST', body: JSON.stringify({ manifest }),
|
|
140
|
+
});
|
|
141
|
+
console.log(`Saved ${extensionId}@${String(pushed.data.version?.version ?? manifest.version)} as draft`);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
if (command === 'promote') {
|
|
145
|
+
const version = args[1];
|
|
146
|
+
if (!version || version.startsWith('-'))
|
|
147
|
+
throw new Error('promote requires a version');
|
|
148
|
+
const promoted = await api(`/api/developer/extensions/${encodeURIComponent(extensionId)}/versions/${encodeURIComponent(version)}/publish`, { method: 'POST' });
|
|
149
|
+
console.log(`${extensionId}@${version}: ${String(promoted.data.version?.status ?? 'published')}`);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
if (command === 'install') {
|
|
153
|
+
const siteId = option(args, '--site');
|
|
154
|
+
if (!siteId)
|
|
155
|
+
throw new Error('install requires --site <site-id>');
|
|
156
|
+
const configPath = option(args, '--config');
|
|
157
|
+
const config = configPath ? await readJson(configPath) : {};
|
|
158
|
+
const permissions = Array.isArray(manifest.permissions) ? manifest.permissions : [];
|
|
159
|
+
if (manifest.distribution === 'private') {
|
|
160
|
+
const current = await api(`/api/developer/extensions/${encodeURIComponent(extensionId)}`);
|
|
161
|
+
const registered = current.data.extension;
|
|
162
|
+
const allowed = Array.isArray(registered?.allowed_site_ids) ? registered.allowed_site_ids.map(String) : [];
|
|
163
|
+
if (!allowed.includes(siteId)) {
|
|
164
|
+
await api(`/api/developer/extensions/${encodeURIComponent(extensionId)}`, {
|
|
165
|
+
method: 'PATCH', body: JSON.stringify({ allowed_site_ids: [...allowed, siteId] }),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const installed = await api(`/api/v1/sites/${encodeURIComponent(siteId)}/extensions`, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
extension_id: extensionId,
|
|
173
|
+
version: manifest.version,
|
|
174
|
+
granted_scopes: permissions.map((permission) => permission.scope).filter((scope) => typeof scope === 'string'),
|
|
175
|
+
config,
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
console.log(`Installed ${extensionId}@${String(manifest.version)} as ${String(installed.data.installation?.id ?? '')}`);
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
help();
|
|
182
|
+
return 1;
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
console.error(`typeroll extension: ${error instanceof Error ? error.message : String(error)}`);
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { runInstallSkillsCli } from './install-skills.js';
|
|
|
19
19
|
import { resolveSiteId } from './resolve-site-id.js';
|
|
20
20
|
import { buildServer } from './server.js';
|
|
21
21
|
import { VERSION } from './version.js';
|
|
22
|
+
import { runExtensionCli } from './extension-cli.js';
|
|
22
23
|
function bail(message) {
|
|
23
24
|
console.error(`typeroll-mcp: ${message}`);
|
|
24
25
|
process.exit(1);
|
|
@@ -33,11 +34,16 @@ async function main() {
|
|
|
33
34
|
const code = await runInstallSkillsCli(argv.slice(1));
|
|
34
35
|
process.exit(code);
|
|
35
36
|
}
|
|
37
|
+
if (argv[0] === 'extension') {
|
|
38
|
+
const code = await runExtensionCli(argv.slice(1));
|
|
39
|
+
process.exit(code);
|
|
40
|
+
}
|
|
36
41
|
if (argv[0] === '--help' || argv[0] === '-h' || argv[0] === 'help') {
|
|
37
42
|
console.error('Usage:');
|
|
38
43
|
console.error(' typeroll-mcp Start the MCP server (reads TYPEROLL_API_URL and TYPEROLL_API_KEY)');
|
|
39
44
|
console.error(' typeroll-mcp init [dir] [-f] Bootstrap a project: skills + .mcp.json + AGENTS.md + imagegen lab');
|
|
40
45
|
console.error(' typeroll-mcp install-skills <dir> [-f] Copy bundled skill files to <dir>');
|
|
46
|
+
console.error(' typeroll extension <command> Validate, push, install or promote an Extension');
|
|
41
47
|
console.error(' typeroll-mcp --help Show this help');
|
|
42
48
|
process.exit(0);
|
|
43
49
|
}
|
package/dist/server.js
CHANGED
|
@@ -28,6 +28,7 @@ import { settingsTools } from './tools/settings.js';
|
|
|
28
28
|
import { siteTools } from './tools/sites.js';
|
|
29
29
|
import { domainTools } from './tools/domain.js';
|
|
30
30
|
import { funnelAttributionTools } from './tools/funnel-attribution.js';
|
|
31
|
+
import { appTools } from './tools/apps.js';
|
|
31
32
|
import { skillTools } from './tools/skills.js';
|
|
32
33
|
import { fail } from './tools/helpers.js';
|
|
33
34
|
import { VERSION } from './version.js';
|
|
@@ -42,7 +43,11 @@ const PERM_RANK = { read: 0, write: 1, admin: 2 };
|
|
|
42
43
|
* mutates.
|
|
43
44
|
*/
|
|
44
45
|
function effectFor(name) {
|
|
45
|
-
if (name === '
|
|
46
|
+
if (name === 'list_apps'
|
|
47
|
+
|| name === 'read_app'
|
|
48
|
+
|| name === 'update_app'
|
|
49
|
+
|| name === 'read_funnel_attribution'
|
|
50
|
+
|| name === 'update_funnel_attribution')
|
|
46
51
|
return 'admin';
|
|
47
52
|
if (name.startsWith('list_') ||
|
|
48
53
|
name.startsWith('read_') ||
|
|
@@ -119,6 +124,7 @@ export function buildServer(options) {
|
|
|
119
124
|
...migrationTools,
|
|
120
125
|
...formTools,
|
|
121
126
|
...settingsTools,
|
|
127
|
+
...appTools,
|
|
122
128
|
...funnelAttributionTools,
|
|
123
129
|
...searchTools,
|
|
124
130
|
...bulkTools,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ok, withErrorBoundary } from './helpers.js';
|
|
3
|
+
export const appTools = [
|
|
4
|
+
{
|
|
5
|
+
name: 'list_apps',
|
|
6
|
+
description: 'List every Typeroll app available to this site, including its field schema, build impact, and masked enabled/config state. Admin permission required.',
|
|
7
|
+
handler: withErrorBoundary(async (_args, { client, siteId }) => {
|
|
8
|
+
return ok(await client.get(siteId, 'apps'));
|
|
9
|
+
}),
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: 'read_app',
|
|
13
|
+
description: 'Read one Typeroll app by registry id, including its field schema and masked state. Use list_apps to discover ids and required config fields. Secret values are never returned. Admin permission required.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
app_id: z.string().min(1).describe('Registry id returned by list_apps, for example analytics or integrations.'),
|
|
16
|
+
},
|
|
17
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
18
|
+
return ok(await client.get(siteId, `apps/${encodeURIComponent(args.app_id)}`));
|
|
19
|
+
}),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'update_app',
|
|
23
|
+
description: 'Enable, configure, or disable any registered Typeroll app through the same admin API key used for publishing. Read the app first and send config keys from its field schema. Omitted fields preserve existing values, including encrypted secrets; secret values are masked on reads and encrypted server-side on writes. Analytics provisioning runs server-side when enabled. If affects_build is true, trigger_deploy is required to publish the change. Admin permission required.',
|
|
24
|
+
inputSchema: {
|
|
25
|
+
app_id: z.string().min(1).describe('Registry id returned by list_apps.'),
|
|
26
|
+
enabled: z.boolean(),
|
|
27
|
+
config: z.record(z.unknown()).optional().describe('Schema-driven app config. Omitted fields preserve their existing values.'),
|
|
28
|
+
},
|
|
29
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
30
|
+
return ok(await client.put(siteId, `apps/${encodeURIComponent(args.app_id)}`, {
|
|
31
|
+
enabled: args.enabled,
|
|
32
|
+
config: args.config ?? {},
|
|
33
|
+
}));
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
36
|
+
];
|
package/dist/tools/forms.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
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
|
-
//
|
|
6
|
-
// <script>, and the endpoint answers form-encoded posts with an HTML
|
|
7
|
-
// confirmation page).
|
|
3
|
+
// /api/forms/submit. Agents place forms by reference: core/form in block mode,
|
|
4
|
+
// or <x-form id="…" /> in HTML mode. Preview/build expands either reference
|
|
5
|
+
// through the same trusted renderer with token, initial state, and runtime.
|
|
8
6
|
import { z } from 'zod';
|
|
9
7
|
import { ok, withErrorBoundary } from './helpers.js';
|
|
10
8
|
const fieldSchema = z.object({
|
|
@@ -42,7 +40,7 @@ export const formTools = [
|
|
|
42
40
|
},
|
|
43
41
|
{
|
|
44
42
|
name: 'read_form',
|
|
45
|
-
description: 'Read one form by id, including fields/steps, submit_text,
|
|
43
|
+
description: 'Read one form by id, including fields/steps, submit_text, and success_message. Place it with a `core/form` block using data.form_id on a block-mode page, or `<x-form id="…" />` in HTML mode. Both references are expanded server-side with validation, signed token, initial state, and the shared runtime.',
|
|
46
44
|
inputSchema: { form_id: z.string() },
|
|
47
45
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
48
46
|
const res = await client.get(siteId, `forms/${encodeURIComponent(args.form_id)}`);
|
|
@@ -51,7 +49,7 @@ export const formTools = [
|
|
|
51
49
|
},
|
|
52
50
|
{
|
|
53
51
|
name: 'create_form',
|
|
54
|
-
description: 'Create a form. Steps are the ONLY stored model — a Block[] tree of form/* field blocks per step. Simple forms: pass `fields`, each { name, type, label, required?, placeholder?, options? } (allowed types: text, email, tel, url, number, textarea, select, checkbox, radio, hidden, gdpr_consent) — the server converts them to a single static step; read_form returns the resulting steps. Multi-step funnels: pass `steps` directly (steps swap client-side, partial submissions persist per step). PLACING THE FORM
|
|
52
|
+
description: 'Create a form. Steps are the ONLY stored model — a Block[] tree of form/* field blocks per step. Simple forms: pass `fields`, each { name, type, label, required?, placeholder?, options? } (allowed types: text, email, tel, url, number, textarea, select, checkbox, radio, hidden, gdpr_consent) — the server converts them to a single static step; read_form returns the resulting steps. Multi-step funnels: pass `steps` directly (steps swap client-side, partial submissions persist per step). PLACING THE FORM: add a `core/form` block with data.form_id on block-mode pages, or `<x-form id="…" />` to HTML-mode page content. Both expand server-side to the same complete signed shell; never hand-write the form or add inline submit scripts.',
|
|
55
53
|
inputSchema: {
|
|
56
54
|
id: z.string().regex(/^[a-z][a-z0-9_-]{0,62}$/),
|
|
57
55
|
name: z.string().min(1),
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typeroll/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.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": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/
|
|
8
|
+
"url": "git+https://github.com/Typeroll/typeroll-cloud.git",
|
|
9
9
|
"directory": "packages/mcp-server"
|
|
10
10
|
},
|
|
11
|
-
"homepage": "https://github.com/
|
|
12
|
-
"bugs": "https://github.com/
|
|
11
|
+
"homepage": "https://github.com/Typeroll/typeroll-cloud/tree/main/packages/mcp-server#readme",
|
|
12
|
+
"bugs": "https://github.com/Typeroll/typeroll-cloud/issues",
|
|
13
13
|
"type": "module",
|
|
14
14
|
"bin": {
|
|
15
|
-
"typeroll-mcp": "dist/index.js"
|
|
15
|
+
"typeroll-mcp": "dist/index.js",
|
|
16
|
+
"typeroll": "dist/index.js"
|
|
16
17
|
},
|
|
17
18
|
"main": "./dist/index.js",
|
|
18
19
|
"exports": {
|