From ec94b8a0c22151992aa255bfe12124d0b6876588 Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Tue, 23 Jun 2026 10:55:46 -0500 Subject: [PATCH 1/3] Fixing issues with the base url not getting properly set and colliding with other configured base url settings. --- .../src/__tests__/project-resolver.test.ts | 22 +++ .../src/__tests__/project_set.test.ts | 128 +++++++++++++++++- packages/mcp-server/src/project-resolver.ts | 8 +- packages/mcp-server/src/tools/project_set.ts | 32 ++++- plugin/README.md | 4 +- plugin/hooks/hooks.json | 6 +- plugin/hooks/verify-project-url.mjs | 13 +- 7 files changed, 193 insertions(+), 20 deletions(-) diff --git a/packages/mcp-server/src/__tests__/project-resolver.test.ts b/packages/mcp-server/src/__tests__/project-resolver.test.ts index f7a084a..0974082 100644 --- a/packages/mcp-server/src/__tests__/project-resolver.test.ts +++ b/packages/mcp-server/src/__tests__/project-resolver.test.ts @@ -37,6 +37,28 @@ describe('resolveProjectConfig', () => { expect(cfg.apiKey).toBe('abc'); }); + it('prefers the mapped base URL over baseConfig.baseUrl in plugin context', () => { + writeProjectEntry('/workspace/pkg-a', { + FORMIO_PROJECT_URL: 'https://mapped.form.io/mapped', + FORMIO_BASE_URL: 'https://mapped.form.io', + }); + + const cfg = resolveProjectConfig('/workspace/pkg-a', baseConfig); + + expect(cfg.baseUrl).toBe('https://mapped.form.io'); + expect(cfg.projectUrl).toBe('https://mapped.form.io/mapped'); + }); + + it('falls back to baseConfig.baseUrl when the mapped entry has no base URL', () => { + writeProjectEntry('/workspace/pkg-a', { + FORMIO_PROJECT_URL: 'https://api.form.io/mapped', + }); + + const cfg = resolveProjectConfig('/workspace/pkg-a', baseConfig); + + expect(cfg.baseUrl).toBe('https://api.form.io'); + }); + it('strips a trailing slash from the mapped project URL', () => { writeProjectEntry('/workspace/pkg-a', { FORMIO_PROJECT_URL: 'https://api.form.io/mapped/', diff --git a/packages/mcp-server/src/__tests__/project_set.test.ts b/packages/mcp-server/src/__tests__/project_set.test.ts index 0cb203b..2da4d56 100644 --- a/packages/mcp-server/src/__tests__/project_set.test.ts +++ b/packages/mcp-server/src/__tests__/project_set.test.ts @@ -8,7 +8,10 @@ import os from 'os'; import { readProjectEntry } from '../project-map.js'; import { registerProjectSetTool } from '../tools/project_set.js'; -async function createTestClient(options?: { cwd?: () => string }) { +async function createTestClient(options?: { + cwd?: () => string; + baseUrl?: () => string | undefined; +}) { const server = new McpServer({ name: 'test', version: '0.0.0' }); registerProjectSetTool(server, options); @@ -128,6 +131,129 @@ describe('project_set tool', () => { expect(mtimeAfter).toBe(mtimeBefore); }); + it('persists FORMIO_BASE_URL alongside FORMIO_PROJECT_URL when a base URL is available', async () => { + const { client } = await createTestClient({ + cwd: () => cwd, + baseUrl: () => 'https://api.form.io', + }); + + await client.callTool({ + name: 'project_set', + arguments: { projectUrl: 'https://api.form.io/next' }, + }); + + expect(readProjectEntry(cwd)).toEqual({ + env: { + FORMIO_PROJECT_URL: 'https://api.form.io/next', + FORMIO_BASE_URL: 'https://api.form.io', + }, + }); + }); + + it('persists an explicit baseUrl argument, overriding the env global', async () => { + const { client } = await createTestClient({ + cwd: () => cwd, + baseUrl: () => 'https://global.form.io', + }); + + await client.callTool({ + name: 'project_set', + arguments: { + projectUrl: 'https://enterprise.form.io/next', + baseUrl: 'https://enterprise.form.io', + }, + }); + + expect(readProjectEntry(cwd)).toEqual({ + env: { + FORMIO_PROJECT_URL: 'https://enterprise.form.io/next', + FORMIO_BASE_URL: 'https://enterprise.form.io', + }, + }); + }); + + it('strips a trailing slash from an explicit baseUrl argument', async () => { + const { client } = await createTestClient({ cwd: () => cwd }); + + await client.callTool({ + name: 'project_set', + arguments: { + projectUrl: 'https://enterprise.form.io/next', + baseUrl: 'https://enterprise.form.io/', + }, + }); + + expect(readProjectEntry(cwd)).toEqual({ + env: { + FORMIO_PROJECT_URL: 'https://enterprise.form.io/next', + FORMIO_BASE_URL: 'https://enterprise.form.io', + }, + }); + }); + + it('rejects an invalid baseUrl and does not write', async () => { + const { client } = await createTestClient({ cwd: () => cwd }); + + const result = await client.callTool({ + name: 'project_set', + arguments: { + projectUrl: 'https://api.form.io/next', + baseUrl: 'not a url', + }, + }); + + expect(result.isError).toBe(true); + expect(readProjectEntry(cwd)).toBeNull(); + }); + + it('strips a trailing slash from the base URL before persisting', async () => { + const { client } = await createTestClient({ + cwd: () => cwd, + baseUrl: () => 'https://api.form.io/', + }); + + await client.callTool({ + name: 'project_set', + arguments: { projectUrl: 'https://api.form.io/next' }, + }); + + expect(readProjectEntry(cwd)).toEqual({ + env: { + FORMIO_PROJECT_URL: 'https://api.form.io/next', + FORMIO_BASE_URL: 'https://api.form.io', + }, + }); + }); + + it('rewrites the entry when only the base URL changed for the same project URL', async () => { + const first = await createTestClient({ + cwd: () => cwd, + baseUrl: () => 'https://old.form.io', + }); + await first.client.callTool({ + name: 'project_set', + arguments: { projectUrl: 'https://api.form.io/same' }, + }); + + const second = await createTestClient({ + cwd: () => cwd, + baseUrl: () => 'https://new.form.io', + }); + const result = await second.client.callTool({ + name: 'project_set', + arguments: { projectUrl: 'https://api.form.io/same' }, + }); + + const [first0] = result.content as Array<{ type: string; text: string }>; + expect(first0.text).not.toContain('no change'); + expect(readProjectEntry(cwd)).toEqual({ + env: { + FORMIO_PROJECT_URL: 'https://api.form.io/same', + FORMIO_BASE_URL: 'https://new.form.io', + }, + }); + }); + it('persists under the explicit cwd argument when provided, ignoring server cwd', async () => { const serverCwd = '/workspace/server-root'; const userCwd = '/workspace/server-root/packages/inner'; diff --git a/packages/mcp-server/src/project-resolver.ts b/packages/mcp-server/src/project-resolver.ts index 3720e5b..3713819 100644 --- a/packages/mcp-server/src/project-resolver.ts +++ b/packages/mcp-server/src/project-resolver.ts @@ -25,19 +25,21 @@ export function resolveProjectConfig(cwd: string, baseConfig: FormioConfig): Res // authoritative. Standalone context: the map is at best stale leftover from // prior plugin use in this cwd — ignore it so the user's .mcp.json env wins. const pluginContext = process.env.FORMIO_PLUGIN_CONTEXT === '1'; - const mapped = pluginContext ? readProjectEntry(cwd)?.env.FORMIO_PROJECT_URL : undefined; + const mappedEnv = pluginContext ? readProjectEntry(cwd)?.env : undefined; + const mapped = mappedEnv?.FORMIO_PROJECT_URL; const projectUrl = mapped ?? baseConfig.projectUrl; if (!projectUrl) { throw new Error( `No Form.io project is mapped for cwd=${cwd}. Call project_set with projectUrl and cwd=${cwd}, or set the FORMIO_PROJECT_URL environment variable, before invoking Form.io tools.` ); } - if (!baseConfig.baseUrl) { + const baseUrl = mappedEnv?.FORMIO_BASE_URL ?? baseConfig.baseUrl; + if (!baseUrl) { throw new Error('baseUrl is missing on config. getConfig() should always populate it.'); } return { ...baseConfig, - baseUrl: baseConfig.baseUrl, + baseUrl: baseUrl.replace(/\/+$/, ''), projectUrl: projectUrl.replace(/\/+$/, ''), }; } diff --git a/packages/mcp-server/src/tools/project_set.ts b/packages/mcp-server/src/tools/project_set.ts index fca58d8..813f7b6 100644 --- a/packages/mcp-server/src/tools/project_set.ts +++ b/packages/mcp-server/src/tools/project_set.ts @@ -2,25 +2,30 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { readProjectEntry, writeProjectEntry } from '../project-map.js'; -function normalizeProjectUrl(input: string): string { +function normalizeHttpUrl(input: string, label: string): string { let parsed: URL; try { parsed = new URL(input); } catch { - throw new Error(`projectUrl must be a valid URL, got: ${input}`); + throw new Error(`${label} must be a valid URL, got: ${input}`); } if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { - throw new Error(`projectUrl must use http or https, got: ${parsed.protocol}`); + throw new Error(`${label} must use http or https, got: ${parsed.protocol}`); } return input.replace(/\/+$/, ''); } export interface ProjectSetOptions { cwd?: () => string; + baseUrl?: () => string | undefined; } export function registerProjectSetTool(server: McpServer, options: ProjectSetOptions = {}) { const getServerCwd = options.cwd ?? (() => process.cwd()); + // Fallback base URL when the caller does not pass one explicitly: the plugin + // user-config sets FORMIO_BASE_URL in the server env (one global value). An + // explicit baseUrl argument lets each cwd map to its own deployment. + const getEnvBaseUrl = options.baseUrl ?? (() => process.env.FORMIO_BASE_URL); server.tool( 'project_set', [ @@ -41,14 +46,23 @@ export function registerProjectSetTool(server: McpServer, options: ProjectSetOpt .describe( "User's current working directory to key the persisted mapping against. Pass whenever known (e.g. from UserPromptSubmit hook context). Falls back to the MCP server's process.cwd() when omitted." ), + baseUrl: z + .url({ protocol: /^https?$/ }) + .optional() + .describe( + 'Deployment URL for the Form.io Enterprise Server that hosts this project, e.g. https://api.form.io. Persisted per-cwd alongside the project URL so each directory can target a different deployment. Falls back to the global FORMIO_BASE_URL when omitted.' + ), }, - async ({ projectUrl, cwd }) => { - const normalized = normalizeProjectUrl(projectUrl); + async ({ projectUrl, cwd, baseUrl: baseUrlArg }) => { + const normalized = normalizeHttpUrl(projectUrl, 'projectUrl'); + const resolvedBase = baseUrlArg ?? getEnvBaseUrl(); + const baseUrl = resolvedBase ? normalizeHttpUrl(resolvedBase, 'baseUrl') : undefined; const entryCwd = cwd ?? getServerCwd(); const existing = readProjectEntry(entryCwd); const previousMapped = existing?.env.FORMIO_PROJECT_URL; + const previousBase = existing?.env.FORMIO_BASE_URL; - if (previousMapped === normalized) { + if (previousMapped === normalized && previousBase === baseUrl) { return { content: [ { @@ -59,7 +73,11 @@ export function registerProjectSetTool(server: McpServer, options: ProjectSetOpt }; } - writeProjectEntry(entryCwd, { FORMIO_PROJECT_URL: normalized }); + const env: Record = { FORMIO_PROJECT_URL: normalized }; + if (baseUrl) { + env.FORMIO_BASE_URL = baseUrl; + } + writeProjectEntry(entryCwd, env); const message = previousMapped ? `Active project set to ${normalized} (was ${previousMapped}; persisted for ${entryCwd})` : `Active project set to ${normalized}; mapping persisted for ${entryCwd}`; diff --git a/plugin/README.md b/plugin/README.md index d92128a..0277d2a 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -50,12 +50,12 @@ The plugin prompts for `FORMIO_BASE_URL` and the default `FORMIO_PROJECT_URL` on | Name | Required | Default | Purpose | | --- | :-: | --- | --- | -| `FORMIO_BASE_URL` | yes | — | Full base URL of your Form.io deployment (e.g. `https://api.form.io`). Set via plugin user-config. | +| `FORMIO_BASE_URL` | yes\* | — | Full base URL of your Form.io deployment (e.g. `https://api.form.io`). Set via plugin user-config. In plugin mode, only the global fallback the `verify-project-url` hook offers as the default base URL when prompting for an unmapped cwd. | | `FORMIO_PROJECT_URL` | yes\* | — | Full URL of the Form.io project the MCP server should target. In plugin mode, only used as the pre-filled default the `verify-project-url` hook offers when prompting for an unmapped cwd. | | `FORMIO_API_KEY` | no | `undefined` | Long-lived project API key. When set, the server skips the browser login flow and attaches `x-token`. | | `FORMIO_LOGIN_FORM` | no | Auto-resolved | Override the portal login form URL. | -\* The `verify-project-url` hook persists per-cwd mappings to `~/.formio/projects.json` via the `project_set` MCP tool (offering the plugin user-config `formio_default_project_url` as the default). Once a directory is mapped, the MCP server resolves `FORMIO_PROJECT_URL` from that file instead of env. +\* The `verify-project-url` hook persists per-cwd mappings to `~/.formio/projects.json` via the `project_set` MCP tool — prompting for **both** the project URL (default: `formio_default_project_url`) and the deployment/base URL (default: `formio_base_url`). Once a directory is mapped, the MCP server resolves `FORMIO_PROJECT_URL` **and** `FORMIO_BASE_URL` from that file per-cwd, falling back to the global env values only when an entry omits them. ### Authentication modes diff --git a/plugin/hooks/hooks.json b/plugin/hooks/hooks.json index 0bc51cf..ae4af8a 100644 --- a/plugin/hooks/hooks.json +++ b/plugin/hooks/hooks.json @@ -6,7 +6,7 @@ { "matcher": "*", "type": "command", - "command": "FORMIO_DEFAULT_PROJECT_URL=\"${user_config.formio_default_project_url}\" node \"${CLAUDE_PLUGIN_ROOT}/hooks/verify-project-url.mjs\"" + "command": "FORMIO_DEFAULT_PROJECT_URL=\"${user_config.formio_default_project_url}\" FORMIO_DEFAULT_BASE_URL=\"${user_config.formio_base_url}\" node \"${CLAUDE_PLUGIN_ROOT}/hooks/verify-project-url.mjs\"" } ] } @@ -17,7 +17,7 @@ { "matcher": "*", "type": "command", - "command": "FORMIO_DEFAULT_PROJECT_URL=\"${user_config.formio_default_project_url}\" node \"${CLAUDE_PLUGIN_ROOT}/hooks/verify-project-url.mjs\"" + "command": "FORMIO_DEFAULT_PROJECT_URL=\"${user_config.formio_default_project_url}\" FORMIO_DEFAULT_BASE_URL=\"${user_config.formio_base_url}\" node \"${CLAUDE_PLUGIN_ROOT}/hooks/verify-project-url.mjs\"" } ] } @@ -28,7 +28,7 @@ "hooks": [ { "type": "command", - "command": "FORMIO_DEFAULT_PROJECT_URL=\"${user_config.formio_default_project_url}\" node \"${CLAUDE_PLUGIN_ROOT}/hooks/verify-project-url.mjs\"" + "command": "FORMIO_DEFAULT_PROJECT_URL=\"${user_config.formio_default_project_url}\" FORMIO_DEFAULT_BASE_URL=\"${user_config.formio_base_url}\" node \"${CLAUDE_PLUGIN_ROOT}/hooks/verify-project-url.mjs\"" } ] } diff --git a/plugin/hooks/verify-project-url.mjs b/plugin/hooks/verify-project-url.mjs index 92111a9..ea7038f 100755 --- a/plugin/hooks/verify-project-url.mjs +++ b/plugin/hooks/verify-project-url.mjs @@ -100,13 +100,18 @@ const mapPath = join(homedir(), '.formio', 'projects.json'); if (readMappedUrl(mapPath, cwd)) { process.exit(0); } -// this env var is defined by the hooks; it doesn't exist in the MCP server +// these env vars are defined by the hooks; they don't exist in the MCP server const defaultUrl = process.env.FORMIO_DEFAULT_PROJECT_URL ?? ''; +const defaultBaseUrl = process.env.FORMIO_DEFAULT_BASE_URL ?? ''; const rule = '~/.formio/projects.json is owned by project_set; never write it by hand (no Write/Edit/heredoc/jq). If project_set fails, surface the error — do not work around it.'; -const reason = defaultUrl - ? `No project mapped for ${cwd}. AskUserQuestion: 'Use default (${defaultUrl})' or 'Other'. Then project_set({ cwd, projectUrl }), then retry. ${rule}` - : `No project mapped for ${cwd}. AskUserQuestion for URL, then project_set({ cwd, projectUrl }), then retry. ${rule}`; +const projectClause = defaultUrl + ? `For the project URL, AskUserQuestion: 'Use default (${defaultUrl})' or 'Other'.` + : `AskUserQuestion for the project URL.`; +const baseClause = defaultBaseUrl + ? `For the deployment (base) URL of the Form.io Enterprise Server, AskUserQuestion: 'Use default (${defaultBaseUrl})' or 'Other'.` + : `AskUserQuestion for the deployment (base) URL of the Form.io Enterprise Server.`; +const reason = `No project mapped for ${cwd}. ${projectClause} ${baseClause} Then project_set({ cwd, projectUrl, baseUrl }), then retry. ${rule}`; const output = event === 'PreToolUse' From 33a27245c73a150eb6b07e4e8335b1eaa9ee0591 Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Tue, 23 Jun 2026 11:21:12 -0500 Subject: [PATCH 2/3] Adding changeset. --- .changeset/curvy-pets-eat.md | 6 ++++ package.json | 2 +- pnpm-lock.yaml | 58 ++++++++++++++++++------------------ turbo.json | 2 +- 4 files changed, 37 insertions(+), 31 deletions(-) create mode 100644 .changeset/curvy-pets-eat.md diff --git a/.changeset/curvy-pets-eat.md b/.changeset/curvy-pets-eat.md new file mode 100644 index 0000000..7c7a761 --- /dev/null +++ b/.changeset/curvy-pets-eat.md @@ -0,0 +1,6 @@ +--- +'@formio/mcp': patch +'@formio/ai': patch +--- + +Fixed issues with baseURL not getting set correctly. diff --git a/package.json b/package.json index 9497644..60e4425 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "husky": "^9.1.7", "prettier": "^3.5.3", "tsx": "^4.19.3", - "turbo": "^2.3.3", + "turbo": "^2.9.18", "typescript-eslint": "^8.30.1" }, "packageManager": "pnpm@10.33.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81b3962..9d8f51e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,8 +33,8 @@ importers: specifier: ^4.19.3 version: 4.21.0 turbo: - specifier: ^2.3.3 - version: 2.9.5 + specifier: ^2.9.18 + version: 2.9.18 typescript-eslint: specifier: ^8.30.1 version: 8.58.1(eslint@9.39.4)(typescript@5.9.3) @@ -604,33 +604,33 @@ packages: '@sphinxxxx/color-conversion@2.2.2': resolution: {integrity: sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw==} - '@turbo/darwin-64@2.9.5': - resolution: {integrity: sha512-qPxhKsLMQP+9+dsmPgAGidi5uNifD4AoAOnEnljab3Qgn0QZRR31Hp+/CgW3Ia5AanWj6JuLLTBYvuQj4mqTWg==} + '@turbo/darwin-64@2.9.18': + resolution: {integrity: sha512-9f27peFu16ur8c0v9nUFUEyBnbKuuFsUTjHFWfmwGfzySBXbHwzU44QhZon6Mznz0cHsIr3984NQj/bVrnGSRw==} cpu: [x64] os: [darwin] - '@turbo/darwin-arm64@2.9.5': - resolution: {integrity: sha512-vkF/9F/l3aWd4bHxTui5Hh0F5xrTZ4e3rbBsc57zA6O8gNbmHN3B6eZ5psAIP2CnJRZ8ZxRjV3WZHeNXMXkPBw==} + '@turbo/darwin-arm64@2.9.18': + resolution: {integrity: sha512-9A6TMRq/Ib+QnbhLlgkhOm+624wO4pzSQ/yQviQfWHOlFvaYxdnIAYmu2H6TS6y7kSVL0DvzNe04NbESTOzFVQ==} cpu: [arm64] os: [darwin] - '@turbo/linux-64@2.9.5': - resolution: {integrity: sha512-z/Get5NUaUxm5HSGFqVMICDRjFNsCUhSc4wnFa/PP1QD0NXCjr7bu9a2EM6md/KMCBW0Qe393Ac+UM7/ryDDTw==} + '@turbo/linux-64@2.9.18': + resolution: {integrity: sha512-zCdIDtz69AnbYh913elJRRoF3QY5aa2HNnf+4rAkc7bQ+tWujiDkCNV7stazOUPggaDvhKIf2Z87qHftTeXSkw==} cpu: [x64] os: [linux] - '@turbo/linux-arm64@2.9.5': - resolution: {integrity: sha512-jyBifaNoI5/NheyswomiZXJvjdAdvT7hDRYzQ4meP0DKGvpXUjnqsD+4/J2YSDQ34OHxFkL30FnSCUIVOh2PHw==} + '@turbo/linux-arm64@2.9.18': + resolution: {integrity: sha512-Va1kXI04naMgYwqv/5Dfa36dTDx8015U7oaQAjrXa45ua9OoFjSV4OmvkML4EmXvUclQHCiBRbY8bvd0jV7eAg==} cpu: [arm64] os: [linux] - '@turbo/windows-64@2.9.5': - resolution: {integrity: sha512-ph24K5uPtvo7UfuyDXnBiB/8XvrO+RQWbbw5zkA/bVNoy9HDiNoIJJj3s62MxT9tjEb6DnPje5PXSz1UR7QAyg==} + '@turbo/windows-64@2.9.18': + resolution: {integrity: sha512-m0kDhZANxSNz9ck1ybogFscHabriAsp4eDFNrN/1H5WrgTF7b3VlcPZnhuO3v2+E2KnCbeAc+UUT10BZZHdDKw==} cpu: [x64] os: [win32] - '@turbo/windows-arm64@2.9.5': - resolution: {integrity: sha512-6c5RccT/+iR39SdT1G5HyZaD2n57W77o+l0TTfxG/cVlhV94Acyg2gTQW7zUOhW1BeQpBjHzu9x8yVBZwrHh7g==} + '@turbo/windows-arm64@2.9.18': + resolution: {integrity: sha512-nUdR8WqoomUys9iIQmG45TMiizJ+5BV8egSeLLZba/AWblyp3fVBcIH1kSE58OtK4g2YzbMJEth6Ttv9w5rqMA==} cpu: [arm64] os: [win32] @@ -2124,8 +2124,8 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - turbo@2.9.5: - resolution: {integrity: sha512-JXNkRe6H6MjSlk5UQRTjyoKX5YN2zlc2632xcSlSFBao5yvbMWTpv9SNolOZlZmUlcDOHuszPLItbKrvcXnnZA==} + turbo@2.9.18: + resolution: {integrity: sha512-bwabv6PupzeavybzEoArBAkwq5fnzwf8OFnRtpHwnviFWuwJPFxtyH+aVp36TmIqK3aYYgtTJ3J0m2ysxxSzQg==} hasBin: true type-check@0.4.0: @@ -2852,22 +2852,22 @@ snapshots: '@sphinxxxx/color-conversion@2.2.2': {} - '@turbo/darwin-64@2.9.5': + '@turbo/darwin-64@2.9.18': optional: true - '@turbo/darwin-arm64@2.9.5': + '@turbo/darwin-arm64@2.9.18': optional: true - '@turbo/linux-64@2.9.5': + '@turbo/linux-64@2.9.18': optional: true - '@turbo/linux-arm64@2.9.5': + '@turbo/linux-arm64@2.9.18': optional: true - '@turbo/windows-64@2.9.5': + '@turbo/windows-64@2.9.18': optional: true - '@turbo/windows-arm64@2.9.5': + '@turbo/windows-arm64@2.9.18': optional: true '@types/body-parser@1.19.6': @@ -4469,14 +4469,14 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - turbo@2.9.5: + turbo@2.9.18: optionalDependencies: - '@turbo/darwin-64': 2.9.5 - '@turbo/darwin-arm64': 2.9.5 - '@turbo/linux-64': 2.9.5 - '@turbo/linux-arm64': 2.9.5 - '@turbo/windows-64': 2.9.5 - '@turbo/windows-arm64': 2.9.5 + '@turbo/darwin-64': 2.9.18 + '@turbo/darwin-arm64': 2.9.18 + '@turbo/linux-64': 2.9.18 + '@turbo/linux-arm64': 2.9.18 + '@turbo/windows-64': 2.9.18 + '@turbo/windows-arm64': 2.9.18 type-check@0.4.0: dependencies: diff --git a/turbo.json b/turbo.json index 3049876..19e991f 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,5 @@ { - "$schema": "https://turbo.build/schema.json", + "$schema": "https://v2-9-18.turborepo.dev/schema.json", "tasks": { "build": { "outputs": ["dist/**"] From ae12152b391384ae64551062147e345fca02c324 Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Tue, 23 Jun 2026 11:38:32 -0500 Subject: [PATCH 3/3] Fixing small glitch with FORMIO_INSECURE_TLS --- .../src/__tests__/insecure-tls.test.ts | 30 +++++++++++++++++++ packages/mcp-server/src/formio-client.ts | 13 +++++++- 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 packages/mcp-server/src/__tests__/insecure-tls.test.ts diff --git a/packages/mcp-server/src/__tests__/insecure-tls.test.ts b/packages/mcp-server/src/__tests__/insecure-tls.test.ts new file mode 100644 index 0000000..ac83798 --- /dev/null +++ b/packages/mcp-server/src/__tests__/insecure-tls.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import { isInsecureTlsEnabled } from '../formio-client.js'; + +describe('isInsecureTlsEnabled', () => { + it('returns true for "true"', () => { + expect(isInsecureTlsEnabled('true')).toBe(true); + }); + + it('returns true for "1"', () => { + expect(isInsecureTlsEnabled('1')).toBe(true); + }); + + it('returns true for mixed/upper case "TRUE"', () => { + expect(isInsecureTlsEnabled('TRUE')).toBe(true); + expect(isInsecureTlsEnabled('True')).toBe(true); + }); + + it('trims surrounding whitespace', () => { + expect(isInsecureTlsEnabled(' true ')).toBe(true); + expect(isInsecureTlsEnabled(' 1 ')).toBe(true); + }); + + it('returns false for falsy/other values', () => { + expect(isInsecureTlsEnabled(undefined)).toBe(false); + expect(isInsecureTlsEnabled('')).toBe(false); + expect(isInsecureTlsEnabled('0')).toBe(false); + expect(isInsecureTlsEnabled('false')).toBe(false); + expect(isInsecureTlsEnabled('yes')).toBe(false); + }); +}); diff --git a/packages/mcp-server/src/formio-client.ts b/packages/mcp-server/src/formio-client.ts index ebe760c..dba9fdf 100644 --- a/packages/mcp-server/src/formio-client.ts +++ b/packages/mcp-server/src/formio-client.ts @@ -3,7 +3,18 @@ import { getAuthHeader } from './auth-header.js'; import { ensureAuthenticated, invalidateJwtCache } from './ensure-auth.js'; import { clearToken } from './token-cache.js'; -if (process.env.FORMIO_INSECURE_TLS === 'true') { +// Accept the common truthy spellings ("true", "TRUE", "1") so a self-signed +// deployment (e.g. a local Form.io Enterprise server) is not rejected over a +// trivial casing/format mismatch in the env value. +export function isInsecureTlsEnabled(value: string | undefined): boolean { + if (!value) { + return false; + } + const normalized = value.trim().toLowerCase(); + return normalized === 'true' || normalized === '1'; +} + +if (isInsecureTlsEnabled(process.env.FORMIO_INSECURE_TLS)) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; }