diff --git a/.oxlintrc.json b/.oxlintrc.json index 1ea05a2..ea02cd5 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -14,7 +14,8 @@ "typescript/no-unused-vars": [ "error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } - ] + ], + "no-shadow": "warn" }, "overrides": [ { diff --git a/README.md b/README.md index 6e5e566..5a11d24 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,20 @@ Works with Claude Code, Claude Desktop, Cursor, Windsurf, Copilot, and any MCP-c ## Getting started +### Setup wizard + +The interactive wizard handles config, auth tokens, keychain storage, and PII mode in one step: + +```bash +# Project-level — writes .mcp.json in the current directory (shared with your team) +npx @hayodev/crystallize-mcp --setup + +# Global — registers via `claude mcp add` (Claude Code) or writes Claude Desktop config +npx @hayodev/crystallize-mcp --setup --global +``` + +### Manual config + Standard MCP config (works in any client): ```json @@ -30,7 +44,7 @@ Standard MCP config (works in any client): Add `CRYSTALLIZE_ACCESS_TOKEN_ID` and `CRYSTALLIZE_ACCESS_TOKEN_SECRET` to the `env` block for PIM tools (shapes, orders, customers). See [Authentication](#authentication).
-Claude Code +Claude Code (CLI) ```bash claude mcp add crystallize \ @@ -42,24 +56,12 @@ claude mcp add crystallize \ Use `--scope project` to write to `.mcp.json` (shared with your team) or `--scope user` for personal use across all projects. -Or run the guided setup wizard, which can optionally store tokens in the OS keychain instead of plain text: - -```bash -npx @hayodev/crystallize-mcp --setup -``` -
Claude Desktop -Run the guided wizard — it writes directly to `claude_desktop_config.json` and can store tokens in the macOS Keychain so they never appear in the config file: - -```bash -npx @hayodev/crystallize-mcp --setup --global -``` - -Or add the standard config manually to `~/Library/Application Support/Claude/claude_desktop_config.json`. +Add the standard config to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows).
@@ -135,13 +137,15 @@ Or copy the standard config JSON above before opening the command — Raycast wi
-From source +From source (maintainers) + +The `--local` flag is for developing crystallize-mcp itself. It writes `.mcp.json` pointing to the local build output — **run this from the repo root only**: ```bash git clone https://github.com/HayoDev/crystallize-mcp.git cd crystallize-mcp npm install && npm run build -npx . --setup --local # writes .mcp.json pointing to local build +npx . --setup --local # writes .mcp.json pointing to ./build/ ``` Or point your MCP client directly at the built entry point: @@ -311,14 +315,14 @@ Easy to pipe into log aggregators (Datadog, CloudWatch, Splunk) — but ensure y ### Keychain storage (optional) -The setup wizard (`npx @hayodev/crystallize-mcp --setup`) can store tokens in the OS keychain (macOS Keychain, Windows Credential Manager, or libsecret on Linux) so they never appear as plain text in config files. This is particularly useful for: +The setup wizard (`npx @hayodev/crystallize-mcp --setup`) can store tokens in the OS keychain (macOS Keychain, Windows Credential Manager, or libsecret on Linux) so they never appear as plain text in config files. The MCP server resolves credentials from the keychain automatically at startup — no extra configuration needed. -- **Claude Desktop users** — config is written to a JSON file with no CLI equivalent for secret management -- **Shared `.mcp.json`** — when your project config is committed to git, keychain storage keeps tokens out of the repository +This is useful when: -When tokens are in the keychain, the config only needs `CRYSTALLIZE_TENANT_IDENTIFIER` — credentials are resolved automatically at startup. +- **Your `.mcp.json` is committed to git** — keychain keeps tokens out of the repository +- **You prefer not to have secrets in plain text config files** — the config only needs `CRYSTALLIZE_TENANT_IDENTIFIER` -Note: CLI-based MCP clients (`claude mcp add`, Cursor, Copilot, etc.) store env vars as plain text in their config files and do not integrate with the OS keychain directly. If plain text env vars in a local config file are acceptable for your setup, the wizard's keychain option is not needed. +When using `--setup --global` with Claude Code, this applies only if keychain storage is available and you opt into it: in that case, the wizard runs `claude mcp add` with only the non-secret env vars, and tokens are read from the keychain at runtime so they do not appear in Claude Code config. If keychain storage is unavailable or you choose not to use it, the wizard passes token env vars to `claude mcp add`, and Claude Code may store them in plain text. ### Access mode diff --git a/src/bin/setup.ts b/src/bin/setup.ts index 908837d..2fc1695 100644 --- a/src/bin/setup.ts +++ b/src/bin/setup.ts @@ -11,9 +11,10 @@ */ import { createInterface } from 'node:readline'; -import { writeFileSync, readFileSync, existsSync } from 'node:fs'; -import { resolve as resolvePath } from 'node:path'; +import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs'; +import { resolve as resolvePath, dirname } from 'node:path'; import { homedir } from 'node:os'; +import { execFileSync } from 'node:child_process'; import { isKeychainAvailable, writeCredentials } from '../credentials.js'; const rl = createInterface({ input: process.stdin, output: process.stderr }); @@ -86,6 +87,25 @@ async function main() { // Access mode const accessMode = await ask('Access mode: read / write / admin', 'read'); + // PII mode — relevant when accessing customer/order data + console.error( + '\nPII mode controls how customer data (emails, phones, addresses) is returned:', + ); + console.error(' full — all data returned as-is (default)'); + console.error(' masked — emails and phones partially masked'); + console.error(' none — contact/PII fields stripped entirely\n'); + const piiMode = await ask('PII mode: full / masked / none', 'full'); + + // Dry-run — only relevant for write/admin modes + let dryRun = false; + if (accessMode === 'write' || accessMode === 'admin') { + console.error( + '\nDry-run mode previews mutations without executing them — useful for testing.', + ); + const dryRunAnswer = await ask('Enable dry-run mode? y/n', 'n'); + dryRun = dryRunAnswer.toLowerCase() === 'y'; + } + // Keychain offer — only if tokens were entered and keychain is reachable const hasSecrets = tokenId || tokenSecret || staticToken; let useKeychain = false; @@ -139,6 +159,12 @@ async function main() { if (accessMode !== 'read') { env.CRYSTALLIZE_ACCESS_MODE = accessMode; } + if (piiMode !== 'full') { + env.CRYSTALLIZE_PII_MODE = piiMode; + } + if (dryRun) { + env.CRYSTALLIZE_DRY_RUN = 'true'; + } // Build MCP config entry const mcpEntry = isLocal @@ -154,36 +180,76 @@ async function main() { }; if (isGlobal) { - // Claude Desktop config - const configPath = - process.platform === 'darwin' - ? resolvePath( - homedir(), - 'Library/Application Support/Claude/claude_desktop_config.json', - ) - : resolvePath( - homedir(), - 'AppData/Roaming/Claude/claude_desktop_config.json', - ); + // Try Claude Code CLI first, then fall back to Claude Desktop config file + const hasClaudeCli = (() => { + try { + execFileSync('claude', ['--version'], { stdio: 'ignore' }); + return true; + } catch { + return false; + } + })(); + + if (hasClaudeCli) { + // Use `claude mcp add` for Claude Code + const args = ['mcp', 'add', '--transport', 'stdio']; + for (const [key, val] of Object.entries(env)) { + args.push('--env', `${key}=${val}`); + } + args.push('crystallize', '--'); + if (isLocal) { + args.push( + 'node', + resolvePath(process.cwd(), 'build/src/bin/crystallize-mcp.js'), + ); + } else { + args.push('npx', '-y', '@hayodev/crystallize-mcp@latest'); + } - let config: Record = {}; - if (existsSync(configPath)) { try { - config = JSON.parse(readFileSync(configPath, 'utf-8')) as Record< - string, - unknown - >; + execFileSync('claude', args, { stdio: 'inherit' }); + console.error('\n✅ Added via Claude Code CLI: claude mcp add'); } catch { - // start fresh + console.error( + '\n⚠️ `claude mcp add` failed. You can add it manually:', + ); + console.error( + ` claude mcp add crystallize -- npx -y @hayodev/crystallize-mcp@latest`, + ); } - } + } else { + // Fall back to Claude Desktop config file + const configPath = + process.platform === 'darwin' + ? resolvePath( + homedir(), + 'Library/Application Support/Claude/claude_desktop_config.json', + ) + : resolvePath( + homedir(), + 'AppData/Roaming/Claude/claude_desktop_config.json', + ); - const servers = (config.mcpServers ?? {}) as Record; - servers.crystallize = mcpEntry; - config.mcpServers = servers; + let config: Record = {}; + if (existsSync(configPath)) { + try { + config = JSON.parse(readFileSync(configPath, 'utf-8')) as Record< + string, + unknown + >; + } catch { + // start fresh + } + } - writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); - console.error(`\n✅ Added to Claude Desktop config: ${configPath}`); + const servers = (config.mcpServers ?? {}) as Record; + servers.crystallize = mcpEntry; + config.mcpServers = servers; + + mkdirSync(dirname(configPath), { recursive: true }); + writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); + console.error(`\n✅ Added to Claude Desktop config: ${configPath}`); + } } else { // Local .mcp.json for Claude Code const configPath = resolvePath(process.cwd(), '.mcp.json'); @@ -203,6 +269,7 @@ async function main() { servers.crystallize = mcpEntry; config.mcpServers = servers; + mkdirSync(dirname(configPath), { recursive: true }); writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); console.error(`\n✅ Created ${configPath}`); } @@ -216,7 +283,16 @@ async function main() { } else { console.error('Auth: none (public catalogue only)'); } - console.error(`Mode: ${accessMode}\n`); + console.error(`Mode: ${accessMode}`); + if (piiMode !== 'full') { + console.error(`PII: ${piiMode}`); + } + if (dryRun) { + console.error( + 'Dry-run: enabled (mutations will be previewed, not executed)', + ); + } + console.error(''); rl.close(); } diff --git a/tests/schema.test.ts b/tests/schema.test.ts index 9839be3..e76bef7 100644 --- a/tests/schema.test.ts +++ b/tests/schema.test.ts @@ -331,10 +331,10 @@ describe('schema introspection with mock API', () => { }); // Generate a schema large enough to exceed the 50k auto-summary threshold - const largeTypes = Array.from({ length: 200 }, (_, i) => ({ + const largeTypes = Array.from({ length: 200 }, (_a, i) => ({ kind: 'OBJECT', name: `GeneratedType${i}`, - fields: Array.from({ length: 20 }, (_, j) => ({ + fields: Array.from({ length: 20 }, (_b, j) => ({ name: `field${j}WithALongNameToInflateSize`, type: { kind: 'SCALAR', name: 'String', ofType: null }, args: [],