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/.github/workflows/mcp-registry.yml b/.github/workflows/mcp-registry.yml index 9a813b6a88..03d68d9109 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,58 @@ jobs: - name: Validate MCP Registry manifest run: "$RUNNER_TEMP/mcp-publisher validate" + # 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 | 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": .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 +135,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..5b280e8f8f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -205,6 +205,24 @@ jobs: # Refresh the lockfile so the release commit remains reproducible. bun install + # 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 + # 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" 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/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/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/scripts/cursor-plugin-policy.test.ts b/scripts/cursor-plugin-policy.test.ts index 5e01859e89..76c6a81092 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,41 @@ 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('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 dd3a0f4e5f..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,14 +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 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@1.0.0', '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 new file mode 100644 index 0000000000..b0e02c6f5d --- /dev/null +++ b/scripts/sync-agent-versions.test.ts @@ -0,0 +1,51 @@ +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' }), + ]) + }) + + 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 new file mode 100644 index 0000000000..fb6ab7097d --- /dev/null +++ b/scripts/sync-agent-versions.ts @@ -0,0 +1,80 @@ +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 + +/** + * 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 { + 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 = stablePackageVersion(root, 'cli') + const mcpVersion = stablePackageVersion(root, 'mcp') + const expected = new Map() + 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( + '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/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" } ] } 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/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/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`. diff --git a/wiki/npm-release.md b/wiki/npm-release.md index b3c9bbccbd..adc2f39700 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 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 + +[`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.