@formio/mcp 0.1.0 → 0.3.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/dist/auth.js CHANGED
@@ -37,6 +37,12 @@ function buildLoginPage(loginFormUrl) {
37
37
  <script>
38
38
  var statusEl = document.getElementById('status');
39
39
  Formio.createForm(document.getElementById('formio'), '${loginFormUrl}').then(function(form) {
40
+ // Suppress only Formio's default success alert (to avoid a UI flash); keep login error alerts intact.
41
+ const baseSetAlert = form.setAlert.bind(form);
42
+ form.setAlert = function(type, message, options) {
43
+ if (type === 'success') return;
44
+ return baseSetAlert(type, message, options);
45
+ };
40
46
  form.on('submit', function(submission) {
41
47
  var token = Formio.getToken();
42
48
  statusEl.innerHTML = token ? 'Token captured, completing login...' : 'Error: No token received from Form.io SDK';
@@ -46,7 +52,10 @@ Formio.createForm(document.getElementById('formio'), '${loginFormUrl}').then(fun
46
52
  headers: { 'Content-Type': 'application/json' },
47
53
  body: JSON.stringify({ token: token })
48
54
  }).then(function() {
49
- statusEl.innerHTML = 'Login successful. You can close this tab.';
55
+ document.querySelector('.auth-card').innerHTML =
56
+ '<div class="alert alert-success text-center mb-0" role="alert">' +
57
+ '<i class="bi bi-check-circle-fill me-2"></i>Login successful. You may close this window.' +
58
+ '</div>';
50
59
  }).catch(function(err) {
51
60
  statusEl.innerHTML = 'Error sending token: ' + err.message;
52
61
  });
@@ -0,0 +1,10 @@
1
+ export type RevisionsConsentChoice = 'enable-original' | 'enable-current' | 'proceed-without-history' | 'cancel';
2
+ export type RevisionsLicenseConsentChoice = 'continue' | 'cancel';
3
+ export interface BrowserConsentOptions {
4
+ onReady?: (port: number) => void;
5
+ openBrowser?: boolean;
6
+ }
7
+ export type RequestRevisionsLicenseConsentOptions = BrowserConsentOptions;
8
+ export declare function requestRevisionsLicenseConsent(deploymentLabel: string, actionLabel: string, options?: RequestRevisionsLicenseConsentOptions): Promise<RevisionsLicenseConsentChoice>;
9
+ export type RequestRevisionsConsentOptions = BrowserConsentOptions;
10
+ export declare function requestRevisionsConsent(formName: string, formId: string, options?: RequestRevisionsConsentOptions): Promise<RevisionsConsentChoice>;
@@ -0,0 +1,220 @@
1
+ // TEMPORARY: browser-based consent prompt for MCP clients that do not yet support
2
+ // the `elicitation` capability. Remove this module (and its call site in
3
+ // form_update.ts) once elicitation is universally supported by the clients we
4
+ // care about.
5
+ import express from 'express';
6
+ import { exec } from 'child_process';
7
+ const esc = (s) => s.replace(/</g, '&lt;');
8
+ const SHARED_STYLE = `
9
+ :root {
10
+ --formio-green: #67b346; --formio-green-dark: #4f9132;
11
+ --ink: #1f2933; --ink-soft: #52606d; --ink-mute: #7b8794;
12
+ --line: #e4e7eb; --line-soft: #f1f4f7;
13
+ --surface: #fff; --bg: #f7f9fb;
14
+ --danger: #b3261e; --danger-soft: #fdecea;
15
+ --warning-bg: #fff8e6; --warning-border: #f4d27a;
16
+ --shadow: 0 1px 2px rgba(15,23,42,.04), 0 8px 24px rgba(15,23,42,.06);
17
+ }
18
+ * { box-sizing: border-box; }
19
+ body { font-family: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif; background: var(--bg); margin: 0; padding: 3rem 1rem; color: var(--ink); -webkit-font-smoothing: antialiased; }
20
+ .shell { max-width: 600px; margin: 0 auto; }
21
+ .brand { display: flex; align-items: center; justify-content: center; margin-bottom: 1.5rem; }
22
+ .brand img { height: 32px; width: auto; }
23
+ .card { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; box-shadow: var(--shadow); overflow: hidden; }
24
+ .card-header { padding: 1.5rem 1.75rem 1rem; border-bottom: 1px solid var(--line-soft); }
25
+ .header-title { margin: 0 0 .4rem; font-size: 1.25rem; font-weight: 600; }
26
+ .header-sub { margin: 0; font-size: .92rem; color: var(--ink-soft); }
27
+ .meta { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center; margin: 1rem 1.75rem; padding: .75rem 1rem; background: var(--warning-bg); border: 1px solid var(--warning-border); border-radius: 8px; font-size: .88rem; color: var(--ink-soft); }
28
+ .meta-label { font-weight: 600; color: var(--ink); }
29
+ .meta-id { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .82rem; color: var(--ink-mute); word-break: break-all; }
30
+ .section-title { margin: 1.5rem 1.75rem .5rem; font-size: .72rem; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-mute); }
31
+ .choices { padding: 0 1.75rem 1.5rem; display: flex; flex-direction: column; gap: .75rem; }
32
+ .choice { display: block; width: 100%; background: var(--surface); color: var(--ink); border: 1px solid var(--line); border-radius: 10px; padding: .9rem 1.1rem; text-align: left; cursor: pointer; font: inherit; transition: border-color 120ms ease, background 120ms ease; }
33
+ .choice:hover { border-color: var(--ink-mute); }
34
+ .choice-title { font-weight: 600; font-size: .95rem; display: flex; align-items: center; gap: .5rem; }
35
+ .choice-desc { margin-top: .25rem; font-size: .85rem; color: var(--ink-soft); line-height: 1.4; }
36
+ .choice.is-recommended { border-color: var(--formio-green); background: rgba(103,179,70,.06); }
37
+ .choice.is-recommended:hover { background: rgba(103,179,70,.12); border-color: var(--formio-green-dark); }
38
+ .choice.is-recommended .choice-title { color: var(--formio-green-dark); }
39
+ .choice.is-danger { color: var(--danger); border-color: var(--danger); }
40
+ .choice.is-danger:hover { background: var(--danger-soft); }
41
+ .pill { font-size: .65rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; padding: .15rem .5rem; border-radius: 999px; background: var(--formio-green); color: #fff; }
42
+ .status { padding: 0 1.75rem 1.5rem; text-align: center; color: var(--ink-mute); font-size: .85rem; min-height: 1.25rem; }
43
+ .footnote { text-align: center; color: var(--ink-mute); font-size: .78rem; margin-top: 1.25rem; }
44
+ `;
45
+ const SHARED_SCRIPT = `
46
+ var statusEl = document.getElementById('status');
47
+ document.querySelectorAll('button[data-choice]').forEach(function (btn) {
48
+ btn.addEventListener('click', function () {
49
+ var choice = btn.getAttribute('data-choice');
50
+ statusEl.textContent = 'Sending choice…';
51
+ document.querySelectorAll('button[data-choice]').forEach(function (b) { b.disabled = true; b.style.opacity = '0.6'; b.style.cursor = 'not-allowed'; });
52
+ fetch('/callback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ choice: choice }) })
53
+ .then(function () { statusEl.textContent = 'Choice captured. You can close this tab.'; })
54
+ .catch(function (err) { statusEl.textContent = 'Error: ' + err.message; });
55
+ });
56
+ });
57
+ `;
58
+ function renderChoice(c) {
59
+ const cls = c.variant ? `choice is-${c.variant}` : 'choice';
60
+ const pill = c.pill ? ` <span class="pill">${esc(c.pill)}</span>` : '';
61
+ return `<button class="${cls}" data-choice="${esc(c.value)}">
62
+ <div class="choice-title">${esc(c.title)}${pill}</div>
63
+ <div class="choice-desc">${esc(c.desc)}</div>
64
+ </button>`;
65
+ }
66
+ function renderPage(opts) {
67
+ const metaIdHtml = opts.meta.id ? `<span class="meta-id">${esc(opts.meta.id)}</span>` : '';
68
+ const sectionsHtml = opts.sections
69
+ .map((s) => {
70
+ const title = s.title ? `<div class="section-title">${esc(s.title)}</div>` : '';
71
+ return `${title}<div class="choices">${s.choices.map(renderChoice).join('')}</div>`;
72
+ })
73
+ .join('');
74
+ return `<!DOCTYPE html>
75
+ <html><head>
76
+ <meta charset="utf-8">
77
+ <meta name="viewport" content="width=device-width, initial-scale=1">
78
+ <title>${esc(opts.title)}</title>
79
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:400,500,600,700&display=swap">
80
+ <style>${SHARED_STYLE}</style>
81
+ </head><body>
82
+ <div class="shell">
83
+ <div class="brand"><img src="https://portal.form.io/template/images/formio-logo-with-slogan.png" alt="Form.io"></div>
84
+ <div class="card">
85
+ <div class="card-header">
86
+ <h1 class="header-title">${esc(opts.headerTitle)}</h1>
87
+ <p class="header-sub">${esc(opts.headerSub)}</p>
88
+ </div>
89
+ <div class="meta">
90
+ <span class="meta-label">${esc(opts.meta.label)}:</span>
91
+ <strong>${esc(opts.meta.value)}</strong>
92
+ ${metaIdHtml}
93
+ </div>
94
+ ${sectionsHtml}
95
+ <div class="status" id="status">&nbsp;</div>
96
+ </div>
97
+ <div class="footnote">Form.io MCP local consent prompt · You can close this tab after choosing.</div>
98
+ </div>
99
+ <script>${SHARED_SCRIPT}</script>
100
+ </body></html>`;
101
+ }
102
+ // Shared runner: spins up a local express server on an ephemeral port, renders
103
+ // the consent page, opens the browser, and waits for the browser to POST the
104
+ // user's choice back to `/callback`.
105
+ async function runBrowserConsent(page, normalize, options = {}) {
106
+ const app = express();
107
+ app.use(express.json());
108
+ let resolveChoice;
109
+ const choicePromise = new Promise((resolve) => {
110
+ resolveChoice = resolve;
111
+ });
112
+ app.get('/', (_req, res) => res.send(page()));
113
+ app.post('/callback', (req, res) => {
114
+ const raw = req.body.choice;
115
+ res.send('Choice captured. You can close this tab.');
116
+ resolveChoice(normalize(raw));
117
+ });
118
+ const server = app.listen(0, '127.0.0.1', () => {
119
+ const addr = server.address();
120
+ if (addr && typeof addr !== 'string') {
121
+ const consentUrl = `http://127.0.0.1:${addr.port}/`;
122
+ if (options.openBrowser !== false) {
123
+ const openCmd = process.platform === 'darwin'
124
+ ? 'open'
125
+ : process.platform === 'win32'
126
+ ? 'start'
127
+ : 'xdg-open';
128
+ exec(`${openCmd} "${consentUrl}"`);
129
+ }
130
+ options.onReady?.(addr.port);
131
+ }
132
+ });
133
+ try {
134
+ return await choicePromise;
135
+ }
136
+ finally {
137
+ server.close();
138
+ }
139
+ }
140
+ export async function requestRevisionsLicenseConsent(deploymentLabel, actionLabel, options = {}) {
141
+ const page = () => renderPage({
142
+ title: 'Form.io — Security Module Required',
143
+ headerTitle: 'Form revisions are not available on this deployment',
144
+ headerSub: `The Security Module is required for revision tracking. You can still ${actionLabel}, but history will not be preserved.`,
145
+ meta: { label: 'Deployment', value: deploymentLabel },
146
+ sections: [
147
+ {
148
+ choices: [
149
+ {
150
+ value: 'continue',
151
+ title: 'Continue without revision tracking',
152
+ desc: 'Proceed with the action. Remembered for this deployment across future sessions.',
153
+ variant: 'recommended',
154
+ },
155
+ {
156
+ value: 'cancel',
157
+ title: 'Cancel',
158
+ desc: 'Abort the action. No changes are made.',
159
+ variant: 'danger',
160
+ },
161
+ ],
162
+ },
163
+ ],
164
+ });
165
+ return runBrowserConsent(page, (raw) => (raw === 'continue' ? 'continue' : 'cancel'), options);
166
+ }
167
+ const REVISIONS_CHOICES = [
168
+ 'enable-original',
169
+ 'enable-current',
170
+ 'proceed-without-history',
171
+ 'cancel',
172
+ ];
173
+ export async function requestRevisionsConsent(formName, formId, options = {}) {
174
+ const page = () => renderPage({
175
+ title: 'Form.io — Revisions Disabled',
176
+ headerTitle: 'Revisions are disabled for this form',
177
+ headerSub: 'Choose how Form.io should track this update. It is recommended to enable revisions. They track every update so you can audit changes, roll back, or pin submissions to a prior form version.',
178
+ meta: { label: 'Form', value: formName, id: formId },
179
+ sections: [
180
+ {
181
+ title: 'Recommended',
182
+ choices: [
183
+ {
184
+ value: 'enable-original',
185
+ title: 'Enable revisions',
186
+ pill: 'Original',
187
+ desc: 'Track revision history; submissions render against the form version active when they were submitted.',
188
+ variant: 'recommended',
189
+ },
190
+ {
191
+ value: 'enable-current',
192
+ title: 'Enable revisions',
193
+ pill: 'Current',
194
+ desc: 'Track revision history; submissions always render against the latest form version.',
195
+ variant: 'recommended',
196
+ },
197
+ ],
198
+ },
199
+ {
200
+ title: 'Other',
201
+ choices: [
202
+ {
203
+ value: 'proceed-without-history',
204
+ title: 'Update without history',
205
+ desc: 'Proceed with the update — no audit trail. The form will continue to operate without revision history.',
206
+ },
207
+ {
208
+ value: 'cancel',
209
+ title: 'Cancel',
210
+ desc: 'Make no changes. The pending update is discarded.',
211
+ variant: 'danger',
212
+ },
213
+ ],
214
+ },
215
+ ],
216
+ });
217
+ return runBrowserConsent(page, (raw) => REVISIONS_CHOICES.includes(raw)
218
+ ? raw
219
+ : 'cancel', options);
220
+ }
@@ -0,0 +1,18 @@
1
+ import { ResolvedFormioConfig } from '../config.js';
2
+ export declare const DRAFT_FIELDS: readonly ["components", "settings", "tags", "properties", "controller", "esign", "display"];
3
+ export declare const REVERT_FIELDS: readonly ["components", "tags", "properties", "display"];
4
+ export interface DraftFlowOptions {
5
+ formId: string;
6
+ form: Record<string, unknown>;
7
+ _vnote: string;
8
+ cfg: ResolvedFormioConfig;
9
+ }
10
+ export interface RevertOptions {
11
+ formId: string;
12
+ version: string;
13
+ _vnote: string;
14
+ cfg: ResolvedFormioConfig;
15
+ }
16
+ export declare function saveDraft({ formId, form, _vnote, cfg }: DraftFlowOptions): Promise<unknown>;
17
+ export declare function publishDraft({ formId, _vnote, cfg }: Omit<DraftFlowOptions, 'form'>): Promise<unknown>;
18
+ export declare function revertToRevision({ formId, version, _vnote, cfg }: RevertOptions): Promise<unknown>;
@@ -0,0 +1,68 @@
1
+ import { formioFetch } from '../formio-client.js';
2
+ import { prefixVnote } from './helpers.js';
3
+ export const DRAFT_FIELDS = [
4
+ 'components',
5
+ 'settings',
6
+ 'tags',
7
+ 'properties',
8
+ 'controller',
9
+ 'esign',
10
+ 'display',
11
+ ];
12
+ export const REVERT_FIELDS = ['components', 'tags', 'properties', 'display'];
13
+ function pickFields(source, fields) {
14
+ return Object.fromEntries(Object.entries(source).filter(([k]) => fields.includes(k)));
15
+ }
16
+ // Draft body must be a subset of DRAFT_FIELDS — anything outside that set is
17
+ // Reject up front so the LLM picks the right tool
18
+ // path (standard form_update for identity/policy edits) instead of staging
19
+ // changes that will vanish.
20
+ function rejectNonDraftFields(form) {
21
+ const allowed = new Set(DRAFT_FIELDS);
22
+ const offending = Object.keys(form).filter((k) => !allowed.has(k));
23
+ if (offending.length === 0)
24
+ return;
25
+ throw new Error(`Draft body contains fields that cannot be staged: ${offending.join(', ')}. ` +
26
+ `Drafts only stage these fields: ${DRAFT_FIELDS.join(', ')}. ` +
27
+ `For identity (title, name, path), policy (access, submissionAccess, revisions), ` +
28
+ `or other fields, call form_update WITHOUT draft: true to apply them immediately. ` +
29
+ `Do NOT retry this call with draft: true.`);
30
+ }
31
+ export async function saveDraft({ formId, form, _vnote, cfg }) {
32
+ rejectNonDraftFields(form);
33
+ // if no draft exists, the endpoint returns the live form
34
+ const base = (await formioFetch(`form/${formId}/draft`, {}, cfg));
35
+ await formioFetch(`form/${formId}/draft`, {}, cfg, {
36
+ method: 'PUT',
37
+ body: { ...base, ...form, _vnote: prefixVnote(_vnote) },
38
+ });
39
+ // fresh GET since PUT returns stale body
40
+ return await formioFetch(`form/${formId}/draft`, {}, cfg);
41
+ }
42
+ export async function publishDraft({ formId, _vnote, cfg }) {
43
+ // GET /draft falls back to the live form when no draft exists, so distinguish
44
+ // by _vid: only the draft revision has _vid === 'draft'.
45
+ const draft = (await formioFetch(`form/${formId}/draft`, {}, cfg));
46
+ if (draft._vid !== 'draft') {
47
+ throw new Error(`No draft exists for form "${formId}". Create one via form_update with draft: true.`);
48
+ }
49
+ const live = (await formioFetch(`form/${formId}`, {}, cfg));
50
+ await formioFetch(`form/${formId}`, {}, cfg, {
51
+ method: 'PUT',
52
+ body: { ...live, ...pickFields(draft, DRAFT_FIELDS), _vnote: prefixVnote(_vnote) },
53
+ });
54
+ return await formioFetch(`form/${formId}`, {}, cfg);
55
+ }
56
+ export async function revertToRevision({ formId, version, _vnote, cfg }) {
57
+ const revision = (await formioFetch(`form/${formId}/v/${version}`, {}, cfg));
58
+ const live = (await formioFetch(`form/${formId}`, {}, cfg));
59
+ await formioFetch(`form/${formId}`, {}, cfg, {
60
+ method: 'PUT',
61
+ body: {
62
+ ...live,
63
+ ...pickFields(revision, REVERT_FIELDS),
64
+ _vnote: prefixVnote(_vnote),
65
+ },
66
+ });
67
+ return await formioFetch(`form/${formId}`, {}, cfg);
68
+ }
@@ -0,0 +1,5 @@
1
+ export declare const VNOTE_PREFIX = "@formio/mcp:";
2
+ export declare function prefixVnote(note: string): string;
3
+ export declare const stripRevisions: ({ revisions: _revisions, ...rest }: Record<string, unknown>) => {
4
+ [x: string]: unknown;
5
+ };
@@ -0,0 +1,7 @@
1
+ export const VNOTE_PREFIX = '@formio/mcp:';
2
+ export function prefixVnote(note) {
3
+ return `${VNOTE_PREFIX} ${note}`;
4
+ }
5
+ export const stripRevisions = ({
6
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
7
+ revisions: _revisions, ...rest }) => rest;
@@ -0,0 +1,4 @@
1
+ export { saveDraft, publishDraft, revertToRevision } from './flows.js';
2
+ export { gateRevisionsLicense } from './license.js';
3
+ export { gateRevisionsTracking } from './tracking.js';
4
+ export { prefixVnote } from './helpers.js';
@@ -0,0 +1,4 @@
1
+ export { saveDraft, publishDraft, revertToRevision } from './flows.js';
2
+ export { gateRevisionsLicense } from './license.js';
3
+ export { gateRevisionsTracking } from './tracking.js';
4
+ export { prefixVnote } from './helpers.js';
@@ -0,0 +1,14 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { ResolvedFormioConfig } from '../config.js';
3
+ import { RevisionsLicenseConsentChoice } from './browser-prompts.js';
4
+ export declare function checkRevisionsLicensed(cfg: ResolvedFormioConfig): Promise<boolean>;
5
+ export declare function getRevisionsLicenseConsent(server: McpServer, cfg: ResolvedFormioConfig, actionLabel: string): Promise<RevisionsLicenseConsentChoice>;
6
+ export declare function confirmProceedWithoutRevisions(server: McpServer, cfg: ResolvedFormioConfig, actionLabel: string): Promise<void>;
7
+ export declare function gateRevisionsLicense(server: McpServer, cfg: ResolvedFormioConfig, { actionLabel, requiresRevisions, form, }: {
8
+ actionLabel: string;
9
+ requiresRevisions: boolean;
10
+ form: Record<string, unknown>;
11
+ }): Promise<{
12
+ licensed: boolean;
13
+ form: Record<string, unknown>;
14
+ }>;
@@ -0,0 +1,137 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { requestRevisionsLicenseConsent, } from './browser-prompts.js';
5
+ import { stripRevisions } from './helpers.js';
6
+ // ─── License detection ──────────────────────────────────────────────────────
7
+ // Resolves the deployment's Security Module flag (`sac`) from the anonymous
8
+ // `/config.js`. Cached per `baseUrl` — license is deployment-wide.
9
+ const revisionsLicensedByBaseUrl = new Map();
10
+ const SAC_PATTERN = /\bsac\s*=\s*(true|false)\b/i;
11
+ export async function checkRevisionsLicensed(cfg) {
12
+ const cached = revisionsLicensedByBaseUrl.get(cfg.baseUrl);
13
+ if (cached !== undefined)
14
+ return cached;
15
+ let revisionsLicensed = false;
16
+ try {
17
+ const url = new URL('config.js', `${cfg.baseUrl.replace(/\/*$/, '/')}`);
18
+ const response = await fetch(url);
19
+ if (response.ok) {
20
+ const body = await response.text();
21
+ const match = body.match(SAC_PATTERN);
22
+ revisionsLicensed = match?.[1]?.toLowerCase() === 'true';
23
+ }
24
+ }
25
+ catch {
26
+ revisionsLicensed = false;
27
+ }
28
+ revisionsLicensedByBaseUrl.set(cfg.baseUrl, revisionsLicensed);
29
+ return revisionsLicensed;
30
+ }
31
+ // ─── Persistent consent store ───────────────────────────────────────────────
32
+ // `~/.formio/revisions-license-consent.json`, keyed by `baseUrl`. Only
33
+ // positive consent ("continue") is persisted; cancel is transient.
34
+ const DEFAULT_CACHE_DIR = path.join(os.homedir(), '.formio');
35
+ const CACHE_FILE = 'revisions-license-consent.json';
36
+ async function readCache(cacheDir) {
37
+ const filePath = path.join(cacheDir, CACHE_FILE);
38
+ try {
39
+ const contents = await fs.readFile(filePath, 'utf-8');
40
+ const parsed = JSON.parse(contents);
41
+ const result = {};
42
+ for (const [k, v] of Object.entries(parsed)) {
43
+ if (v === true)
44
+ result[k] = true;
45
+ }
46
+ return result;
47
+ }
48
+ catch {
49
+ return {};
50
+ }
51
+ }
52
+ async function writeCache(cacheDir, data) {
53
+ await fs.mkdir(cacheDir, { recursive: true });
54
+ const filePath = path.join(cacheDir, CACHE_FILE);
55
+ await fs.writeFile(filePath, JSON.stringify(data), { mode: 0o600 });
56
+ }
57
+ async function readRevisionsLicenseConsent(baseUrl, cacheDir = DEFAULT_CACHE_DIR) {
58
+ const data = await readCache(cacheDir);
59
+ return data[baseUrl] === true;
60
+ }
61
+ async function saveRevisionsLicenseConsent(baseUrl, cacheDir = DEFAULT_CACHE_DIR) {
62
+ const data = await readCache(cacheDir);
63
+ data[baseUrl] = true;
64
+ await writeCache(cacheDir, data);
65
+ }
66
+ // ─── In-memory consent layered over the persistent store ───────────────────
67
+ // Lookup order: memory → disk → prompt.
68
+ const revisionsLicenseConsentByBaseUrl = new Map();
69
+ export async function getRevisionsLicenseConsent(server, cfg, actionLabel) {
70
+ const cached = revisionsLicenseConsentByBaseUrl.get(cfg.baseUrl);
71
+ if (cached)
72
+ return cached;
73
+ if (await readRevisionsLicenseConsent(cfg.baseUrl)) {
74
+ revisionsLicenseConsentByBaseUrl.set(cfg.baseUrl, 'continue');
75
+ return 'continue';
76
+ }
77
+ const supportsElicitation = Boolean(server.server.getClientCapabilities()?.elicitation);
78
+ let choice;
79
+ if (supportsElicitation) {
80
+ const result = await server.server.elicitInput({
81
+ message: `Form revisions are not available on this Form.io deployment (the Security Module is required on the license). You can still ${actionLabel}, but history will not be saved. Continue?`,
82
+ requestedSchema: {
83
+ type: 'object',
84
+ properties: {
85
+ choice: {
86
+ type: 'string',
87
+ title: 'How to proceed',
88
+ enum: ['continue', 'cancel'],
89
+ enumNames: [
90
+ 'Continue without revision tracking (remembered for this deployment across future sessions)',
91
+ 'Cancel',
92
+ ],
93
+ },
94
+ },
95
+ required: ['choice'],
96
+ },
97
+ });
98
+ if (result.action !== 'accept' || !result.content?.choice) {
99
+ choice = 'cancel';
100
+ }
101
+ else {
102
+ choice = result.content.choice === 'continue' ? 'continue' : 'cancel';
103
+ }
104
+ }
105
+ else {
106
+ // TEMPORARY: browser-consent fallback for MCP clients that do not yet support elicitation.
107
+ choice = await requestRevisionsLicenseConsent(cfg.baseUrl, actionLabel);
108
+ }
109
+ // Only cache positive consent — cancel is transient so users can change their mind.
110
+ if (choice === 'continue') {
111
+ revisionsLicenseConsentByBaseUrl.set(cfg.baseUrl, choice);
112
+ await saveRevisionsLicenseConsent(cfg.baseUrl);
113
+ }
114
+ return choice;
115
+ }
116
+ // Prompts the user if they want to proceed when revisions are not enabled on the license
117
+ export async function confirmProceedWithoutRevisions(server, cfg, actionLabel) {
118
+ const consent = await getRevisionsLicenseConsent(server, cfg, actionLabel);
119
+ if (consent === 'cancel') {
120
+ throw new Error(`USER CANCELLED. The user explicitly chose to cancel: ${actionLabel}. Do NOT retry. Do NOT suggest workarounds, alternative projects, or enabling the Security Module. Do NOT offer to switch deployments. Simply acknowledge the cancellation to the user in one short sentence and stop. The user is aware of why they cancelled.`);
121
+ }
122
+ }
123
+ // Top-level license gate. Throws when the action requires revisions on an
124
+ // unlicensed deployment; otherwise prompts for "continue without history"
125
+ // consent when unlicensed. Returns the resolved licensed flag and the form
126
+ // body with `revisions` stripped when unlicensed (passthrough when licensed).
127
+ export async function gateRevisionsLicense(server, cfg, { actionLabel, requiresRevisions, form, }) {
128
+ const licensed = await checkRevisionsLicensed(cfg);
129
+ if (!licensed && requiresRevisions) {
130
+ throw new Error(`Cannot ${actionLabel} — the Security Module is required to use revisions, so drafts, publishes, and reverts are unavailable. Drop the draft/publish/revert flag and call form_update as a standard update to apply your changes.`);
131
+ }
132
+ // for standard creates/updates, confirm with the user that they don't care if history is not preserved
133
+ if (!licensed) {
134
+ await confirmProceedWithoutRevisions(server, cfg, actionLabel);
135
+ }
136
+ return { licensed, form: licensed ? form : stripRevisions(form) };
137
+ }
@@ -0,0 +1,10 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { ResolvedFormioConfig } from '../config.js';
3
+ export interface RevisionsTrackingGateOptions {
4
+ formId: string;
5
+ form: Record<string, unknown>;
6
+ /** Whether the deployment is licensed for revisions. When false, skip the per-form prompt. */
7
+ licensed: boolean;
8
+ cfg: ResolvedFormioConfig;
9
+ }
10
+ export declare function gateRevisionsTracking(server: McpServer, opts: RevisionsTrackingGateOptions): Promise<Record<string, unknown>>;
@@ -0,0 +1,103 @@
1
+ import { formioFetch } from '../formio-client.js';
2
+ import { requestRevisionsConsent } from './browser-prompts.js';
3
+ import { stripRevisions } from './helpers.js';
4
+ // Session-scoped set of formIds the user already approved "without history" for.
5
+ // Module-level so per-form approval persists across calls
6
+ const approvedWithoutHistory = new Set();
7
+ // Per-form revisions-tracking mode gate. Distinct from the deployment-level
8
+ // license gate in license.ts: this prompt asks "for THIS specific form, how
9
+ // should revisions be tracked" (original / current / off), not "is the
10
+ // deployment licensed at all."
11
+ //
12
+ // The prompt fires ONLY when ALL of the following hold:
13
+ // 1. Deployment IS licensed for revisions (`licensed` is true).
14
+ // 2. The targeted form has revisions disabled (`stored.revisions` is falsy).
15
+ // 3. The caller did not opt in by passing `revisions: 'original' | 'current'`
16
+ // on the body.
17
+ // 4. The user has not already approved "proceed without history" for this
18
+ // form in the current session.
19
+ //
20
+ // If any of those fail, the gate is a no-op and returns the body unchanged.
21
+ async function promptRevisionsMode(server, stored, formId) {
22
+ const supportsElicitation = Boolean(server.server.getClientCapabilities()?.elicitation);
23
+ if (supportsElicitation) {
24
+ const result = await server.server.elicitInput({
25
+ message: `Form "${stored.name ?? formId}" has revisions disabled. It is recommended to enable revisions. They track every update so you can audit changes, roll back, or pin submissions to a prior form version. How would you like to proceed?`,
26
+ requestedSchema: {
27
+ type: 'object',
28
+ properties: {
29
+ choice: {
30
+ type: 'string',
31
+ title: 'Revision mode for this update',
32
+ enum: ['enable-original', 'enable-current', 'proceed-without-history'],
33
+ enumNames: [
34
+ 'Enable revisions (original) and update',
35
+ 'Enable revisions (current) and update',
36
+ 'Proceed without history (not tracked)',
37
+ ],
38
+ description: 'original = submissions render against the form version active when they were submitted; current = submissions always render against the latest form version; proceed-without-history = no audit trail.',
39
+ },
40
+ },
41
+ required: ['choice'],
42
+ },
43
+ });
44
+ if (result.action !== 'accept' || !result.content?.choice)
45
+ return 'cancel';
46
+ const c = result.content.choice;
47
+ if (c === 'enable-original' || c === 'enable-current' || c === 'proceed-without-history') {
48
+ return c;
49
+ }
50
+ return 'cancel';
51
+ }
52
+ // TEMPORARY: browser-consent fallback for MCP clients that do not yet support elicitation.
53
+ // Remove once elicitation is universally supported by the clients we care about.
54
+ const formName = typeof stored.name === 'string' ? stored.name : formId;
55
+ return requestRevisionsConsent(formName, formId);
56
+ }
57
+ // Returns the stored form when a per-form revisions prompt is required;
58
+ // returns null when the gate should be bypassed.
59
+ //
60
+ // Bypass rule: only treat the caller as having supplied `revisions` when they
61
+ // opted IN to tracking (`original` / `current`). Passing `revisions: ''`
62
+ // mirrors the disabled stored state and must NOT bypass the prompt — that
63
+ // loophole lets an LLM silently skip the audit-trail decision on every form
64
+ // by always echoing the disabled value.
65
+ async function shouldPromptForRevisions(opts) {
66
+ const { formId, form, licensed, cfg } = opts;
67
+ const callerOptedIn = form.revisions === 'original' || form.revisions === 'current';
68
+ // Skip when the deployment doesn't have revisions enabled on the license — the user's "continue without
69
+ // revision tracking" choice is captured at the license-gate layer, so
70
+ // re-prompting per-form is redundant.
71
+ if (callerOptedIn || !licensed)
72
+ return null;
73
+ const stored = (await formioFetch(`form/${formId}`, {}, cfg));
74
+ if (stored.revisions || approvedWithoutHistory.has(formId))
75
+ return null;
76
+ return stored;
77
+ }
78
+ // Returns the PUT body for the standard form_update path with the user's
79
+ // per-form revisions-mode choice applied. Throws on cancel so the calling
80
+ // tool's outer try/catch surfaces it via toMcpError.
81
+ export async function gateRevisionsTracking(server, opts) {
82
+ const { formId, form } = opts;
83
+ let putBody = { ...form };
84
+ const stored = await shouldPromptForRevisions(opts);
85
+ if (!stored)
86
+ return putBody;
87
+ const choice = await promptRevisionsMode(server, stored, formId);
88
+ if (choice === 'cancel') {
89
+ throw new Error(`User declined to update form "${formId}". No changes were made.`);
90
+ }
91
+ if (choice === 'enable-original' || choice === 'enable-current') {
92
+ putBody = {
93
+ ...putBody,
94
+ revisions: choice === 'enable-original' ? 'original' : 'current',
95
+ };
96
+ return putBody;
97
+ }
98
+ // proceed-without-history: drop any caller-supplied `revisions: ''` so the
99
+ // PUT body matches "no revisions change". Remember for the rest of the
100
+ // session so the user is asked only once per form.
101
+ approvedWithoutHistory.add(formId);
102
+ return stripRevisions(putBody);
103
+ }
@@ -2,8 +2,9 @@ import { z } from 'zod';
2
2
  import { formioFetch } from '../formio-client.js';
3
3
  import { toMcpTextResult, toMcpError } from '../mcp-responses.js';
4
4
  import { cwdSchema, resolveProjectConfig } from '../project-resolver.js';
5
+ import { gateRevisionsLicense, prefixVnote } from '../revisions/index.js';
5
6
  export function registerFormCreateTool(server, config) {
6
- server.tool('form_create', "Create a new form in the Form.io project mapped to the user's current working directory. IMPORTANT: Before calling this tool, use the formio-form skill to construct a properly structured Form.io form JSON definition based on the user's requirements. The skill documents all component types, validation options, layout patterns, and conditional logic available in Form.io.", {
7
+ server.tool('form_create', 'Create a new form in the Form.io project mapped to the user\'s current working directory. IMPORTANT: Before calling this tool, use the formio-schema skill to construct a properly structured Form.io form JSON definition based on the user\'s requirements. The skill documents all component types, validation options, layout patterns, and conditional logic available in Form.io. New forms default to `revisions: \'original\'` so form change history is preserved. NOT for: creating a draft revision of an existing form. When the user says "create/save a draft", "draft <change>", call form_update with `formId` and `draft: true` instead.', {
7
8
  cwd: cwdSchema,
8
9
  form: z
9
10
  .looseObject({
@@ -19,15 +20,30 @@ export function registerFormCreateTool(server, config) {
19
20
  .optional()
20
21
  .describe('Display mode (default: "form")'),
21
22
  tags: z.array(z.string()).optional().describe('Tags for categorization'),
23
+ revisions: z
24
+ .enum(['current', 'original', ''])
25
+ .optional()
26
+ .describe('Revision mode (default: "original"). Pass "" to disable'),
22
27
  })
23
28
  .catchall(z.unknown())
24
29
  .describe('Form.io form JSON definition'),
25
- }, async ({ cwd, form }) => {
30
+ note: z.string().optional().describe('Note describing the initial revision'),
31
+ }, async ({ cwd, form: rawForm, note }) => {
26
32
  try {
27
33
  const cfg = resolveProjectConfig(cwd, config);
34
+ const { licensed, form } = await gateRevisionsLicense(server, cfg, {
35
+ actionLabel: 'create this form',
36
+ requiresRevisions: false,
37
+ form: rawForm,
38
+ });
28
39
  const created = await formioFetch('form', {}, cfg, {
29
40
  method: 'POST',
30
- body: form,
41
+ body: {
42
+ // gate already stripped `revisions` on unlicensed deployments; on
43
+ // licensed ones default to 'original' unless the caller overrode.
44
+ ...(licensed ? { revisions: 'original', ...form } : form),
45
+ ...(note && { _vnote: prefixVnote(note) }),
46
+ },
31
47
  });
32
48
  return toMcpTextResult(created);
33
49
  }
@@ -3,19 +3,29 @@ import { formioFetch, isMongoId } from '../formio-client.js';
3
3
  import { toMcpTextResult, toMcpError } from '../mcp-responses.js';
4
4
  import { cwdSchema, resolveProjectConfig } from '../project-resolver.js';
5
5
  export function registerFormGetTool(server, config) {
6
- server.tool('form_get', "Fetch a single form definition from the Form.io project mapped to the user's current working directory, by form ID or path.", {
6
+ server.tool('form_get', "Fetch a single form definition from the Form.io project mapped to the user's current working directory, by form ID or path. Pass `draft: true` to fetch the form's current in-flight draft instead of the published form.", {
7
7
  cwd: cwdSchema,
8
8
  formIdOrPath: z.string().describe('Form ID (_id) or path (e.g. "user/login")'),
9
9
  select: z
10
10
  .string()
11
11
  .optional()
12
12
  .describe('Comma-separated fields to return (omit for full form JSON)'),
13
- }, async ({ cwd, formIdOrPath, select }) => {
13
+ draft: z
14
+ .boolean()
15
+ .optional()
16
+ .describe("When true, fetch the form's current draft (GET /<form>/draft)"),
17
+ }, async ({ cwd, formIdOrPath, select, draft }) => {
14
18
  try {
15
19
  const cfg = resolveProjectConfig(cwd, config);
16
20
  const params = { select };
17
- const path = isMongoId(formIdOrPath) ? `form/${formIdOrPath}` : formIdOrPath;
18
- const form = await formioFetch(path, params, cfg);
21
+ const base = isMongoId(formIdOrPath) ? `form/${formIdOrPath}` : formIdOrPath;
22
+ const path = draft ? `${base}/draft` : base;
23
+ const form = (await formioFetch(path, params, cfg));
24
+ // GET /draft falls back to the live form when no draft exists, so
25
+ // distinguish by _vid: only the draft revision has _vid === 'draft'.
26
+ if (draft && form._vid !== 'draft') {
27
+ throw new Error(`No draft exists for form "${formIdOrPath}". Create one via form_update with draft: true.`);
28
+ }
19
29
  return toMcpTextResult(form);
20
30
  }
21
31
  catch (error) {
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { FormioConfig } from '../config.js';
3
+ export declare function registerFormRevisionGetTool(server: McpServer, config: FormioConfig): void;
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ import { formioFetch, isMongoId } from '../formio-client.js';
3
+ import { toMcpTextResult, toMcpError } from '../mcp-responses.js';
4
+ import { cwdSchema, resolveProjectConfig } from '../project-resolver.js';
5
+ export function registerFormRevisionGetTool(server, config) {
6
+ server.tool('form_revision_get', 'Fetch a single immutable form revision from the Form.io project mapped to the current working directory. `version` accepts either the revision `_vid` (e.g. "3") or the revision document `_id` (24-character hex). To revert the live form to this revision, pass its `form` body to `form_update` with a `note` like `Revert to v<vid>`.', {
7
+ cwd: cwdSchema,
8
+ formIdOrPath: z.string().describe('Form ID (_id) or path alias (e.g. "user/login")'),
9
+ version: z.string().describe('Revision _vid (e.g. "3") or revision document _id'),
10
+ }, async ({ cwd, formIdOrPath, version }) => {
11
+ try {
12
+ const cfg = resolveProjectConfig(cwd, config);
13
+ const base = isMongoId(formIdOrPath) ? `form/${formIdOrPath}` : formIdOrPath;
14
+ const revision = await formioFetch(`${base}/v/${version}`, {}, cfg);
15
+ return toMcpTextResult(revision);
16
+ }
17
+ catch (error) {
18
+ return toMcpError(error);
19
+ }
20
+ });
21
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { FormioConfig } from '../config.js';
3
+ export declare function registerFormRevisionsListTool(server: McpServer, config: FormioConfig): void;
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ import { formioFetch, isMongoId } from '../formio-client.js';
3
+ import { toMcpTextResult, toMcpError } from '../mcp-responses.js';
4
+ import { cwdSchema, resolveProjectConfig } from '../project-resolver.js';
5
+ export function registerFormRevisionsListTool(server, config) {
6
+ server.tool('form_revisions_list', 'List immutable published revision summaries for a single form in the Form.io project mapped to the current working directory. Returns compact revision metadata (_vid, _id, modified, user, _vnote) for the form identified by `formIdOrPath`. To inspect a specific revision body, call `form_revision_get` with the desired `_vid`. To revert the live form to a prior revision, pass that revision body to `form_update` with a `note` like `Revert to v<vid>`.', {
7
+ cwd: cwdSchema,
8
+ formIdOrPath: z.string().describe('Form ID (_id) or path alias (e.g. "user/login")'),
9
+ }, async ({ cwd, formIdOrPath }) => {
10
+ try {
11
+ const cfg = resolveProjectConfig(cwd, config);
12
+ const base = isMongoId(formIdOrPath) ? `form/${formIdOrPath}` : formIdOrPath;
13
+ const revisions = await formioFetch(`${base}/v`, {}, cfg);
14
+ return toMcpTextResult(revisions);
15
+ }
16
+ catch (error) {
17
+ return toMcpError(error);
18
+ }
19
+ });
20
+ }
@@ -2,8 +2,13 @@ import { z } from 'zod';
2
2
  import { formioFetch, MONGO_ID_PATTERN } from '../formio-client.js';
3
3
  import { toMcpTextResult, toMcpError } from '../mcp-responses.js';
4
4
  import { cwdSchema, resolveProjectConfig } from '../project-resolver.js';
5
+ import { gateRevisionsLicense, gateRevisionsTracking, prefixVnote, publishDraft, revertToRevision, saveDraft, } from '../revisions/index.js';
5
6
  export function registerFormUpdateTool(server, config) {
6
- server.tool('form_update', "Update an existing form in the Form.io project mapped to the user's current working directory. IMPORTANT: Before calling this tool, first use form_get to fetch the current form definition, then use the formio-form skill to apply the requested modifications (add, remove, or modify fields and settings), and finally call this tool with the complete updated form JSON.", {
7
+ server.tool('form_update', [
8
+ "Update an existing form in the Form.io project mapped to the user's current working directory. IMPORTANT: Before calling this tool, first use form_get to fetch the current form definition, then use the formio-schema skill to understand the schema so that you can apply the requested modifications (add, remove, or modify fields and settings), and finally call this tool with the complete updated form JSON.",
9
+ '`draft`, `publish`, and `revert` are mutually exclusive — pass at most one.',
10
+ 'If `revisions` in the response differs from the stored value, the per-form revisions-mode gate prompted the USER and they chose.',
11
+ ].join(' '), {
7
12
  cwd: cwdSchema,
8
13
  formId: z
9
14
  .string()
@@ -20,17 +25,69 @@ export function registerFormUpdateTool(server, config) {
20
25
  type: z.enum(['form', 'resource']).optional().describe('Form type'),
21
26
  display: z.enum(['form', 'wizard', 'pdf']).optional().describe('Display mode'),
22
27
  tags: z.array(z.string()).optional().describe('Tags for categorization'),
28
+ revisions: z
29
+ .enum(['current', 'original', ''])
30
+ .optional()
31
+ .describe('Revision mode. Pass "" to disable; omit to leave the stored value unchanged.'),
23
32
  })
24
33
  .catchall(z.unknown())
25
34
  .describe('Complete updated Form.io form JSON definition'),
26
- }, async ({ cwd, formId, form }) => {
35
+ note: z
36
+ .string()
37
+ .describe('Required note describing the diff (live form vs updated body) — no action preambles ("Published draft:", "Saved draft:", "Reverted:"). For `revert: true`, default to "Reverted to version {version}" unless the user explicitly provides a different note.'),
38
+ draft: z
39
+ .boolean()
40
+ .optional()
41
+ .describe('When true, create or update a draft (PUT /form/{formId}/draft) instead of publishing. Caller `form` fields merge on top of existing draft fields, preserving prior unpublished draft edits.'),
42
+ publish: z
43
+ .boolean()
44
+ .optional()
45
+ .describe('When true, publish the current draft. Caller `form` body is ignored; only allowlisted revision fields flow from existing draft to live (PUT /form/{formId}).'),
46
+ revert: z
47
+ .boolean()
48
+ .optional()
49
+ .describe('When true, revert the live form to a prior revision. Requires `version`. Caller `form` body is ignored; only allowlisted revision fields flow from the revision to live.'),
50
+ version: z
51
+ .string()
52
+ .optional()
53
+ .describe('Revision identifier for `revert: true` — either the revision `_vid` (e.g. "3") or the revision document `_id` (24-char hex).'),
54
+ }, async ({ cwd, formId, form: rawForm, note, draft, publish, revert, version }) => {
27
55
  try {
56
+ const exclusiveFlags = [draft, publish, revert].filter(Boolean);
57
+ if (exclusiveFlags.length > 1) {
58
+ throw new Error('`draft`, `publish`, and `revert` flags are mutually exclusive — pass only one.');
59
+ }
60
+ if (revert && !version) {
61
+ throw new Error('`revert: true` requires `version` (revision `_vid` or document `_id`).');
62
+ }
28
63
  const cfg = resolveProjectConfig(cwd, config);
29
- const updated = await formioFetch(`form/${formId}`, {}, cfg, {
30
- method: 'PUT',
31
- body: form,
64
+ const actionLabel = `${revert ? 'revert' : publish ? 'publish' : draft ? 'save a draft of' : 'update'} this form`;
65
+ const { licensed, form } = await gateRevisionsLicense(server, cfg, {
66
+ actionLabel,
67
+ requiresRevisions: Boolean(draft || publish || revert),
68
+ form: rawForm,
32
69
  });
33
- return toMcpTextResult(updated);
70
+ if (revert || publish || draft) {
71
+ const args = { formId, _vnote: note, cfg };
72
+ return toMcpTextResult(await (revert && version
73
+ ? revertToRevision({ ...args, version })
74
+ : publish
75
+ ? publishDraft(args)
76
+ : saveDraft({ ...args, form })));
77
+ }
78
+ // Standard PUT path. Apply per-form revisions consent (prompts when
79
+ // the stored form has revisions disabled and the caller did not opt
80
+ // in via `revisions: 'original'|'current'`).
81
+ const putBody = await gateRevisionsTracking(server, {
82
+ formId,
83
+ form,
84
+ licensed,
85
+ cfg,
86
+ });
87
+ return toMcpTextResult(await formioFetch(`form/${formId}`, {}, cfg, {
88
+ method: 'PUT',
89
+ body: { ...putBody, _vnote: prefixVnote(note) },
90
+ }));
34
91
  }
35
92
  catch (error) {
36
93
  return toMcpError(error);
@@ -8,6 +8,8 @@ import { registerActionUpdateTool } from './action_update.js';
8
8
  import { registerFormCreateTool } from './form_create.js';
9
9
  import { registerFormGetTool } from './form_get.js';
10
10
  import { registerFormListTool } from './form_list.js';
11
+ import { registerFormRevisionGetTool } from './form_revision_get.js';
12
+ import { registerFormRevisionsListTool } from './form_revisions_list.js';
11
13
  import { registerFormUpdateTool } from './form_update.js';
12
14
  import { registerHelloTool } from './hello.js';
13
15
  import { registerProjectExportTool } from './project_export.js';
@@ -21,6 +23,8 @@ export function registerAllTools(server, config, options = {}) {
21
23
  registerFormCreateTool(server, config);
22
24
  registerFormGetTool(server, config);
23
25
  registerFormListTool(server, config);
26
+ registerFormRevisionGetTool(server, config);
27
+ registerFormRevisionsListTool(server, config);
24
28
  registerFormUpdateTool(server, config);
25
29
  registerProjectExportTool(server, config);
26
30
  registerProjectImportTool(server, config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formio/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Form.io MCP Server",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Form.io LLC
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.