Thanks for contributing to @formio/ai — the Form.io agentic coding toolset.
Requirements: Node.js >= 22.12, pnpm 10 (the repo pins packageManager: pnpm@10.33.2 — corepack enable picks it up automatically).
git clone https://github.com/formio/ai.git
cd ai
pnpm installThis is a pnpm + Turborepo monorepo shipping three things:
packages/mcp-server/— the@formio/mcpModel Context Protocol server (form_*,role_*,action_*,project_*tools).plugin/— the@formio/aiagent plugin, bundling the MCP server plus the skill library atplugin/skills/. Everything here ships to a consumer's own project, so maintainer tooling belongs outside it.packages/skill-tests/— structural tests for the skill library, plus executable tests that run theformio-sdkskill's doc examples against the real@formio/js.
Some agent-facing files in this repository are generated rather than committed:
-
OpenSpec skill mirrors —
.claude/skills/openspec-*,.claude/skills/tdd-*,.cursor/skills/, and.github/skills/. They are gitignored, so a fresh clone will not have the/opsx:*skills until you regenerate them with the OpenSpec CLI (npm install -g @fission-ai/openspec@latest, then run it in the repo root).They are excluded deliberately. The
skillsCLI discovers skills additively across a repository's agent directories, and its-sflag matches exact names rather than globs — so while those mirrors were committed, anyone runningnpx skills add formio/aiwas offered this repository's OpenSpec and TDD workflow skills alongside the Form.io library, with no way to filter them out. -
.claude/skills/formio-*— these are the exception: committed symlinks intoplugin/skills/, which is how this repository's own sessions load the library. TheskillsCLI ignores them because it does not follow symlinks while discovering skills.
The OpenSpec command mirrors (.claude/commands/opsx/, .cursor/commands/, .github/prompts/) stay committed on purpose: they are what makes /opsx:* work on a fresh clone, and the skills CLI never looks at them — it discovers SKILL.md files only.
Every change must pass all three before it is complete:
pnpm test # all Vitest tests
pnpm lint # type-check, no TypeScript errors
pnpm format # PrettierCI runs the same checks (plus pnpm build and pnpm format:check) on every pull request.
TDD is the expected workflow: write a failing test first, then the minimum implementation to make it pass.
- TypeScript strict, no
any— useunknownwith type guards, generics, or explicit types. - Functional style — pure functions over mutable classes,
constover reassignment, side effects at the edges. - Single responsibility — one thing per function/module/tool; extend by adding new modules (the
registerAllToolsregistry is the extension point) rather than modifying existing ones. - No backward-compatibility shims unless explicitly required — make breaking changes cleanly.
Skills live at plugin/skills/<skill>/SKILL.md (frontmatter description is the routing surface; the body is the playbook). Conventions:
- Descriptions must fit a 1,024-character budget and end with a
Not for:clause pointing at sibling skills — enforced bypackages/skill-tests/src/skill-descriptions/. - Some skills have an eval harness at
packages/skill-tests/evals/<skill>/(see each harness'sREADME.mdfor the run loop). Use it to measure whether a skill change helped or regressed. - To add a framework implementor (e.g.
formio-react), add the skill directory and register it as a row inplugin/skills/formio-application/FRAMEWORK.md— no orchestrator changes needed. See "Adding a framework implementor" below.
FRAMEWORK.md ships to every installation, so the steps for adding a framework live here rather than there. For a new framework skill (here, Vue):
-
Author the skill at
plugin/skills/formio-vue/— itsSKILL.md, sub-skills, and sibling documents — independent of the registry. -
In the same change, add one row to the registry in
plugin/skills/formio-application/FRAMEWORK.md, with all five cells. TheDefaultcell isno: exactly one row carriesDefault: yes, and changing which one is a separate decision from adding a framework. Write the detection signal to test only for the new framework's own presence.| Vue | formio-vue | formio-vue-resources — nested at formio-vue/formio-vue-resources/SKILL.md | `vue` listed in `package.json` dependencies | no | -
Check that the new Entry skill and Extend sub-skill accept the payloads in
FRAMEWORK.md→ "Handoff context reference" — no URLs; each resolves the project withproject_getforworkspacePath. If they don't, change the new framework skill; the orchestrator does not adapt. -
Run
pnpm test—packages/skill-tests/src/formio-react/framework-registry.test.tscovers the registry, and the routing eval inpackages/skill-tests/evals/skill-routing/measures whether the new description routes as intended.
Adding a framework is a row: no edit to the orchestrator's SKILL.md, INTENT.md, or IMPORT.md, and no new routing logic.
pnpm build:plugin # build the plugin bundle into dist/plugin/
pnpm test:plugin # verify the bundle
pnpm build:plugin:local # dist/plugin-local/: the same plugin, every client launching its own serverTo try this commit's skills and server together in a real session, build the local copy and load it from any folder outside this repository: claude --plugin-dir <repo>/dist/plugin-local. Its manifests all launch dist/plugin-local/server/stdio.js — the server compiled from this commit, with Node's --use-system-ca where supported — and the script installs that server's locked dependencies there, because Claude Code skips its dependency install for a plugin loaded in place. Disable an installed formio-ai plugin for that session, or you load the skills and the formio-mcp server twice. dist/plugin itself is unchanged and is still what pnpm test:plugin verifies.
examples/ ships copy-paste prompts for exercising the skills library end to end (see examples/README.md); the .claude/skills/ symlinks at the repo root load the live plugin/skills/ sources, so a fresh Claude Code session here tests uncommitted skill changes directly.
pnpm test mocks Form.io. pnpm test:live builds @formio/mcp, starts its stdio server, and calls every registered tool against a real Form.io project — the check that the server's assumptions about Form.io's responses still hold. Run it before a release and after any change to how a tool talks to Form.io. It is never part of pnpm test or CI.
The run creates and deletes forms, roles, and version tags, so it only runs against a throwaway project you name:
- Point the repository folder at that project the way every tool resolves it —
node packages/mcp-server/dist/stdio.js project set --project-url <projectUrl>from the repository root, after a build (or set theFORMIO_PROJECT_URLenvironment variable). - Set
FORMIO_LIVE_TEST_PROJECTto the same Project URL. The run refuses, before calling anything that reaches the deployment, when the folder resolves anywhere else. - Certificates: the runner starts the server, and makes its own clean-up requests, with Node's
--use-system-cawherever this Node supports it, so a local deployment whose certificate your operating system already trusts — a*.testhost in the macOS keychain, for example — works with verification on and nothing to set. For a certificate only a CA file vouches for, setNODE_EXTRA_CA_CERTS=<path to the CA's PEM>($(mkcert -CAROOT)/rootCA.pemfor an mkcert certificate).FORMIO_INSECURE_TLS=trueskips the check for the target project's and deployment's hosts only; the clean-up requests honour it exactly as the server does. - Run
FORMIO_LIVE_TEST_PROJECT=https://form.test/sandbox pnpm test:live. On first use the server prints a portal-login URL; open it and log in. Later runs reuse the cached login, or aFORMIO_API_KEYbound to that project.
Every entity a run creates carries a per-run prefix (mcplive<timestamp>). Clean-up runs after the cases — and on Ctrl-C, bounded so a stalled deployment cannot hold the exit — whether or not they passed: it deletes the run's forms, roles, and tags and restores the project's version, each part running on whatever could be listed, and it reports anything it could not list or remove. It authenticates exactly as the server does for the repository folder: an API key bound to that project (the one the FORMIO_PROJECT_URL environment variable names) first, otherwise the cached portal login. A registered tool with no case in packages/mcp-server/src/__tests__/live/cases.ts fails pnpm test, so a new tool ships with its live case.
Releases are driven by changesets. If your change affects a published package (@formio/mcp, @formio/ai), include a changeset:
pnpm changesetPick the bump level and describe the change. On merge to main, the release workflow opens/updates a "Version Packages" PR; merging that PR publishes to npm.
Four steps run inside pnpm changeset:version — which is what the release workflow invokes to build the Version Packages PR — so that PR carries their output and merging it is the only action needed:
pnpm changeset:followadds a patch changeset for@formio/aiwhen the pending release bumps@formio/mcpwithout it. The pins below live in files that ship inside the plugin, so a server-only release would otherwise change plugin content that never republishes, leaving the published plugin launching the previous server. The coupling is one-directional: a plugin-only release leaves the server alone.pnpm sync:versionswritesplugin/package.json's version into the three client manifests.pnpm sync:pinswritespackages/mcp-server/package.json's version — already bumped bychangeset versionat this point, so it is the version about to publish — into everynpxlaunch of the server (the Cursor and Agent Plugins manifests, the install docs, and every skill that prints the command) and intopackages/mcp-server/src/version.ts, the version the server reports.pnpm sync:plugin-serverwrites the server the Claude Code manifest runs:plugin/server/(the server'stscoutput, compiled without source maps or declarations), the server's runtime dependencies intoplugin/package.jsonat the exact versionspnpm-lock.yamlresolves, andplugin/package-lock.json— the full runtime closure read frompnpm-lock.yaml, each package with its registry URL and integrity hash, so the tree Claude Code installs is the one CI tested. It runs offline. Run it yourself after changing the server or its dependencies:pnpm testrunspnpm sync:plugin-server --check, andpnpm build:pluginrefuses a drifted copy.
Launches are pinned (npx -y @formio/mcp@<version>) rather than floating so an install runs the server the release was tested against instead of whatever the registry serves at that moment. Never hand-edit a pin or hand-write the follow-on changeset; the release PR is where both appear. pnpm sync:pins --check shows drift without writing, and plugin-manifests.test.ts, plugin-follows-server.test.ts, and mcp-setup-project-config.test.ts fail when either is stale.
CI also runs pnpm check:releases, which fails a PR that changes published content without a changeset releasing the owning package. It skips the changeset-release/* branch: that PR has already consumed every changeset, so a changeset added there would release the release.
Bumping the CDN assets the portal-login page loads (packages/mcp-server/src/auth.ts) means recomputing their Subresource Integrity digests. Do not hand-compute them — run pnpm sync:sri, which fetches each pinned URL and writes the digest of the bytes it served; pnpm sync:sri --check verifies them, and login-asset-integrity.test.ts runs that check whenever the CDN is reachable. A wrong digest passes every shape assertion and then blocks the renderer in the browser, hanging authenticate on a blank page.
- Branch from
main; keep PRs focused on one change. - Include the changeset when a published package is affected.
- All CI checks must pass.
Open a GitHub issue or see form.io for product documentation and support.