Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ The repository ships a Claude skills library at [plugin/skills/](plugin/skills/)

The library's default "build me an app" entry point is [`skills/formio-application/`](skills/formio-application/) — a framework-agnostic orchestrator that runs the full pipeline from plain-language intent through `formio-resource-planner`, the `project_import` MCP tool, and handoff to a framework-specific scaffolding skill. Today the only framework implementor is [`skills/formio-angular/`](skills/formio-angular/) (with its sub-skill [`skills/formio-angular/resources/`](skills/formio-angular/resources/) for per-resource NgModule work); `formio-angular` is no longer a top-level build-an-app skill — it is the Angular-specific implementor that `formio-application` delegates to. Future framework skills (`formio-react`, etc.) add themselves as rows in `skills/formio-application/FRAMEWORK.md`'s registry table; no other change to the orchestrator is required.

The library also ships [`skills/formio-sdk/`](plugin/skills/formio-sdk/) — a source-derived reference for the `@formio/js` SDK and `@formio/js/utils` Utilities, authored directly from the Form.io source code (`packages/core/src/sdk`, `packages/core/src/utils`, `packages/formio.js/src/Formio.js`, `packages/formio.js/src/utils`) rather than from drift-prone online docs. Reference docs cover SDK setup, auth, forms, submissions, projects, roles, files, plugins, VanillaJS rendering (`Formio.createForm`), and the Utils surface (Evaluator, form traversal, conditions, logic, JSONLogic, mask/sanitize, misc). The skill mandates ESM imports (`import { Formio } from '@formio/js'`, `import { Utils } from '@formio/js/utils'`) and explicit Hosted-vs-SaaS URL configuration.

The router skill's `description` follows a three-clause template: capability statement, a "Use when the user asks to …" trigger clause, and a "Not for: …" negative-trigger clause disambiguating from `formio-application` (orchestrator) and `formio-resource-planner` (planner). Each reference document includes a `## MCP Tool Preference` section instructing Claude to prefer the MCP server's first-party tools (`form_*`, `role_*`, `project_*`, `authenticate`) when they cover the requested operation.

Authentication: the MCP server uses a browser-based portal-login flow — a short-lived local Express server renders the Form.io portal login form and captures the returned JWT via a `/callback` endpoint; `formioFetch` then attaches `x-jwt-token` on every request. Skills do NOT use PKCE or API-key auth.

Skills are validated by [packages/mcp-server/src/skills-validator.ts](packages/mcp-server/src/skills-validator.ts); `pnpm test` fails if the router's frontmatter drifts, the router description is missing its trigger or negative-trigger clause, any required reference file is missing or empty, any reference doc drifts from the required heading layout, the canonical portal-login JWT auth paragraph is missing (except in `server-status.md`), or scope-consistency rules are violated. Terminology is strict: `baseUrl`/`base_url` refers only to `FORMIO_BASE_URL`; `projectUrl`/`project_url` refers only to `FORMIO_PROJECT_URL`.
Skill authoring conventions (not enforced by automated tests): the router's frontmatter and three-clause description, required reference files present and non-empty, the required reference-doc heading layout, the canonical portal-login JWT auth paragraph (except in `server-status.md`), and scope consistency. Terminology is strict: `baseUrl`/`base_url` refers only to `FORMIO_BASE_URL`; `projectUrl`/`project_url` refers only to `FORMIO_PROJECT_URL`.

## Iterating on skills

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Structure: `{ "<projectUrl>": "<jwt>" }`

**Why `~/.formio/`**: Follows the convention of CLI tools storing config in a dotfile directory in the user's home. Keeps it separate from project-level config.

### 4. Token validation via `GET {projectUrl}/current`
### 4. Token validation via `GET {baseUrl}/current`

On startup (after reading config and cache), send a request to the `/current` endpoint. If it returns 200, the token is valid. If 401, clear the cached token and trigger the login flow (or fail in API key mode).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The MCP server currently authenticates all API requests using a shared admin API

- Add an authentication module that spins up an ephemeral Express server, renders the project's login form via the Form.io SDK, captures the user's JWT on successful login, and shuts down
- Add token caching to disk (`~/.formio/mcp-tokens.json`) keyed by project URL so users don't re-authenticate on every MCP server restart
- Add a startup token validation step that hits `GET {projectUrl}/current` to check if a cached or provided token is still valid
- Add a startup token validation step that hits `GET {baseUrl}/current` to check if a cached or provided token is still valid
- **BREAKING**: `FORMIO_API_KEY` becomes optional instead of required — if not provided, the server triggers the browser login flow
- Add `FORMIO_LOGIN_FORM` optional env var to override the default login form URL (`{projectUrl}/user/login`)
- Modify `formioFetch` to send `x-jwt-token` header when using a user JWT, falling back to `x-token` when using an API key
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,31 @@

### Requirement: Token is validated on startup via GET /current

On startup, the MCP server SHALL validate any available token (cached JWT or API key) by sending a request to `GET {projectUrl}/current` with the appropriate auth header. A 200 response means the token is valid.
On startup, the MCP server SHALL validate any available token (cached JWT or API key) by sending a request to `GET {baseUrl}/current` with the appropriate auth header. A 200 response means the token is valid.

#### Scenario: Valid JWT token on startup

- **WHEN** the server starts with a cached JWT that is still valid
- **THEN** `GET {projectUrl}/current` returns 200
- **THEN** `GET {baseUrl}/current` returns 200
- **AND** the server proceeds without triggering the login flow

#### Scenario: Valid API key on startup

- **WHEN** the server starts with `FORMIO_API_KEY` set to a valid key
- **THEN** `GET {projectUrl}/current` with `x-token` header returns 200
- **THEN** `GET {baseUrl}/current` with `x-token` header returns 200
- **AND** the server proceeds normally

#### Scenario: Expired JWT triggers login flow

- **WHEN** the server starts with a cached JWT that has expired
- **THEN** `GET {projectUrl}/current` returns 401
- **THEN** `GET {baseUrl}/current` returns 401
- **AND** the cached token is cleared
- **AND** the login flow is triggered

#### Scenario: Invalid API key fails with error

- **WHEN** the server starts with `FORMIO_API_KEY` set to an invalid key
- **THEN** `GET {projectUrl}/current` returns 401
- **THEN** `GET {baseUrl}/current` returns 401
- **AND** the server throws an error indicating the API key is invalid

#### Scenario: No token and no API key triggers login flow
Expand Down
2 changes: 1 addition & 1 deletion openspec/changes/archive/2026-04-16-add-user-auth/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@

### Green

- [x] 4.5 Create `src/token-validation.ts` with `validateToken(config)` that sends `GET {projectUrl}/current` with the appropriate auth header and returns a boolean
- [x] 4.5 Create `src/token-validation.ts` with `validateToken(config)` that sends `GET {baseUrl}/current` with the appropriate auth header and returns a boolean

### Refactor

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The MCP server SHALL register a new tool named `authenticate` at `packages/mcp-s
- Returns a JSON-serialized text content block whose payload is `{ authenticated: boolean, cached: boolean, projectUrl: string, userEmail?: string }`. The JWT MUST NOT appear in the return payload.
- Reads the project URL from the server's `FormioConfig`. The agent does NOT pass a URL.
- `cached: true` when the JWT was already present before the call; `cached: false` when the call triggered a fresh login.
- `userEmail` is best-effort — populated from a `GET {projectUrl}/current` call when the returned submission has an email field. Any error fetching the current user is swallowed; the field is simply omitted.
- `userEmail` is best-effort — populated from a `GET {baseUrl}/current` call when the returned submission has an email field. Any error fetching the current user is swallowed; the field is simply omitted.
- Is the tool Step 4 of `formio-application` calls explicitly to trigger authentication. It is also available to any other skill that wants to pre-authenticate before a sensitive sequence.

#### Scenario: Tool is registered and callable
Expand All @@ -33,7 +33,7 @@ The MCP server SHALL register a new tool named `authenticate` at `packages/mcp-s

#### Scenario: Current-user email included when available

- **WHEN** a successful `authenticate` call finishes and `GET {projectUrl}/current` returns a submission with an email field
- **WHEN** a successful `authenticate` call finishes and `GET {baseUrl}/current` returns a submission with an email field
- **THEN** the payload contains `userEmail: <the email>`

#### Scenario: Current-user fetch failure is swallowed
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-tdd
created: 2026-05-22
Loading
Loading