From c8f48ce6b1b43b0220426e62001ae285647e4232 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 19 Oct 2026 23:24:40 +0000 Subject: [PATCH 1/5] Derive the Cursor plugin CLI pin from packages/cli/package.json sync-agent-versions.ts rewrites every @pascal-app/cli@ quote and the server.json version from the package versions; skills:validate fails on drift. The pin moves from 1.0.0 to the published 1.0.4. --- .cursor-plugin/mcp.json | 2 +- package.json | 2 +- scripts/cursor-plugin-policy.test.ts | 14 ++++- scripts/cursor-plugin-policy.ts | 3 +- scripts/sync-agent-versions.test.ts | 47 ++++++++++++++++ scripts/sync-agent-versions.ts | 69 ++++++++++++++++++++++++ skills/.cursor-plugin/mcp.json | 2 +- skills/furniture-fit/references/setup.md | 2 +- skills/pascal-3d/references/setup.md | 2 +- 9 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 scripts/sync-agent-versions.test.ts create mode 100644 scripts/sync-agent-versions.ts diff --git a/.cursor-plugin/mcp.json b/.cursor-plugin/mcp.json index 393dd3b26c..0983e143d9 100644 --- a/.cursor-plugin/mcp.json +++ b/.cursor-plugin/mcp.json @@ -5,7 +5,7 @@ "command": "npx", "args": [ "--yes", - "--package=@pascal-app/cli@1.0.0", + "--package=@pascal-app/cli@1.0.4", "pascal", "mcp", "connect" diff --git a/package.json b/package.json index 44d899d105..f402d84ba4 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "test": "turbo run test", "ci": "bun run check && bun run skills:validate && bun run check-types && bun run test && bun run build", "postinstall": "([ -n \"$CI\" ] || [ ! -e .git ] || git config core.hooksPath .githooks)", - "skills:validate": "bun scripts/validate-skills.ts && bun test scripts/cursor-plugin-policy.test.ts scripts/clawhub-ignore-policy.test.ts scripts/claude-mcp-config-policy.test.ts scripts/openai-tool-annotation-policy.test.ts scripts/path-containment.test.ts scripts/public-skill-discovery-policy.test.ts", + "skills:validate": "bun scripts/validate-skills.ts && bun scripts/sync-agent-versions.ts --check && bun test scripts/sync-agent-versions.test.ts scripts/cursor-plugin-policy.test.ts scripts/clawhub-ignore-policy.test.ts scripts/claude-mcp-config-policy.test.ts scripts/openai-tool-annotation-policy.test.ts scripts/path-containment.test.ts scripts/public-skill-discovery-policy.test.ts", "kill": "lsof -ti:3002 | xargs kill -9 2>/dev/null || echo 'No processes found on port 3002'", "clean:cache": "rm -rf apps/*/.next apps/*/.swc apps/*/.turbo packages/*/.turbo tooling/*/.turbo .turbo node_modules/.cache", "restart": "bun kill && bun clean:cache && bun dev", diff --git a/scripts/cursor-plugin-policy.test.ts b/scripts/cursor-plugin-policy.test.ts index 5e01859e89..b8fe37b2cc 100644 --- a/scripts/cursor-plugin-policy.test.ts +++ b/scripts/cursor-plugin-policy.test.ts @@ -11,7 +11,7 @@ const temporaryRoots: string[] = [] function fixture(): string { const target = mkdtempSync(join(tmpdir(), 'cursor-plugin-policy-')) temporaryRoots.push(target) - for (const path of ['skills', '.cursor-plugin', 'assets']) { + for (const path of ['skills', '.cursor-plugin', 'assets', 'packages/cli/package.json']) { cpSync(join(root, path), join(target, path), { recursive: true }) } return target @@ -58,6 +58,18 @@ describe('Cursor marketplace installs from skills/', () => { 'Cursor local MCP must bootstrap the pinned npm CLI without a global Pascal dependency', ) }) + test('rejects a pin that lags the CLI package version', () => { + const target = fixture() + const path = join(target, 'packages/cli/package.json') + writeFileSync( + path, + JSON.stringify({ ...JSON.parse(readFileSync(path, 'utf8')), version: '99.0.0' }), + ) + expect(validateCursorPluginPackage(target)).toContain( + 'Cursor local MCP must bootstrap the pinned npm CLI without a global Pascal dependency', + ) + }) + test('rejects a legacy API-key header on the browser OAuth server', () => { const target = fixture() for (const name of ['.cursor-plugin/mcp.json', 'skills/.cursor-plugin/mcp.json']) { diff --git a/scripts/cursor-plugin-policy.ts b/scripts/cursor-plugin-policy.ts index dd3a0f4e5f..85dec519dd 100644 --- a/scripts/cursor-plugin-policy.ts +++ b/scripts/cursor-plugin-policy.ts @@ -31,10 +31,11 @@ export function validateCursorPluginPackage(root: string): string[] { if (JSON.stringify(config) !== JSON.stringify(rootConfig)) { failures.push('Cursor MCP configs must match at both install roots') } + const cliVersion = read('packages/cli/package.json').version const expectedLocalServer = { type: 'stdio', command: 'npx', - args: ['--yes', '--package=@pascal-app/cli@1.0.0', 'pascal', 'mcp', 'connect'], + args: ['--yes', `--package=@pascal-app/cli@${cliVersion}`, 'pascal', 'mcp', 'connect'], } if ( JSON.stringify((config.mcpServers as Record | undefined)?.pascal) !== diff --git a/scripts/sync-agent-versions.test.ts b/scripts/sync-agent-versions.test.ts new file mode 100644 index 0000000000..7cf1f763da --- /dev/null +++ b/scripts/sync-agent-versions.test.ts @@ -0,0 +1,47 @@ +import { afterEach, describe, expect, test } from 'bun:test' +import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { agentVersionDrift, cliPinFiles } from './sync-agent-versions' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const temporaryRoots: string[] = [] + +function fixture(version: string): string { + const target = mkdtempSync(join(tmpdir(), 'sync-agent-versions-')) + temporaryRoots.push(target) + for (const path of [...cliPinFiles, 'server.json']) { + cpSync(join(root, path), join(target, path), { recursive: true }) + } + for (const pkg of ['cli', 'mcp']) { + const path = join(target, 'packages', pkg, 'package.json') + cpSync(join(root, 'packages', pkg, 'package.json'), path) + writeFileSync(path, JSON.stringify({ ...JSON.parse(readFileSync(path, 'utf8')), version })) + } + return target +} + +afterEach(() => { + for (const path of temporaryRoots.splice(0)) rmSync(path, { recursive: true, force: true }) +}) + +describe('agent integration versions follow the package versions', () => { + test('the repository is in sync', () => { + expect([...agentVersionDrift(root).keys()]).toEqual([]) + }) + + test('a release bump rewrites the CLI pin and the registry manifest', () => { + const drift = agentVersionDrift(fixture('9.8.7')) + expect([...drift.keys()].sort()).toEqual([...cliPinFiles, 'server.json'].sort()) + + const cursor = JSON.parse(drift.get('.cursor-plugin/mcp.json') ?? '{}') + expect(cursor.mcpServers.pascal.args).toContain('--package=@pascal-app/cli@9.8.7') + + const server = JSON.parse(drift.get('server.json') ?? '{}') + expect(server.version).toBe('9.8.7') + expect(server.packages).toEqual([ + expect.objectContaining({ identifier: '@pascal-app/mcp', version: '9.8.7' }), + ]) + }) +}) diff --git a/scripts/sync-agent-versions.ts b/scripts/sync-agent-versions.ts new file mode 100644 index 0000000000..52478fa4ea --- /dev/null +++ b/scripts/sync-agent-versions.ts @@ -0,0 +1,69 @@ +import { readFileSync, writeFileSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +// Agent integrations quote published npm versions. The package.json files are the +// source of truth; this rewrites every quote so a release cannot leave one behind. +export const cliPinFiles = [ + '.cursor-plugin/mcp.json', + 'skills/.cursor-plugin/mcp.json', + 'skills/README.md', + 'skills/pascal-3d/references/setup.md', + 'skills/furniture-fit/references/setup.md', +] as const + +export const mcpPackageName = '@pascal-app/mcp' + +const cliPin = /@pascal-app\/cli@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/g + +function packageVersion(root: string, pkg: string): string { + return JSON.parse(readFileSync(join(root, 'packages', pkg, 'package.json'), 'utf8')).version +} + +function syncServerJson(source: string, version: string): string { + const manifest = JSON.parse(source) + manifest.version = version + for (const entry of manifest.packages ?? []) { + if (entry.identifier === mcpPackageName) entry.version = version + } + return `${JSON.stringify(manifest, null, 2)}\n` +} + +/** Returns each managed file whose content differs from what the package versions require. */ +export function agentVersionDrift(root: string): Map { + const cliVersion = packageVersion(root, 'cli') + const mcpVersion = packageVersion(root, 'mcp') + const expected = new Map() + for (const path of cliPinFiles) { + expected.set( + path, + readFileSync(join(root, path), 'utf8').replace(cliPin, `@pascal-app/cli@${cliVersion}`), + ) + } + expected.set( + 'server.json', + syncServerJson(readFileSync(join(root, 'server.json'), 'utf8'), mcpVersion), + ) + for (const [path, content] of expected) { + if (readFileSync(join(root, path), 'utf8') === content) expected.delete(path) + } + return expected +} + +if (import.meta.main) { + const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') + const drift = agentVersionDrift(root) + if (process.argv.includes('--check')) { + if (drift.size > 0) { + console.error( + `Agent integration versions are out of sync with packages/*/package.json:\n${[...drift.keys()].map((path) => ` ${path}`).join('\n')}\nRun: bun scripts/sync-agent-versions.ts`, + ) + process.exit(1) + } + } else { + for (const [path, content] of drift) { + writeFileSync(join(root, path), content) + console.log(`Updated ${path}`) + } + } +} diff --git a/skills/.cursor-plugin/mcp.json b/skills/.cursor-plugin/mcp.json index 4b5435e8c4..0a82304640 100644 --- a/skills/.cursor-plugin/mcp.json +++ b/skills/.cursor-plugin/mcp.json @@ -3,7 +3,7 @@ "pascal": { "type": "stdio", "command": "npx", - "args": ["--yes", "--package=@pascal-app/cli@1.0.0", "pascal", "mcp", "connect"] + "args": ["--yes", "--package=@pascal-app/cli@1.0.4", "pascal", "mcp", "connect"] }, "pascal-hosted": { "type": "http", diff --git a/skills/furniture-fit/references/setup.md b/skills/furniture-fit/references/setup.md index f28572a015..e62c402e82 100644 --- a/skills/furniture-fit/references/setup.md +++ b/skills/furniture-fit/references/setup.md @@ -81,7 +81,7 @@ The current OpenClaw static-header path stores the expanded key in its private M Cursor: -The Cursor marketplace installs this repository's `skills/` directory. Its `.cursor-plugin/plugin.json` explicitly selects the Cursor MCP configuration, so the Claude-only `${user_config.pascal_api_key}` header is never used by Cursor. The local server runs `npx --yes --package=@pascal-app/cli@1.0.0 pascal mcp connect`; Node.js 22.13 or newer and npm must be available to Cursor. No global `pascal` installation or web-editor runtime is required. The first connection downloads the pinned CLI from npm, and later connections reuse npm's cache. This starts only the local MCP service and keeps existing local project storage. If Cursor reports `spawn npx ENOENT`, install Node.js/npm and fully restart Cursor so it picks up the executable path. For an older installed bundle that still runs `pascal`, update the plugin; `npm install --global @pascal-app/cli@1.0.0` followed by **Customize → MCPs → pascal → Reload** repairs that legacy local command. +The Cursor marketplace installs this repository's `skills/` directory. Its `.cursor-plugin/plugin.json` explicitly selects the Cursor MCP configuration, so the Claude-only `${user_config.pascal_api_key}` header is never used by Cursor. The local server runs `npx --yes --package=@pascal-app/cli@1.0.4 pascal mcp connect`; Node.js 22.13 or newer and npm must be available to Cursor. No global `pascal` installation or web-editor runtime is required. The first connection downloads the pinned CLI from npm, and later connections reuse npm's cache. This starts only the local MCP service and keeps existing local project storage. If Cursor reports `spawn npx ENOENT`, install Node.js/npm and fully restart Cursor so it picks up the executable path. For an older installed bundle that still runs `pascal`, update the plugin; `npm install --global @pascal-app/cli@1.0.4` followed by **Customize → MCPs → pascal → Reload** repairs that legacy local command. The hosted `pascal-hosted` server uses browser sign-in. In **Customize → MCPs**, choose **Authenticate** (or **Connect**) beside `pascal-hosted`. Pascal opens in your browser: sign in or create an account with Google or email, choose the intended workspace, review access, then return to Cursor. No API key or plugin variable is required. Let the user approve the account and workspace shown in consent. Ordinary access covers reading and editing projects; it does not authorize publishing, credit spending, external AI processing, community posting, or account management. Disconnect at `https://editor.pascal.app/settings/connected-apps`. diff --git a/skills/pascal-3d/references/setup.md b/skills/pascal-3d/references/setup.md index e9bc540102..73c51ef2d7 100644 --- a/skills/pascal-3d/references/setup.md +++ b/skills/pascal-3d/references/setup.md @@ -107,7 +107,7 @@ The current OpenClaw static-header path stores the expanded key in its private M Cursor: -The Cursor marketplace installs this repository's `skills/` directory. Its `.cursor-plugin/plugin.json` explicitly selects the Cursor MCP configuration, so the Claude-only `${user_config.pascal_api_key}` header is never used by Cursor. The local server runs `npx --yes --package=@pascal-app/cli@1.0.0 pascal mcp connect`; Node.js 22.13 or newer and npm must be available to Cursor. No global `pascal` installation or web-editor runtime is required. The first connection downloads the pinned CLI from npm, and later connections reuse npm's cache. This starts only the local MCP service and keeps existing local project storage. If Cursor reports `spawn npx ENOENT`, install Node.js/npm and fully restart Cursor so it picks up the executable path. For an older installed bundle that still runs `pascal`, update the plugin; `npm install --global @pascal-app/cli@1.0.0` followed by **Customize → MCPs → pascal → Reload** repairs that legacy local command. +The Cursor marketplace installs this repository's `skills/` directory. Its `.cursor-plugin/plugin.json` explicitly selects the Cursor MCP configuration, so the Claude-only `${user_config.pascal_api_key}` header is never used by Cursor. The local server runs `npx --yes --package=@pascal-app/cli@1.0.4 pascal mcp connect`; Node.js 22.13 or newer and npm must be available to Cursor. No global `pascal` installation or web-editor runtime is required. The first connection downloads the pinned CLI from npm, and later connections reuse npm's cache. This starts only the local MCP service and keeps existing local project storage. If Cursor reports `spawn npx ENOENT`, install Node.js/npm and fully restart Cursor so it picks up the executable path. For an older installed bundle that still runs `pascal`, update the plugin; `npm install --global @pascal-app/cli@1.0.4` followed by **Customize → MCPs → pascal → Reload** repairs that legacy local command. The hosted `pascal-hosted` server uses browser sign-in. In **Customize → MCPs**, choose **Authenticate** (or **Connect**) beside `pascal-hosted`. Pascal opens in your browser: sign in or create an account with Google or email, choose the intended workspace, review access, then return to Cursor. No API key or plugin variable is required. Let the user approve the account and workspace shown in consent. Ordinary access covers reading and editing projects; it does not authorize publishing, credit spending, external AI processing, community posting, or account management. Disconnect at `https://editor.pascal.app/settings/connected-apps`. From 304e2579dede27b66f670f9bbdaf7f83436c6285 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 19 Oct 2026 23:24:40 +0000 Subject: [PATCH 2/5] Name server.json app.pascal/editor with the npm package and OAuth remote Lists @pascal-app/mcp (stdio via npx) next to the hosted Streamable HTTP endpoint, drops the required API-key header since the endpoint advertises OAuth through its 401 challenge, and adds mcpName for npm ownership. --- packages/mcp/package.json | 1 + server.json | 31 ++++++++++++++++++------------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 8c24ad7f76..a3068fdaa9 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -2,6 +2,7 @@ "name": "@pascal-app/mcp", "version": "1.0.4", "description": "Model Context Protocol server for Pascal 3D editor", + "mcpName": "app.pascal/editor", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/server.json b/server.json index 86beefc921..d90cc6afc6 100644 --- a/server.json +++ b/server.json @@ -1,13 +1,14 @@ { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", - "name": "io.github.pascalorg/editor", + "name": "app.pascal/editor", "title": "Pascal 3D Editor", - "description": "Create, inspect, validate, and save editable 3D building scenes with Pascal's hosted MCP server.", - "version": "0.6.1", + "description": "Create, inspect, validate, and save editable 3D building scenes with Pascal's MCP server.", + "version": "1.0.4", "websiteUrl": "https://editor.pascal.app/docs/developers/mcp", "repository": { "url": "https://github.com/pascalorg/editor", - "source": "github" + "source": "github", + "subfolder": "packages/mcp" }, "icons": [ { @@ -25,18 +26,22 @@ ] } ], + "packages": [ + { + "registryType": "npm", + "registryBaseUrl": "https://registry.npmjs.org", + "identifier": "@pascal-app/mcp", + "version": "1.0.4", + "runtimeHint": "npx", + "transport": { + "type": "stdio" + } + } + ], "remotes": [ { "type": "streamable-http", - "url": "https://editor.pascal.app/api/mcp", - "headers": [ - { - "name": "Authorization", - "description": "Bearer API key created in Pascal Settings, formatted as Bearer sk_live_...", - "isRequired": true, - "isSecret": true - } - ] + "url": "https://editor.pascal.app/api/mcp" } ] } From 495a7f0fb77e613c8faaf80c5eaba02dd617b76b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 19 Oct 2026 23:24:40 +0000 Subject: [PATCH 3/5] Sync and validate server.json in release; gate hosted drift before publish The release bump runs sync-agent-versions.ts and mcp-publisher validate. The registry workflow checks server.json against packages/mcp/package.json on PRs and moves the live API catalog and server card checks to a manual pre-publish job. --- .github/workflows/mcp-registry.yml | 63 +++++++++++++++++++++++++----- .github/workflows/release.yml | 17 ++++++++ 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/.github/workflows/mcp-registry.yml b/.github/workflows/mcp-registry.yml index 9a813b6a88..8ca3bed372 100644 --- a/.github/workflows/mcp-registry.yml +++ b/.github/workflows/mcp-registry.yml @@ -4,12 +4,16 @@ on: pull_request: paths: - server.json + - packages/mcp/package.json - .github/workflows/mcp-registry.yml push: branches: [main] paths: - server.json + - packages/mcp/package.json - .github/workflows/mcp-registry.yml + # Run before publishing: the hosted deployment must already serve this version. + workflow_dispatch: env: MCP_PUBLISHER_VERSION: 1.8.1 @@ -36,30 +40,56 @@ jobs: - name: Validate MCP Registry manifest run: "$RUNNER_TEMP/mcp-publisher validate" + # The registry verifies npm ownership through the published package's mcpName. + - name: Manifest follows the @pascal-app/mcp package + run: | + jq --exit-status --slurpfile pkg packages/mcp/package.json ' + .version == $pkg[0].version + and .name == $pkg[0].mcpName + and .packages == [{ + "registryType": "npm", + "registryBaseUrl": "https://registry.npmjs.org", + "identifier": $pkg[0].name, + "version": $pkg[0].version, + "runtimeHint": "npx", + "transport": { "type": "stdio" } + }] + ' server.json + + # OAuth is discovered from the 401 challenge, so the remote declares no headers. - name: Verify hosted endpoint contract run: | jq --exit-status ' - (.version | strings | test("^[0-9]+\\.[0-9]+\\.[0-9]+$")) - and .remotes == [{ + .remotes == [{ "type": "streamable-http", - "url": "https://editor.pascal.app/api/mcp", - "headers": [{ - "name": "Authorization", - "description": "Bearer API key created in Pascal Settings, formatted as Bearer sk_live_...", - "isRequired": true, - "isSecret": true - }] + "url": "https://editor.pascal.app/api/mcp" }] ' server.json curl --fail --location --output /dev/null \ https://editor.pascal.app/docs/developers/mcp status=$(curl --silent --show-error --output "$RUNNER_TEMP/mcp-response.json" \ + --dump-header "$RUNNER_TEMP/mcp-response.headers" \ --write-out '%{http_code}' \ --request POST \ --header 'content-type: application/json' \ --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"registry-check","version":"1"}}}' \ https://editor.pascal.app/api/mcp) test "$status" = "401" + grep --ignore-case --fixed-strings \ + 'resource_metadata="https://editor.pascal.app/.well-known/oauth-protected-resource"' \ + "$RUNNER_TEMP/mcp-response.headers" + + # The hosted API catalog and server card are served by the hosted deployment, not + # this repository, so they only match after that deployment picks up this version. + hosted-matches-manifest: + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false - name: Verify live API catalog matches manifest run: | @@ -103,3 +133,18 @@ jobs: | tr -d '\r' \ | grep --fixed-strings --ignore-case \ 'link: ; rel="api-catalog"' + + - name: Verify live server card matches manifest + run: | + curl --fail \ + --connect-timeout 10 \ + --max-time 30 \ + --retry 3 \ + --retry-connrefused \ + --retry-delay 2 \ + --output "$RUNNER_TEMP/server-card.json" \ + https://editor.pascal.app/.well-known/mcp/server-card.json + jq --exit-status --slurpfile manifest server.json ' + .serverInfo.version == $manifest[0].version + and .transport.endpoint == $manifest[0].remotes[0].url + ' "$RUNNER_TEMP/server-card.json" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 54dfa3f172..7952250746 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -205,6 +205,23 @@ jobs: # Refresh the lockfile so the release commit remains reproducible. bun install + # The Cursor plugin's CLI pin and server.json quote these versions. + bun scripts/sync-agent-versions.ts + + # Publishing server.json to the MCP Registry stays a manual owner step after + # this release lands; see wiki/npm-release.md. + - name: Validate MCP Registry manifest + env: + MCP_PUBLISHER_VERSION: 1.8.1 + MCP_PUBLISHER_LINUX_AMD64_SHA256: a06c9096dcb9727c13555b6be26c7effa707b01f06a4c561ba7a3635443cf2cc + run: | + curl --fail --location \ + "https://github.com/modelcontextprotocol/registry/releases/download/v${MCP_PUBLISHER_VERSION}/mcp-publisher_linux_amd64.tar.gz" \ + --output "$RUNNER_TEMP/mcp-publisher.tar.gz" + echo "${MCP_PUBLISHER_LINUX_AMD64_SHA256} $RUNNER_TEMP/mcp-publisher.tar.gz" | sha256sum --check + tar -xzf "$RUNNER_TEMP/mcp-publisher.tar.gz" -C "$RUNNER_TEMP" mcp-publisher + "$RUNNER_TEMP/mcp-publisher" validate + - id: version run: | echo "version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT" From 122d462a4cd45b9e00119d6bc37bbc9a2a20a15b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 19 Oct 2026 23:24:40 +0000 Subject: [PATCH 4/5] Document the app.pascal/editor MCP Registry publish steps --- README.md | 10 ++++++---- skills/README.md | 4 ++-- wiki/npm-release.md | 11 ++++++++++- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 35d8b3a7ef..0a92abbfdf 100644 --- a/README.md +++ b/README.md @@ -127,10 +127,12 @@ These workflows require a connected Pascal MCP server for their tool-backed acti ### MCP Registry -[`server.json`](server.json) is Pascal's manifest for the official MCP Registry. Its -version tracks the hosted MCP implementation independently of the npm package version. -Pull requests validate the manifest and production endpoint. A Pascal organization -owner publishes an approved version from `main` with the official registry publisher. +[`server.json`](server.json) is Pascal's manifest for the official MCP Registry, named +`app.pascal/editor`. It lists the `@pascal-app/mcp` npm package and the hosted Streamable HTTP +endpoint, which signs in with OAuth. Its version is the `@pascal-app/mcp` package version: +`bun scripts/sync-agent-versions.ts` rewrites it, and the Cursor plugin's CLI pin, from the +package versions, and CI fails when they drift. A Pascal owner publishes from `main` after a +release; see [wiki/npm-release.md](wiki/npm-release.md#mcp-registry). ## Using Published Packages diff --git a/skills/README.md b/skills/README.md index 2ec735f4ff..69294ba683 100644 --- a/skills/README.md +++ b/skills/README.md @@ -16,7 +16,7 @@ marketplace install may continue to use the older bundle until Cursor publishes | Claude Code plugin | Installable from this Git marketplace; not submitted to the Anthropic plugin directory. | | Codex and Cursor Agent Plugin | Installable from this repository, including the root [`mcp.json`](../mcp.json) server; a Cursor-native [`.cursor-plugin/plugin.json`](../.cursor-plugin/plugin.json) carries the marketplace logo and category and uses browser sign-in for hosted access; [listed in the Cursor marketplace](https://cursor.com/marketplace/pascal). The corrected Cursor bundle below still needs marketplace refresh; OpenAI submission remains separate. | | Gemini CLI extension | Root [`gemini-extension.json`](../gemini-extension.json) is present; installable from a release tag that carries it, and gallery listing waits on the `gemini-cli-extension` repository topic. | -| Official MCP Registry | `io.github.pascalorg/editor` 0.6.1 is published. | +| Official MCP Registry | `io.github.pascalorg/editor` 0.6.1 is published. [`server.json`](../server.json) now names `app.pascal/editor` with the npm package and hosted remote; not published yet. | | ClawHub and OpenClaw | Not published; waiting on an authorized publisher accepting the MIT-0 terms. | ## Install with skills.sh @@ -88,7 +88,7 @@ Use one active agent client per local CLI service. Its standalone HTTP runtime s ## Install in Cursor -The Cursor marketplace installs this repository's `skills/` directory. Its `.cursor-plugin/plugin.json` explicitly selects the Cursor MCP configuration, so the Claude-only `${user_config.pascal_api_key}` header is never used by Cursor. The local server runs `npx --yes --package=@pascal-app/cli@1.0.0 pascal mcp connect`; Node.js 22.13 or newer and npm must be available to Cursor. No global `pascal` installation or web-editor runtime is required. The first connection downloads the pinned CLI from npm, and later connections reuse npm's cache. This starts only the local MCP service and keeps existing local project storage. If Cursor reports `spawn npx ENOENT`, install Node.js/npm and fully restart Cursor so it picks up the executable path. For an older installed bundle that still runs `pascal`, update the plugin; `npm install --global @pascal-app/cli@1.0.0` followed by **Customize → MCPs → pascal → Reload** repairs that legacy local command. +The Cursor marketplace installs this repository's `skills/` directory. Its `.cursor-plugin/plugin.json` explicitly selects the Cursor MCP configuration, so the Claude-only `${user_config.pascal_api_key}` header is never used by Cursor. The local server runs `npx --yes --package=@pascal-app/cli@1.0.4 pascal mcp connect`; Node.js 22.13 or newer and npm must be available to Cursor. No global `pascal` installation or web-editor runtime is required. The first connection downloads the pinned CLI from npm, and later connections reuse npm's cache. This starts only the local MCP service and keeps existing local project storage. If Cursor reports `spawn npx ENOENT`, install Node.js/npm and fully restart Cursor so it picks up the executable path. For an older installed bundle that still runs `pascal`, update the plugin; `npm install --global @pascal-app/cli@1.0.4` followed by **Customize → MCPs → pascal → Reload** repairs that legacy local command. The hosted `pascal-hosted` server uses browser sign-in. In **Customize → MCPs**, choose **Authenticate** (or **Connect**) beside `pascal-hosted`. Pascal opens in your browser: sign in or create an account with Google or email, choose the intended workspace, review access, then return to Cursor. No API key or plugin variable is required. Let the user approve the account and workspace shown in consent. Ordinary access covers reading and editing projects; it does not authorize publishing, credit spending, external AI processing, community posting, or account management. Disconnect at `https://editor.pascal.app/settings/connected-apps`. diff --git a/wiki/npm-release.md b/wiki/npm-release.md index b3c9bbccbd..21f6144b63 100644 --- a/wiki/npm-release.md +++ b/wiki/npm-release.md @@ -44,6 +44,15 @@ The same `release.yml` run publishes `ghcr.io/pascalorg/pascal-editor` after the - A repository that pins this one as a git submodule bumps the pin to the release commit and runs `bun install`: its root lockfile records the workspace versions, so it changes with the pin. - Plugins that declare `@pascal-app/*` peer ranges must widen them when a release leaves the range; `bun install` reports unmet peers until they do. -- Update anything that quotes a published CLI version, such as the `--package=@pascal-app/cli@` pin in `.cursor-plugin/mcp.json` (checked by `scripts/cursor-plugin-policy.ts`). [`server.json`](../server.json) versions the hosted MCP server, not the npm packages, and does not change. +- The release commit already carries the Cursor plugin's `--package=@pascal-app/cli@` pin and the [`server.json`](../server.json) version: the workflow runs `bun scripts/sync-agent-versions.ts` after the bump, and `bun run skills:validate` fails when either drifts from `packages/*/package.json`. A manual release runs the same script before committing. + +## MCP Registry + +[`server.json`](../server.json) publishes as `app.pascal/editor` with the `@pascal-app/mcp` npm package and the hosted remote. Nothing publishes it automatically. After a release lands on `main`: + +1. The registry checks npm ownership by reading `mcpName` from the published `@pascal-app/mcp@`, so publish only versions whose npm tarball carries `"mcpName": "app.pascal/editor"`. +2. Deploy the hosted editor at the same version, then run the **MCP Registry** workflow by hand (`gh workflow run mcp-registry.yml`). Its `hosted-matches-manifest` job checks that the live API catalog and `/.well-known/mcp/server-card.json` report this version and registry name. +3. Authenticate for the `app.pascal` namespace once per session: `mcp-publisher login dns --domain pascal.app --private-key `, which needs the `pascal.app` TXT record `v=MCPv1; k=ed25519; p=`. HTTP verification (`mcp-publisher login http`, serving `https://pascal.app/.well-known/mcp-registry-auth`) is the alternative. +4. From the repository root, `mcp-publisher publish`, then confirm with `curl "https://registry.modelcontextprotocol.io/v0/servers?search=app.pascal/editor"`. GitHub release notes follow the house style: emoji sections, every line linked to its PR, a 📦 package table, individual contributor attributions, and the compare link last. Verify the table against npm before publishing — an earlier release claimed versions that were never published. From a6ab970946d8f93a41b05d4d4ccf9c85ad7c88a4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 04:06:45 +0000 Subject: [PATCH 5/5] Keep agent integrations on the last stable version during a beta A beta release commits to main, so syncing would pin Cursor marketplace installs and server.json to a prerelease. stablePackageVersion skips the sync for prerelease trees; the Cursor policy and the registry workflow accept the last stable pin while the packages are on a beta. --- .github/workflows/mcp-registry.yml | 8 ++++--- .github/workflows/release.yml | 3 ++- scripts/cursor-plugin-policy.test.ts | 23 +++++++++++++++++++ scripts/cursor-plugin-policy.ts | 18 +++++++++++---- scripts/sync-agent-versions.test.ts | 4 ++++ scripts/sync-agent-versions.ts | 33 ++++++++++++++++++---------- wiki/npm-release.md | 2 +- 7 files changed, 71 insertions(+), 20 deletions(-) diff --git a/.github/workflows/mcp-registry.yml b/.github/workflows/mcp-registry.yml index 8ca3bed372..03d68d9109 100644 --- a/.github/workflows/mcp-registry.yml +++ b/.github/workflows/mcp-registry.yml @@ -40,17 +40,19 @@ jobs: - name: Validate MCP Registry manifest run: "$RUNNER_TEMP/mcp-publisher validate" - # The registry verifies npm ownership through the published package's mcpName. + # The registry verifies npm ownership through the published package's mcpName. A + # prerelease tree keeps the manifest on the last stable version. - name: Manifest follows the @pascal-app/mcp package run: | jq --exit-status --slurpfile pkg packages/mcp/package.json ' - .version == $pkg[0].version + (.version | test("^[0-9]+\\.[0-9]+\\.[0-9]+$")) + and (($pkg[0].version | contains("-")) or .version == $pkg[0].version) and .name == $pkg[0].mcpName and .packages == [{ "registryType": "npm", "registryBaseUrl": "https://registry.npmjs.org", "identifier": $pkg[0].name, - "version": $pkg[0].version, + "version": .version, "runtimeHint": "npx", "transport": { "type": "stdio" } }] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7952250746..5b280e8f8f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -205,7 +205,8 @@ jobs: # Refresh the lockfile so the release commit remains reproducible. bun install - # The Cursor plugin's CLI pin and server.json quote these versions. + # The Cursor plugin's CLI pin and server.json quote these versions; a beta + # leaves them on the last stable release. bun scripts/sync-agent-versions.ts # Publishing server.json to the MCP Registry stays a manual owner step after diff --git a/scripts/cursor-plugin-policy.test.ts b/scripts/cursor-plugin-policy.test.ts index b8fe37b2cc..76c6a81092 100644 --- a/scripts/cursor-plugin-policy.test.ts +++ b/scripts/cursor-plugin-policy.test.ts @@ -70,6 +70,29 @@ describe('Cursor marketplace installs from skills/', () => { ) }) + test('keeps the stable pin while the CLI is on a beta', () => { + const target = fixture() + const path = join(target, 'packages/cli/package.json') + writeFileSync( + path, + JSON.stringify({ ...JSON.parse(readFileSync(path, 'utf8')), version: '99.0.0-beta.1' }), + ) + expect(validateCursorPluginPackage(target)).toEqual([]) + }) + + test('rejects a prerelease pin', () => { + const target = fixture() + for (const name of ['.cursor-plugin/mcp.json', 'skills/.cursor-plugin/mcp.json']) { + const path = join(target, name) + const config = JSON.parse(readFileSync(path, 'utf8')) + config.mcpServers.pascal.args[1] = '--package=@pascal-app/cli@99.0.0-beta.1' + writeFileSync(path, JSON.stringify(config)) + } + expect(validateCursorPluginPackage(target)).toContain( + 'Cursor local MCP must bootstrap the pinned npm CLI without a global Pascal dependency', + ) + }) + test('rejects a legacy API-key header on the browser OAuth server', () => { const target = fixture() for (const name of ['.cursor-plugin/mcp.json', 'skills/.cursor-plugin/mcp.json']) { diff --git a/scripts/cursor-plugin-policy.ts b/scripts/cursor-plugin-policy.ts index 85dec519dd..bd8c331d45 100644 --- a/scripts/cursor-plugin-policy.ts +++ b/scripts/cursor-plugin-policy.ts @@ -1,5 +1,6 @@ import { existsSync, readFileSync } from 'node:fs' import { join } from 'node:path' +import { stablePackageVersion } from './sync-agent-versions' export function validateCursorPluginPackage(root: string): string[] { const failures: string[] = [] @@ -31,15 +32,24 @@ export function validateCursorPluginPackage(root: string): string[] { if (JSON.stringify(config) !== JSON.stringify(rootConfig)) { failures.push('Cursor MCP configs must match at both install roots') } - const cliVersion = read('packages/cli/package.json').version + const hasCliPackage = existsSync(join(root, 'packages/cli/package.json')) + if (!hasCliPackage) failures.push('Cursor CLI pin needs packages/cli/package.json') + const cliVersion = hasCliPackage ? stablePackageVersion(root, 'cli') : undefined + const localServer = (config.mcpServers as Record | undefined)?.pascal as + | { args?: unknown[] } + | undefined + const pin = localServer?.args?.[1] + const pinnedVersion = + typeof pin === 'string' ? /^--package=@pascal-app\/cli@(\d+\.\d+\.\d+)$/.exec(pin)?.[1] : null const expectedLocalServer = { type: 'stdio', command: 'npx', - args: ['--yes', `--package=@pascal-app/cli@${cliVersion}`, 'pascal', 'mcp', 'connect'], + args: ['--yes', `--package=@pascal-app/cli@${pinnedVersion}`, 'pascal', 'mcp', 'connect'], } if ( - JSON.stringify((config.mcpServers as Record | undefined)?.pascal) !== - JSON.stringify(expectedLocalServer) + !pinnedVersion || + (cliVersion !== undefined && pinnedVersion !== cliVersion) || + JSON.stringify(localServer) !== JSON.stringify(expectedLocalServer) ) { failures.push( 'Cursor local MCP must bootstrap the pinned npm CLI without a global Pascal dependency', diff --git a/scripts/sync-agent-versions.test.ts b/scripts/sync-agent-versions.test.ts index 7cf1f763da..b0e02c6f5d 100644 --- a/scripts/sync-agent-versions.test.ts +++ b/scripts/sync-agent-versions.test.ts @@ -44,4 +44,8 @@ describe('agent integration versions follow the package versions', () => { expect.objectContaining({ identifier: '@pascal-app/mcp', version: '9.8.7' }), ]) }) + + test('a beta release keeps every integration on the last stable version', () => { + expect([...agentVersionDrift(fixture('9.8.7-beta.1')).keys()]).toEqual([]) + }) }) diff --git a/scripts/sync-agent-versions.ts b/scripts/sync-agent-versions.ts index 52478fa4ea..fb6ab7097d 100644 --- a/scripts/sync-agent-versions.ts +++ b/scripts/sync-agent-versions.ts @@ -16,8 +16,15 @@ export const mcpPackageName = '@pascal-app/mcp' const cliPin = /@pascal-app\/cli@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/g -function packageVersion(root: string, pkg: string): string { - return JSON.parse(readFileSync(join(root, 'packages', pkg, 'package.json'), 'utf8')).version +/** + * The version integrations should quote for a package, or undefined while the tree is on a + * prerelease. Prereleases publish on npm's `beta` tag, so integrations keep the last stable one. + */ +export function stablePackageVersion(root: string, pkg: string): string | undefined { + const { version } = JSON.parse( + readFileSync(join(root, 'packages', pkg, 'package.json'), 'utf8'), + ) as { version: string } + return version.includes('-') ? undefined : version } function syncServerJson(source: string, version: string): string { @@ -31,19 +38,23 @@ function syncServerJson(source: string, version: string): string { /** Returns each managed file whose content differs from what the package versions require. */ export function agentVersionDrift(root: string): Map { - const cliVersion = packageVersion(root, 'cli') - const mcpVersion = packageVersion(root, 'mcp') + const cliVersion = stablePackageVersion(root, 'cli') + const mcpVersion = stablePackageVersion(root, 'mcp') const expected = new Map() - for (const path of cliPinFiles) { + if (cliVersion) { + for (const path of cliPinFiles) { + expected.set( + path, + readFileSync(join(root, path), 'utf8').replace(cliPin, `@pascal-app/cli@${cliVersion}`), + ) + } + } + if (mcpVersion) { expected.set( - path, - readFileSync(join(root, path), 'utf8').replace(cliPin, `@pascal-app/cli@${cliVersion}`), + 'server.json', + syncServerJson(readFileSync(join(root, 'server.json'), 'utf8'), mcpVersion), ) } - expected.set( - 'server.json', - syncServerJson(readFileSync(join(root, 'server.json'), 'utf8'), mcpVersion), - ) for (const [path, content] of expected) { if (readFileSync(join(root, path), 'utf8') === content) expected.delete(path) } diff --git a/wiki/npm-release.md b/wiki/npm-release.md index 21f6144b63..adc2f39700 100644 --- a/wiki/npm-release.md +++ b/wiki/npm-release.md @@ -44,7 +44,7 @@ The same `release.yml` run publishes `ghcr.io/pascalorg/pascal-editor` after the - A repository that pins this one as a git submodule bumps the pin to the release commit and runs `bun install`: its root lockfile records the workspace versions, so it changes with the pin. - Plugins that declare `@pascal-app/*` peer ranges must widen them when a release leaves the range; `bun install` reports unmet peers until they do. -- The release commit already carries the Cursor plugin's `--package=@pascal-app/cli@` pin and the [`server.json`](../server.json) version: the workflow runs `bun scripts/sync-agent-versions.ts` after the bump, and `bun run skills:validate` fails when either drifts from `packages/*/package.json`. A manual release runs the same script before committing. +- The release commit already carries the Cursor plugin's `--package=@pascal-app/cli@` pin and the [`server.json`](../server.json) version: the workflow runs `bun scripts/sync-agent-versions.ts` after the bump, and `bun run skills:validate` fails when either drifts from `packages/*/package.json`. A beta leaves both on the last stable release, so marketplace installs never pin a prerelease. A manual release runs the same script before committing. ## MCP Registry