diff --git a/CHANGELOG.md b/CHANGELOG.md index d76ede4b04..bda8ab43ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - **Public agent skills** — `pascal-3d` and `furniture-fit` teach MCP-capable agents to build, inspect, validate, and hand off scenes, and to report measured furniture footprints with evidence-scoped conclusions, fail-closed input gates, blocker-aware next actions, and an optional no-sign-in footprint pre-check link ([#777](https://github.com/pascalorg/editor/pull/777), [#781](https://github.com/pascalorg/editor/pull/781), [#791](https://github.com/pascalorg/editor/pull/791), [#794](https://github.com/pascalorg/editor/pull/794), [#824](https://github.com/pascalorg/editor/pull/824)) - **Plugin bundles 0.1.3 → 0.1.8** — the same canonical `skills/` source ships as one versioned plugin with a recorded validation ledger per bundle ([#782](https://github.com/pascalorg/editor/pull/782), [#795](https://github.com/pascalorg/editor/pull/795), [#802](https://github.com/pascalorg/editor/pull/802), [#811](https://github.com/pascalorg/editor/pull/811), [#825](https://github.com/pascalorg/editor/pull/825)) - **Claude Code and Codex plugin marketplaces** — this repository is installable as `pascal-agent-skills@pascal`, with a credential-free local `pascal mcp connect` server bundled for Claude Code ([#777](https://github.com/pascalorg/editor/pull/777), [#810](https://github.com/pascalorg/editor/pull/810)) +- **Hosted MCP server in the Claude Code plugin** — `pascal-agent-skills@pascal` now also bundles a `pascal-hosted` Streamable HTTP server for `https://editor.pascal.app/api/mcp`, authenticated with an optional Pascal API key collected as sensitive plugin user configuration and stored in the OS keychain rather than any file ([#835](https://github.com/pascalorg/editor/pull/835)) - **OpenAI portable plugin manifest** — root `plugin.json` carries the Agent Plugins schema, listing metadata, branding assets, a With MCP review packet, and per-tool annotation justifications for all 46 MCP tools ([#796](https://github.com/pascalorg/editor/pull/796), [#797](https://github.com/pascalorg/editor/pull/797), [#799](https://github.com/pascalorg/editor/pull/799), [#812](https://github.com/pascalorg/editor/pull/812), [#813](https://github.com/pascalorg/editor/pull/813)) - **ClawHub publication readiness** — scoped `.clawhubignore` policies with regression tests that reject re-inclusion and legacy-override rules ([#798](https://github.com/pascalorg/editor/pull/798), [#806](https://github.com/pascalorg/editor/pull/806)) - **Official MCP Registry entry** — `io.github.pascalorg/editor` 0.6.1 publishes the hosted Streamable HTTP endpoint, with CI validating the manifest against the live API catalog ([#808](https://github.com/pascalorg/editor/pull/808), [#809](https://github.com/pascalorg/editor/pull/809)) diff --git a/README.md b/README.md index f875e63250..b221f66284 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,8 @@ Claude Code users can install the same canonical skill source as a plugin: The Claude plugin also supplies the local `pascal mcp connect` server. Install and start the Pascal CLI first, and keep `pascal` on the `PATH` used to launch Claude Code. This local connector needs no Pascal account or API key and does not upload projects automatically. Its plugin root is this repository's `skills/` directory, so an install copies only the skill bundles and their plugin metadata rather than the repository. +The plugin bundles two servers: the local `pascal` connector above and a hosted `pascal-hosted` server for `https://editor.pascal.app/api/mcp`, which prompts for an optional Pascal API key at enable time and stores it in the OS keychain. Leave the key empty to run local-only. + Claude Code 2.1.258 loads both the user-scoped `pascal` server created by `pascal mcp setup claude` and the plugin-provided server. Remove the manual entry before reloading or restarting Claude Code so only the plugin owns the connection lifecycle: ```bash diff --git a/scripts/claude-mcp-config-policy.test.ts b/scripts/claude-mcp-config-policy.test.ts index b5d1468da0..42b5961386 100644 --- a/scripts/claude-mcp-config-policy.test.ts +++ b/scripts/claude-mcp-config-policy.test.ts @@ -1,7 +1,11 @@ import { describe, expect, test } from 'bun:test' import { readFileSync } from 'node:fs' import { join, resolve } from 'node:path' -import { validateClaudeMcpPolicy } from './claude-mcp-config-policy' +import { + hostedAuthorizationHeader, + hostedMcpUrl, + validateClaudeMcpPolicy, +} from './claude-mcp-config-policy' const repositoryRoot = resolve(import.meta.dir, '..') const canonicalConfig = JSON.parse( @@ -21,45 +25,60 @@ const upgradeGuidancePaths = [ 'skills/pascal-3d/references/setup.md', 'skills/furniture-fit/references/setup.md', ] as const +const hostedGuidancePaths = [ + 'skills/pascal-3d/references/setup.md', + 'skills/furniture-fit/references/setup.md', +] as const + +const localServer = { type: 'stdio', command: 'pascal', args: ['mcp', 'connect'] } +const hostedServer = { + type: 'http', + url: hostedMcpUrl, + headers: { Authorization: hostedAuthorizationHeader }, +} + +function withServers(servers: Record): Record { + return { mcpServers: servers } +} + +const canonicalKeyOption = (canonicalPlugin.userConfig as Record) + .pascal_api_key as Record + +function pluginWithUserConfig(userConfig: unknown): Record { + return { ...canonicalPlugin, userConfig } +} describe('Claude plugin MCP configuration', () => { - test('uses the protected local connector configuration', () => { + test('accepts the local connector beside the hosted endpoint', () => { expect( validateClaudeMcpPolicy(canonicalConfig, canonicalPlugin, canonicalMarketplaceEntry), ).toEqual([]) }) test.each([ - ['another server', { ...canonicalConfig, mcpServers: { pascal: {}, other: {} } }], + ['a third server', withServers({ pascal: localServer, 'pascal-hosted': hostedServer, o: {} })], + ['a missing hosted server', withServers({ pascal: localServer })], + ['a missing local server', withServers({ 'pascal-hosted': hostedServer })], [ - 'a remote URL', - { mcpServers: { pascal: { type: 'http', url: 'https://editor.pascal.app/api/mcp' } } }, + 'a remote URL on the local server', + withServers({ + pascal: { type: 'http', url: 'https://editor.pascal.app/api/mcp' }, + 'pascal-hosted': hostedServer, + }), ], [ - 'request headers', - { - mcpServers: { - pascal: { - type: 'stdio', - command: 'pascal', - args: ['mcp', 'connect'], - headers: { Authorization: 'Bearer placeholder' }, - }, - }, - }, + 'request headers on the local server', + withServers({ + pascal: { ...localServer, headers: { Authorization: 'Bearer placeholder' } }, + 'pascal-hosted': hostedServer, + }), ], [ - 'environment credentials', - { - mcpServers: { - pascal: { - type: 'stdio', - command: 'pascal', - args: ['mcp', 'connect'], - env: { PASCAL_API_KEY: 'placeholder' }, - }, - }, - }, + 'environment credentials on the local server', + withServers({ + pascal: { ...localServer, env: { PASCAL_API_KEY: 'placeholder' } }, + 'pascal-hosted': hostedServer, + }), ], ])('rejects %s', (_label, config) => { expect( @@ -67,14 +86,35 @@ describe('Claude plugin MCP configuration', () => { ).toBeGreaterThan(0) }) + test.each([ + [ + 'a literal hosted credential', + { ...hostedServer, headers: { Authorization: 'Bearer pascal_live_placeholder' } }, + ], + [ + 'an unexpected hosted header', + { ...hostedServer, headers: { ...hostedServer.headers, 'X-Pascal-Org': 'acme' } }, + ], + ['a redirected hosted URL', { ...hostedServer, url: 'https://mcp.example.com/api/mcp' }], + ['a non-HTTP hosted transport', { ...hostedServer, type: 'sse' }], + ['a hosted download helper', { ...hostedServer, headersHelper: './fetch-headers.sh' }], + ])('rejects hosted %s', (_label, server) => { + expect( + validateClaudeMcpPolicy( + withServers({ pascal: localServer, 'pascal-hosted': server }), + canonicalPlugin, + canonicalMarketplaceEntry, + ).length, + ).toBeGreaterThan(0) + }) + test('rejects command or argument changes', () => { expect( validateClaudeMcpPolicy( - { - mcpServers: { - pascal: { type: 'stdio', command: 'npx', args: ['pascal', 'mcp', 'connect'] }, - }, - }, + withServers({ + pascal: { type: 'stdio', command: 'npx', args: ['pascal', 'mcp', 'connect'] }, + 'pascal-hosted': hostedServer, + }), canonicalPlugin, canonicalMarketplaceEntry, ), @@ -86,7 +126,26 @@ describe('Claude plugin MCP configuration', () => { test.each([ ['inline MCP servers', { ...canonicalPlugin, mcpServers: { remote: {} } }], - ['user configuration', { ...canonicalPlugin, userConfig: { apiKey: { sensitive: true } } }], + ['a missing hosted key option', pluginWithUserConfig(undefined)], + [ + 'an extra user configuration option', + pluginWithUserConfig({ + pascal_api_key: canonicalKeyOption, + pascal_password: { type: 'string', sensitive: true, required: false }, + }), + ], + [ + 'a plaintext hosted key option', + pluginWithUserConfig({ pascal_api_key: { ...canonicalKeyOption, sensitive: false } }), + ], + [ + 'a required hosted key option', + pluginWithUserConfig({ pascal_api_key: { ...canonicalKeyOption, required: true } }), + ], + [ + 'a default hosted credential', + pluginWithUserConfig({ pascal_api_key: { ...canonicalKeyOption, default: 'placeholder' } }), + ], ])('rejects plugin-manifest %s', (_label, pluginManifest) => { expect( validateClaudeMcpPolicy(canonicalConfig, pluginManifest, canonicalMarketplaceEntry).length, @@ -117,4 +176,13 @@ describe('Claude plugin MCP upgrade guidance', () => { expect(content).toContain('before reloading or restarting Claude Code') expect(content).toContain('one-active-agent-client-per-local-service requirement') }) + + test.each(hostedGuidancePaths)('%s documents the plugin hosted key path', (path) => { + const content = readFileSync(join(repositoryRoot, path), 'utf8') + expect(content).toContain('pascal-hosted') + expect(content).toContain( + 'claude plugin install pascal-agent-skills@pascal --config pascal_api_key=', + ) + expect(content).toContain('keychain') + }) }) diff --git a/scripts/claude-mcp-config-policy.ts b/scripts/claude-mcp-config-policy.ts index 57609a1453..d4044580c3 100644 --- a/scripts/claude-mcp-config-policy.ts +++ b/scripts/claude-mcp-config-policy.ts @@ -11,6 +11,88 @@ function hasExactKeys(value: Record, expected: readonly string[ ) } +export const hostedMcpUrl = 'https://editor.pascal.app/api/mcp' +export const hostedApiKeyOption = 'pascal_api_key' +export const hostedAuthorizationHeader = `Bearer \${user_config.${hostedApiKeyOption}}` + +function validateLocalServer(server: unknown, failures: string[]): void { + if (!isRecord(server) || !hasExactKeys(server, ['type', 'command', 'args'])) { + failures.push( + 'skills/.mcp.json pascal server must contain only type, command, and args; remote or credential fields are not allowed', + ) + return + } + + if (server.type !== 'stdio') failures.push('skills/.mcp.json pascal server type must be stdio') + if (server.command !== 'pascal') + failures.push('skills/.mcp.json pascal server command must be pascal') + if ( + !Array.isArray(server.args) || + server.args.length !== 2 || + server.args[0] !== 'mcp' || + server.args[1] !== 'connect' + ) { + failures.push('skills/.mcp.json pascal server args must be exactly ["mcp", "connect"]') + } +} + +function validateHostedServer(server: unknown, failures: string[]): void { + if (!isRecord(server) || !hasExactKeys(server, ['type', 'url', 'headers'])) { + failures.push('skills/.mcp.json pascal-hosted server must contain only type, url, and headers') + return + } + + if (server.type !== 'http') + failures.push('skills/.mcp.json pascal-hosted server type must be http') + if (server.url !== hostedMcpUrl) + failures.push(`skills/.mcp.json pascal-hosted server url must be ${hostedMcpUrl}`) + + const headers = server.headers + if (!isRecord(headers) || !hasExactKeys(headers, ['Authorization'])) { + failures.push('skills/.mcp.json pascal-hosted server must send only an Authorization header') + return + } + // The header must stay a ${user_config.*} reference. A literal token here would publish a + // credential in the installed plugin source instead of resolving it from the host's secret store. + if (headers.Authorization !== hostedAuthorizationHeader) { + failures.push( + `skills/.mcp.json pascal-hosted Authorization header must be exactly "${hostedAuthorizationHeader}"`, + ) + } +} + +function validateUserConfig(userConfig: unknown, failures: string[]): void { + if (!isRecord(userConfig) || !hasExactKeys(userConfig, [hostedApiKeyOption])) { + failures.push( + `Claude plugin manifest must declare exactly one user configuration option named ${hostedApiKeyOption}`, + ) + return + } + + const option = userConfig[hostedApiKeyOption] + if (!isRecord(option)) { + failures.push(`Claude plugin manifest ${hostedApiKeyOption} option must be an object`) + return + } + + if (option.type !== 'string') { + failures.push(`Claude plugin manifest ${hostedApiKeyOption} type must be string`) + } + if (option.sensitive !== true) { + failures.push( + `Claude plugin manifest ${hostedApiKeyOption} must be sensitive so the key is stored outside settings.json`, + ) + } + if (option.required !== false) { + failures.push( + `Claude plugin manifest ${hostedApiKeyOption} must set required to false so the local connector works without a key`, + ) + } + if ('default' in option) { + failures.push(`Claude plugin manifest ${hostedApiKeyOption} must not ship a default credential`) + } +} + export function validateClaudeMcpPolicy( config: unknown, pluginManifest: unknown, @@ -22,29 +104,12 @@ export function validateClaudeMcpPolicy( } const servers = config.mcpServers - if (!isRecord(servers) || !hasExactKeys(servers, ['pascal'])) { - return ['skills/.mcp.json must contain exactly one server named pascal'] - } - - const pascal = servers.pascal - if (!isRecord(pascal) || !hasExactKeys(pascal, ['type', 'command', 'args'])) { - failures.push( - 'skills/.mcp.json pascal server must contain only type, command, and args; remote or credential fields are not allowed', - ) - return failures + if (!isRecord(servers) || !hasExactKeys(servers, ['pascal', 'pascal-hosted'])) { + return ['skills/.mcp.json must declare exactly the pascal and pascal-hosted servers'] } - if (pascal.type !== 'stdio') failures.push('skills/.mcp.json pascal server type must be stdio') - if (pascal.command !== 'pascal') - failures.push('skills/.mcp.json pascal server command must be pascal') - if ( - !Array.isArray(pascal.args) || - pascal.args.length !== 2 || - pascal.args[0] !== 'mcp' || - pascal.args[1] !== 'connect' - ) { - failures.push('skills/.mcp.json pascal server args must be exactly ["mcp", "connect"]') - } + validateLocalServer(servers.pascal, failures) + validateHostedServer(servers['pascal-hosted'], failures) if (!isRecord(pluginManifest)) { failures.push('Claude plugin manifest must be an object') @@ -52,9 +117,7 @@ export function validateClaudeMcpPolicy( if ('mcpServers' in pluginManifest) { failures.push('Claude plugin manifest must not define inline MCP servers') } - if ('userConfig' in pluginManifest) { - failures.push('Claude plugin manifest must not request credentials or user configuration') - } + validateUserConfig(pluginManifest.userConfig, failures) } if (!isRecord(marketplaceEntry)) { diff --git a/scripts/validate-skills.ts b/scripts/validate-skills.ts index 4ebcd5028c..88b8516dba 100644 --- a/scripts/validate-skills.ts +++ b/scripts/validate-skills.ts @@ -909,12 +909,30 @@ if (portableMcpConfig.$schema !== portableMcpSchema) { if (Object.keys(portableMcpConfig).sort().join(',') !== '$schema,mcpServers') { fail('Portable mcp.json must contain only $schema and mcpServers') } -if (canonicalJson(portableMcpConfig.mcpServers) !== canonicalJson(claudeMcpConfig.mcpServers)) { - fail('Portable mcp.json and skills/.mcp.json must declare the same mcpServers block') +const claudeMcpServers = (claudeMcpConfig.mcpServers ?? {}) as Record +const portableMcpServers = (portableMcpConfig.mcpServers ?? {}) as Record +if (Object.keys(portableMcpServers).sort().join(',') !== 'pascal') { + fail('Portable mcp.json must declare only the local pascal server') +} +if (canonicalJson(portableMcpServers.pascal) !== canonicalJson(claudeMcpServers.pascal)) { + fail('Portable mcp.json and skills/.mcp.json must declare an identical pascal server') +} +// The hosted server reads its key through ${user_config.*}, which only Claude Code substitutes, so +// it stays in the Claude plugin root instead of the portable Agent Plugins manifest. +if (Object.keys(claudeMcpServers).sort().join(',') !== 'pascal,pascal-hosted') { + fail('skills/.mcp.json must add only the Claude-specific pascal-hosted server') +} + +const claudeUserConfig = (claudePlugin.userConfig ?? {}) as Record +const claudeHostedKeyOption = claudeUserConfig.pascal_api_key as Record | undefined +if (claudeHostedKeyOption?.sensitive !== true) { + fail('Claude plugin userConfig.pascal_api_key must set sensitive so the key never reaches a file') +} +if (claudeHostedKeyOption?.required !== false) { + fail('Claude plugin userConfig.pascal_api_key must set required to false for local-only installs') } -const portablePascalServer = (portableMcpConfig.mcpServers as Record | undefined) - ?.pascal as Record | undefined +const portablePascalServer = portableMcpServers.pascal as Record | undefined if (geminiExtension.name !== 'pascal') fail('Gemini CLI extension name must be pascal') if (geminiExtension.version !== pluginVersion) { fail(`Gemini CLI extension version must match the root plugin.json version ${pluginVersion}`) diff --git a/skills/.claude-plugin/plugin.json b/skills/.claude-plugin/plugin.json index 73926b3e61..3c6ffec9d6 100644 --- a/skills/.claude-plugin/plugin.json +++ b/skills/.claude-plugin/plugin.json @@ -13,5 +13,14 @@ "repository": "https://github.com/pascalorg/editor", "license": "MIT", "keywords": ["pascal", "3d", "architecture", "mcp", "furniture", "spatial"], + "userConfig": { + "pascal_api_key": { + "type": "string", + "title": "Pascal API key (hosted)", + "description": "Paste a key from editor.pascal.app Settings to work on your hosted projects and Capture scans; leave empty to use only the local Pascal CLI.", + "sensitive": true, + "required": false + } + }, "skills": ["./pascal-3d", "./furniture-fit"] } diff --git a/skills/.mcp.json b/skills/.mcp.json index 12a490e574..01f779f616 100644 --- a/skills/.mcp.json +++ b/skills/.mcp.json @@ -4,6 +4,13 @@ "type": "stdio", "command": "pascal", "args": ["mcp", "connect"] + }, + "pascal-hosted": { + "type": "http", + "url": "https://editor.pascal.app/api/mcp", + "headers": { + "Authorization": "Bearer ${user_config.pascal_api_key}" + } } } } diff --git a/skills/README.md b/skills/README.md index 0548e8a221..512f1ed233 100644 --- a/skills/README.md +++ b/skills/README.md @@ -70,13 +70,13 @@ codex plugin marketplace add pascalorg/editor codex plugin add pascal-agent-skills@pascal ``` -The Claude plugin installs the instructions from the canonical `skills/` directory and supplies one local stdio server that runs `pascal mcp connect`. This `skills/` directory is itself the Claude plugin root, so an install copies only the two skill bundles and their plugin metadata rather than the repository. Install and start the Pascal CLI first, and keep `pascal` on Claude Code's `PATH`. The bundled local connector needs no Pascal account or API key and does not upload projects automatically. Codex and individually installed skills still use the setup reference included in either skill. +The Claude plugin installs the instructions from the canonical `skills/` directory and supplies two servers: a local stdio server that runs `pascal mcp connect`, and a hosted `pascal-hosted` server for `https://editor.pascal.app/api/mcp` that prompts for an optional Pascal API key when the plugin is enabled and keeps it in the OS keychain. This `skills/` directory is itself the Claude plugin root, so an install copies only the two skill bundles and their plugin metadata rather than the repository. Install and start the Pascal CLI first, and keep `pascal` on Claude Code's `PATH`. The bundled local connector needs no Pascal account or API key and does not upload projects automatically; leaving the hosted key empty keeps the install local-only. Codex and individually installed skills still use the setup reference included in either skill. Claude Code 2.1.258 loads both the user-scoped `pascal` server created by `pascal mcp setup claude` and the plugin-provided server. Run `claude mcp remove --scope user pascal` before reloading or restarting Claude Code so only the plugin owns the connection lifecycle. Use `/mcp` to remove or disable any project- or local-scoped Pascal connection too. Leaving both connections active violates the one-active-agent-client-per-local-service requirement. For a hosted Pascal project, disable the plugin-provided local server in `/mcp`, then configure the hosted endpoint from the setup reference. Plugin installation alone never creates an account, uploads a project, or authorizes paid work. -The root [`plugin.json`](../plugin.json) is the portable Agent Plugins manifest used for OpenAI submission, and the root [`mcp.json`](../mcp.json) is the only MCP configuration path Codex and Cursor read; Claude Code reads the same server from [`.mcp.json`](.mcp.json) in its `skills/` plugin root, next to [`.claude-plugin/plugin.json`](.claude-plugin/plugin.json). The repository keeps `.codex-plugin/plugin.json` as a compatibility fallback and validates that both expose the same OpenAI listing metadata. Public-directory submission, review, and publication are separate external steps; a Git marketplace install does not make the plugin publicly listed in ChatGPT or Codex. +The root [`plugin.json`](../plugin.json) is the portable Agent Plugins manifest used for OpenAI submission, and the root [`mcp.json`](../mcp.json) is the only MCP configuration path Codex and Cursor read; Claude Code reads the same local server from [`.mcp.json`](.mcp.json) in its `skills/` plugin root, next to [`.claude-plugin/plugin.json`](.claude-plugin/plugin.json), and only that file carries the hosted server because `${user_config.*}` substitution is Claude-specific. The repository keeps `.codex-plugin/plugin.json` as a compatibility fallback and validates that both expose the same OpenAI listing metadata. Public-directory submission, review, and publication are separate external steps; a Git marketplace install does not make the plugin publicly listed in ChatGPT or Codex. The npm `beta` CLI remains on the older runtime contract. For the read-only `check_collisions.candidate` capability used by the current furniture workflow, follow the checksum-verified [GitHub preview instructions](pascal-3d/references/setup.md#verified-github-preview). The preview archive is published on GitHub, not npm. @@ -111,4 +111,4 @@ bun run skills:validate Run `claude plugin validate . --strict` manually as well. It stays out of the script and out of CI because it needs the Claude Code CLI, which is not installed on every runner. -The repository validator checks the exact two-skill public discovery surface, keeps contributor-only workflows internal, and checks frontmatter, semantic skill versions, bundled links and their heading anchors, task and trigger fixtures, semantic furniture next-action decision cases, scoped ClawHub ignore policies without re-inclusion overrides, the exact credential-free Claude local MCP configuration, an identical `mcp.json` server block for Codex and Cursor, a Gemini CLI manifest that runs the same command at the same version, Claude marketplace and plugin skills lists that equal the packaged bundles exactly, the publishing suite, the exact 46-tool OpenAI annotation and justification packet, portable and compatibility manifest consistency with one plugin version, description, and author across every plugin descriptor, OpenAI public-directory metadata limits including its documented interface fields, bundled branding assets, and accidental private-path or credential leakage. +The repository validator checks the exact two-skill public discovery surface, keeps contributor-only workflows internal, and checks frontmatter, semantic skill versions, bundled links and their heading anchors, task and trigger fixtures, semantic furniture next-action decision cases, scoped ClawHub ignore policies without re-inclusion overrides, the exact Claude local and hosted MCP configuration with the hosted key declared as an optional, sensitive user-configuration option, an identical local `pascal` server in the `mcp.json` Codex and Cursor read, a Gemini CLI manifest that runs the same command at the same version, Claude marketplace and plugin skills lists that equal the packaged bundles exactly, the publishing suite, the exact 46-tool OpenAI annotation and justification packet, portable and compatibility manifest consistency with one plugin version, description, and author across every plugin descriptor, OpenAI public-directory metadata limits including its documented interface fields, bundled branding assets, and accidental private-path or credential leakage. diff --git a/skills/furniture-fit/references/setup.md b/skills/furniture-fit/references/setup.md index 035f0379e4..fecfbba343 100644 --- a/skills/furniture-fit/references/setup.md +++ b/skills/furniture-fit/references/setup.md @@ -58,7 +58,7 @@ Use only one active agent client with each local CLI service. The standalone HTT ## Existing hosted project -Create an API key in Pascal Settings for the same user or organization that owns the target project. Set `PASCAL_API_KEY` to that key without printing it. If you assign it in a shell command, avoid or remove that command from shell history. The hosted Streamable HTTP endpoint is: +Create an API key in Pascal Settings (`https://editor.pascal.app/settings`) for the same user or organization that owns the target project. Set `PASCAL_API_KEY` to that key without printing it. If you assign it in a shell command, avoid or remove that command from shell history. The hosted Streamable HTTP endpoint is: ```text https://editor.pascal.app/api/mcp @@ -79,6 +79,10 @@ Codex stores the environment-variable name, not its value. Set `PASCAL_API_KEY` Claude Code: +Plugin users set the key once in the configuration prompt shown when `pascal-agent-skills@pascal` is enabled. To add or change it later, reinstall with `claude plugin install pascal-agent-skills@pascal --config pascal_api_key=`, or open `/plugin` in a session and use its configure flow; there is no `claude plugin config` command. The hosted tools then load under the plugin's `pascal-hosted` server beside the local `pascal` server, and Claude Code keeps the key in the OS keychain, falling back to `~/.claude/.credentials.json`, rather than writing it into `settings.json` or any project file. + +Without the plugin, register the hosted endpoint manually: + ```bash : "${PASCAL_API_KEY:?Set PASCAL_API_KEY to the apiKey returned by Pascal}" && \ claude mcp add --scope user --transport http pascal https://editor.pascal.app/api/mcp \ diff --git a/skills/pascal-3d/SKILL.md b/skills/pascal-3d/SKILL.md index 360f26de3b..0edb0213e6 100644 --- a/skills/pascal-3d/SKILL.md +++ b/skills/pascal-3d/SKILL.md @@ -21,7 +21,7 @@ Use Pascal as the scene authority. Prefer its semantic tools and validation resu ## Start here -1. Check whether a Pascal MCP server is already connected. If it is, read `pascal://agent-guide` and inspect the available tools and their input schemas before changing anything. Installed and hosted releases can differ from this skill's source-review snapshot. +1. Check whether a Pascal MCP server is already connected. If it is, read `pascal://agent-guide` and inspect the available tools and their input schemas before changing anything. Installed and hosted releases can differ from this skill's source-review snapshot. When both the `pascal` and `pascal-hosted` servers are connected, use `pascal-hosted` for projects that live in the person's Pascal account, including Capture scans, and `pascal` for local work; never call both for the same task. 2. If Pascal is not connected, select the data boundary that matches the request: - **Local:** use the Pascal CLI for projects that should remain on this machine. - **Hosted existing account:** use an API key created by the same Pascal user or organization that owns the target project. diff --git a/skills/pascal-3d/references/setup.md b/skills/pascal-3d/references/setup.md index 078ece3607..539c43bc8a 100644 --- a/skills/pascal-3d/references/setup.md +++ b/skills/pascal-3d/references/setup.md @@ -93,7 +93,7 @@ Use the hosted endpoint when the user wants the agent to work in a Pascal accoun https://editor.pascal.app/api/mcp ``` -The user creates an API key in Pascal Settings and chooses the intended personal or organization workspace. Set `PASCAL_API_KEY` to that key without printing it. If you assign it in a shell command, avoid or remove that command from shell history. +The user creates an API key in Pascal Settings (`https://editor.pascal.app/settings`) and chooses the intended personal or organization workspace. Set `PASCAL_API_KEY` to that key without printing it. If you assign it in a shell command, avoid or remove that command from shell history. Codex CLI: @@ -110,6 +110,10 @@ Codex stores the environment-variable name, not its value. Set `PASCAL_API_KEY` Claude Code: +Plugin users set the key once in the configuration prompt shown when `pascal-agent-skills@pascal` is enabled. To add or change it later, reinstall with `claude plugin install pascal-agent-skills@pascal --config pascal_api_key=`, or open `/plugin` in a session and use its configure flow; there is no `claude plugin config` command. The hosted tools then load under the plugin's `pascal-hosted` server beside the local `pascal` server, and Claude Code keeps the key in the OS keychain, falling back to `~/.claude/.credentials.json`, rather than writing it into `settings.json` or any project file. + +Without the plugin, register the hosted endpoint manually: + ```bash : "${PASCAL_API_KEY:?Set PASCAL_API_KEY to the apiKey returned by Pascal}" && \ claude mcp add --scope user --transport http pascal https://editor.pascal.app/api/mcp \