From 508932c99e4343b0ba86251ca2289f8872aaff69 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 8 Nov 2025 10:41:59 -0500 Subject: [PATCH 1/8] docs: add scope3 package to CLI installation instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Highlights the recommended scope3 package for shorter npx commands. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8583d9f..65d7222 100644 --- a/README.md +++ b/README.md @@ -48,12 +48,27 @@ const campaign = await client.campaigns.create({ ## CLI Usage -The CLI dynamically discovers available commands from the API server, ensuring it's always up-to-date: +The CLI dynamically discovers available commands from the API server, ensuring it's always up-to-date. + +### Quick Start (Recommended) + +For the shortest command, use the `scope3` package: + +```bash +# Use with npx (no install needed) +npx scope3 --help + +# Or install globally +npm install -g scope3 +scope3 --help +``` + +### Alternative: Full Package Name + +You can also use the full package name: ```bash -# Install globally or use npx npm install -g @scope3/agentic-client -# or npx @scope3/agentic-client --help # Configure authentication From ccbd1f5b87bcc016c627a2a00f469bae8e2de3d5 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 8 Nov 2025 10:42:42 -0500 Subject: [PATCH 2/8] Add empty changeset for docs-only change --- .changeset/readme-update.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changeset/readme-update.md diff --git a/.changeset/readme-update.md b/.changeset/readme-update.md new file mode 100644 index 0000000..663cef8 --- /dev/null +++ b/.changeset/readme-update.md @@ -0,0 +1,4 @@ +--- +--- + +Documentation update for scope3 package installation From 85559efc2cf77d535e94be8da21a728b8b560099 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 8 Nov 2025 10:46:31 -0500 Subject: [PATCH 3/8] Require structuredContent in all API responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes the client fail loudly when the API doesn't return structuredContent, helping catch API specification violations that need upstream fixes. Changes: - Remove fallback JSON parsing from text content - Throw descriptive error when structuredContent is missing - Include debug details (tool name, content type, preview) in error - Update tests to verify strict validation This ensures we catch and report any API endpoints that aren't following the Scope3 MCP specification correctly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/__tests__/client-mcp.test.ts | 54 ++++++++++++------------ src/client.ts | 71 +++++++++++--------------------- 2 files changed, 50 insertions(+), 75 deletions(-) diff --git a/src/__tests__/client-mcp.test.ts b/src/__tests__/client-mcp.test.ts index 577f14b..1b1f471 100644 --- a/src/__tests__/client-mcp.test.ts +++ b/src/__tests__/client-mcp.test.ts @@ -149,52 +149,50 @@ describe('Scope3Client MCP Protocol', () => { }); }); - describe('text content fallback (JSON parsing)', () => { - it('should parse valid JSON from text content', async () => { + describe('API specification violations', () => { + it('should throw error when JSON is in text content instead of structuredContent', async () => { const data = { id: '456', status: 'completed' }; mockMcpClient.callTool.mockResolvedValue({ content: [{ type: 'text', text: JSON.stringify(data) }], }); - const result = await client['callTool']('test_tool', {}); - - expect(result).toEqual(data); + await expect(client['callTool']('test_tool', {})).rejects.toThrow( + 'MCP API returned response without structuredContent' + ); + await expect(client['callTool']('test_tool', {})).rejects.toThrow( + 'This violates the Scope3 API specification' + ); }); - it('should parse JSON array from text content', async () => { + it('should throw error when array is in text content instead of structuredContent', async () => { const data = [{ id: '1' }, { id: '2' }]; mockMcpClient.callTool.mockResolvedValue({ content: [{ type: 'text', text: JSON.stringify(data) }], }); - const result = await client['callTool']('test_tool', {}); - - expect(result).toEqual(data); + await expect(client['callTool']('test_tool', {})).rejects.toThrow( + 'MCP API returned response without structuredContent' + ); }); - it('should wrap non-JSON text in message object', async () => { + it('should throw error when plain text is returned without structuredContent', async () => { const plainText = 'Operation completed successfully'; mockMcpClient.callTool.mockResolvedValue({ content: [{ type: 'text', text: plainText }], }); - const result = await client['callTool'], { message: string }>( - 'test_tool', - {} + await expect(client['callTool']('test_tool', {})).rejects.toThrow( + 'MCP API returned response without structuredContent' ); - - expect(result).toEqual({ message: plainText }); }); - it('should handle text content with special characters', async () => { - const data = { message: 'Success! 🎉 Campaign created.' }; + it('should include debug info in error when structuredContent is missing', async () => { mockMcpClient.callTool.mockResolvedValue({ - content: [{ type: 'text', text: JSON.stringify(data) }], + content: [{ type: 'text', text: 'some data' }], }); - const result = await client['callTool']('test_tool', {}); - - expect(result).toEqual(data); + await expect(client['callTool']('test_tool', {})).rejects.toThrow('test_tool'); + await expect(client['callTool']('test_tool', {})).rejects.toThrow('Debug info'); }); }); @@ -205,7 +203,7 @@ describe('Scope3Client MCP Protocol', () => { }); await expect(client['callTool']('test_tool', {})).rejects.toThrow( - 'Unexpected tool response format' + 'MCP API returned response without structuredContent' ); }); @@ -215,7 +213,7 @@ describe('Scope3Client MCP Protocol', () => { }); await expect(client['callTool']('test_tool', {})).rejects.toThrow( - 'Unexpected tool response format' + 'MCP API returned response without structuredContent' ); }); @@ -267,7 +265,7 @@ describe('Scope3Client MCP Protocol', () => { expect(client.lastDebugInfo?.durationMs).toBeGreaterThanOrEqual(0); }); - it('should store raw response when parsing JSON from text', async () => { + it('should not store debug info when API violates spec (no structuredContent)', async () => { const data = { id: '456' }; const rawText = JSON.stringify(data); @@ -275,10 +273,10 @@ describe('Scope3Client MCP Protocol', () => { content: [{ type: 'text', text: rawText }], }); - await client['callTool']('test_tool', {}); - - expect(client.lastDebugInfo?.rawResponse).toBe(rawText); - expect(client.lastDebugInfo?.response).toEqual(data); + // Should throw before storing debug info + await expect(client['callTool']('test_tool', {})).rejects.toThrow( + 'MCP API returned response without structuredContent' + ); }); it('should not store debug info when debug mode is disabled', async () => { diff --git a/src/client.ts b/src/client.ts index 47765ec..00d1eed 100644 --- a/src/client.ts +++ b/src/client.ts @@ -145,10 +145,10 @@ export class Scope3Client { }); } - // MCP tools can return structured content or text content - // Priority: structuredContent > parsed JSON from text > raw text + // MCP tools MUST return structured content according to Scope3 API spec + // If structuredContent is missing, this is an API bug that needs to be fixed upstream - // Check for structuredContent first (preferred) + // Check for structuredContent (required) if (result.structuredContent) { if (this.debug) { this.lastDebugInfo = { @@ -161,51 +161,28 @@ export class Scope3Client { return result.structuredContent as TResponse; } - // Fall back to text content - if (result.content && Array.isArray(result.content) && result.content.length > 0) { - const content = result.content[0]; - if (content.type === 'text') { - const rawResponse = content.text; - - // Try to parse as JSON first, if that fails return the text as-is - try { - const parsed = JSON.parse(rawResponse); - - // Store debug info if enabled - if (this.debug) { - this.lastDebugInfo = { - toolName, - request: args as Record, - response: parsed, - rawResponse, - durationMs, - }; - } - - return parsed as TResponse; - } catch { - // If not JSON, return the text wrapped in an object - if (this.debug) { - logger.warn('MCP tool returned non-JSON text (no structuredContent)', { - toolName, - textLength: rawResponse.length, - }); - - this.lastDebugInfo = { - toolName, - request: args as Record, - response: { message: rawResponse }, - rawResponse, - durationMs, - }; - } - - return { message: rawResponse } as TResponse; - } - } - } + // FAIL LOUDLY: structuredContent is missing + // This helps catch API bugs that need upstream fixes + const content = result.content as Array<{ type: string; text?: string }> | undefined; + const firstContent = content && content.length > 0 ? content[0] : null; + const errorDetails = { + toolName, + hasContent: Boolean(content), + contentType: firstContent?.type, + textPreview: + firstContent?.type === 'text' && firstContent.text + ? firstContent.text.substring(0, 200) + : undefined, + }; - throw new Error('Unexpected tool response format'); + logger.error('MCP API VIOLATION: Missing structuredContent', errorDetails); + + throw new Error( + `MCP API returned response without structuredContent for tool "${toolName}". ` + + `This violates the Scope3 API specification. ` + + `The API must be fixed to include structuredContent in all responses. ` + + `Debug info: ${JSON.stringify(errorDetails)}` + ); } protected getClient(): Client { From a33bcfd48b2721c126d3e4391987c8f18f620b31 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 8 Nov 2025 10:50:20 -0500 Subject: [PATCH 4/8] Display human-readable message alongside structured data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the MCP API returns both content.text (human message) and structuredContent (structured data), the CLI now displays both: 1. Human-readable message in cyan 2. Blank line 3. Structured data in table/list format In JSON mode, includes _message field with the text. Changes: - Client extracts text message from content and includes as _message - CLI detects _message and displays it before structured data - Updated tests to verify message handling - 84 tests passing Example output: Found 1 agent: 1. Yahoo Staging Sales Agent [table with structured data follows] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/__tests__/client-mcp.test.ts | 20 ++++++++++++++++++-- src/cli.ts | 17 +++++++++++++++++ src/client.ts | 14 ++++++++++++-- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/__tests__/client-mcp.test.ts b/src/__tests__/client-mcp.test.ts index 1b1f471..a0bc4ff 100644 --- a/src/__tests__/client-mcp.test.ts +++ b/src/__tests__/client-mcp.test.ts @@ -103,11 +103,27 @@ describe('Scope3Client MCP Protocol', () => { }); describe('structuredContent handling (preferred path)', () => { - it('should return structuredContent when present', async () => { + it('should return structuredContent with message when text content present', async () => { const expectedData = { id: '123', name: 'Test Campaign', status: 'active' }; + const textMessage = 'Campaign retrieved successfully'; mockMcpClient.callTool.mockResolvedValue({ structuredContent: expectedData, - content: [{ type: 'text', text: 'This should be ignored' }], + content: [{ type: 'text', text: textMessage }], + }); + + const result = await client['callTool'], typeof expectedData>( + 'campaigns_get', + { campaignId: '123' } + ); + + expect(result).toEqual({ _message: textMessage, ...expectedData }); + }); + + it('should return structuredContent without message when no text content', async () => { + const expectedData = { id: '123', name: 'Test Campaign', status: 'active' }; + mockMcpClient.callTool.mockResolvedValue({ + structuredContent: expectedData, + content: [], }); const result = await client['callTool'], typeof expectedData>( diff --git a/src/cli.ts b/src/cli.ts index 75fefdd..5bfed39 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -193,6 +193,23 @@ function formatOutput(data: unknown, format: string): void { const dataObj = data as Record; let actualData: unknown = dataObj.data || data; + // Extract and display human-readable message if present (from MCP content.text) + let humanMessage: string | undefined; + if (typeof actualData === 'object' && actualData && '_message' in actualData) { + const dataRecord = actualData as Record; + humanMessage = String(dataRecord._message); + // Remove _message from the data to process + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { _message, ...rest } = dataRecord; + actualData = rest; + } + + // Display the human-readable message first (if not in JSON mode) + if (humanMessage) { + console.log(chalk.cyan(humanMessage)); + console.log(); // Blank line before structured data + } + // If the response has an array field, extract it (common pattern for list responses) // Check for: items, brandAgents, campaigns, agents, etc. if (typeof actualData === 'object' && actualData && !Array.isArray(actualData)) { diff --git a/src/client.ts b/src/client.ts index 00d1eed..beb649d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -150,15 +150,25 @@ export class Scope3Client { // Check for structuredContent (required) if (result.structuredContent) { + // Extract human-readable message from content if present + const content = result.content as Array<{ type: string; text?: string }> | undefined; + const textMessage = + content && content.length > 0 && content[0].type === 'text' ? content[0].text : undefined; + + // Wrap response with message if it exists + const response = textMessage + ? { _message: textMessage, ...result.structuredContent } + : result.structuredContent; + if (this.debug) { this.lastDebugInfo = { toolName, request: args as Record, - response: result.structuredContent, + response, durationMs, }; } - return result.structuredContent as TResponse; + return response as TResponse; } // FAIL LOUDLY: structuredContent is missing From 1aa8b27b496aeafaa489c2331cbb2de2331c2718 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 8 Nov 2025 11:01:33 -0500 Subject: [PATCH 5/8] fix: Remove workspace config to fix changeset publishing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workspace setup was causing changesets to fail because it expected @scope3/agentic-client to be in packages/* but it's in the root directory. Changes: - Removed workspaces from package.json - Updated scope3 package to depend on @scope3/agentic-client@^1.0.5 - Updated changeset to only version the main package - The scope3 wrapper will be published separately after the main package This allows changesets to properly version and publish the main package. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .changeset/cli-tool.md | 1 - package.json | 3 --- packages/scope3-cli/package.json | 2 +- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.changeset/cli-tool.md b/.changeset/cli-tool.md index 8e3a876..b32b086 100644 --- a/.changeset/cli-tool.md +++ b/.changeset/cli-tool.md @@ -1,6 +1,5 @@ --- "@scope3/agentic-client": minor -"scope3": minor --- Add dynamic CLI tool for Scope3 Agentic API with automatic command generation diff --git a/package.json b/package.json index ca44047..8b26666 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,6 @@ "name": "@scope3/agentic-client", "version": "1.0.5", "description": "TypeScript client for the Scope3 Agentic API with AdCP webhook support", - "workspaces": [ - "packages/*" - ], "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ diff --git a/packages/scope3-cli/package.json b/packages/scope3-cli/package.json index 05ff4df..c28c341 100644 --- a/packages/scope3-cli/package.json +++ b/packages/scope3-cli/package.json @@ -28,7 +28,7 @@ "url": "https://github.com/scope3data/agentic-client/issues" }, "dependencies": { - "@scope3/agentic-client": "file:../.." + "@scope3/agentic-client": "^1.0.5" }, "publishConfig": { "access": "public" From 17c32099101bdd4fbfb339fe9e9d1afb462ad618 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 8 Nov 2025 12:43:47 -0500 Subject: [PATCH 6/8] Rename package from @scope3/agentic-client to scope3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidate to a single package with a shorter, more memorable name. Changes: - Renamed package from @scope3/agentic-client to scope3 - Removed packages/scope3-cli/ wrapper (no longer needed) - Updated README with new package name and simpler installation - Updated changeset to reference scope3 instead of @scope3/agentic-client - All 84 tests passing Benefits: - Shorter CLI command: npx scope3 (vs npx @scope3/agentic-client) - Single package to maintain and publish - Better developer experience - Follows CLI tool naming conventions (aws, gh, docker, etc.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .changeset/cli-tool.md | 2 +- README.md | 17 ++--------- package.json | 7 +++-- packages/scope3-cli/.npmrc | 1 - packages/scope3-cli/README.md | 48 -------------------------------- packages/scope3-cli/cli.js | 3 -- packages/scope3-cli/package.json | 36 ------------------------ 7 files changed, 8 insertions(+), 106 deletions(-) delete mode 100644 packages/scope3-cli/.npmrc delete mode 100644 packages/scope3-cli/README.md delete mode 100755 packages/scope3-cli/cli.js delete mode 100644 packages/scope3-cli/package.json diff --git a/.changeset/cli-tool.md b/.changeset/cli-tool.md index b32b086..04fbbd9 100644 --- a/.changeset/cli-tool.md +++ b/.changeset/cli-tool.md @@ -1,5 +1,5 @@ --- -"@scope3/agentic-client": minor +"scope3": minor --- Add dynamic CLI tool for Scope3 Agentic API with automatic command generation diff --git a/README.md b/README.md index 65d7222..bd6a8bc 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,13 @@ TypeScript client for the Scope3 Agentic API with AdCP webhook support. ## Installation ```bash -npm install @scope3/agentic-client +npm install scope3 ``` ## Quick Start ```typescript -import { Scope3AgenticClient } from '@scope3/agentic-client'; +import { Scope3AgenticClient } from 'scope3'; const client = new Scope3AgenticClient({ apiKey: process.env.SCOPE3_API_KEY, @@ -50,9 +50,7 @@ const campaign = await client.campaigns.create({ The CLI dynamically discovers available commands from the API server, ensuring it's always up-to-date. -### Quick Start (Recommended) - -For the shortest command, use the `scope3` package: +### Quick Start ```bash # Use with npx (no install needed) @@ -61,15 +59,6 @@ npx scope3 --help # Or install globally npm install -g scope3 scope3 --help -``` - -### Alternative: Full Package Name - -You can also use the full package name: - -```bash -npm install -g @scope3/agentic-client -npx @scope3/agentic-client --help # Configure authentication scope3 config set apiKey your_api_key_here diff --git a/package.json b/package.json index 8b26666..b331f6c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "@scope3/agentic-client", + "name": "scope3", "version": "1.0.5", - "description": "TypeScript client for the Scope3 Agentic API with AdCP webhook support", + "description": "CLI and TypeScript client for the Scope3 Agentic API with AdCP webhook support", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ @@ -46,7 +46,8 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/scope3data/agentic-client.git" + "url": "git+https://github.com/scope3data/agentic-client.git", + "directory": "." }, "bugs": { "url": "https://github.com/scope3data/agentic-client/issues" diff --git a/packages/scope3-cli/.npmrc b/packages/scope3-cli/.npmrc deleted file mode 100644 index 63b729b..0000000 --- a/packages/scope3-cli/.npmrc +++ /dev/null @@ -1 +0,0 @@ -# Automatically rewrite file: dependencies to version numbers on publish diff --git a/packages/scope3-cli/README.md b/packages/scope3-cli/README.md deleted file mode 100644 index b083782..0000000 --- a/packages/scope3-cli/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# scope3 - -Command-line interface for the Scope3 Agentic API. - -This is a thin wrapper around [@scope3/agentic-client](https://www.npmjs.com/package/@scope3/agentic-client) that provides a shorter package name for easier CLI usage. - -## Installation - -```bash -# Global installation -npm install -g scope3 - -# Or use directly with npx -npx scope3 --help -``` - -## Usage - -```bash -# Configure your API key -scope3 config set apiKey YOUR_API_KEY - -# List available commands -scope3 list-tools - -# Use dynamic commands -scope3 brand-agent list -scope3 campaign create --name "My Campaign" - -# Output formats -scope3 media-product list --format json -scope3 media-product list --format list -scope3 media-product list --format table # default - -# Environment switching -scope3 --environment staging brand-agent list - -# Debug mode -scope3 --debug campaign get --campaignId 123 -``` - -## Documentation - -For full documentation, see [@scope3/agentic-client](https://www.npmjs.com/package/@scope3/agentic-client) - -## License - -MIT diff --git a/packages/scope3-cli/cli.js b/packages/scope3-cli/cli.js deleted file mode 100755 index c29d1f0..0000000 --- a/packages/scope3-cli/cli.js +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env node -// Thin wrapper that re-exports the CLI from @scope3/agentic-client -require('@scope3/agentic-client/dist/cli.js'); diff --git a/packages/scope3-cli/package.json b/packages/scope3-cli/package.json deleted file mode 100644 index c28c341..0000000 --- a/packages/scope3-cli/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "scope3", - "version": "1.0.5", - "description": "Scope3 Agentic CLI - Command-line interface for the Scope3 Agentic API", - "bin": { - "scope3": "./cli.js" - }, - "files": [ - "cli.js", - "README.md" - ], - "keywords": [ - "scope3", - "agentic", - "cli", - "advertising", - "adtech" - ], - "author": "Scope3", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/scope3data/agentic-client.git", - "directory": "packages/scope3-cli" - }, - "homepage": "https://github.com/scope3data/agentic-client#readme", - "bugs": { - "url": "https://github.com/scope3data/agentic-client/issues" - }, - "dependencies": { - "@scope3/agentic-client": "^1.0.5" - }, - "publishConfig": { - "access": "public" - } -} From 2d6e008eb4d2b0aa1f8642900ddeb290d50f8b04 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 8 Nov 2025 15:44:16 -0500 Subject: [PATCH 7/8] feat: Add support for environment-specific API keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users can now configure separate API keys for production and staging environments, making it easier to work with both simultaneously. Changes: - Added productionApiKey and stagingApiKey to config - CLI automatically selects correct key based on --environment flag - Falls back to legacy single apiKey for backwards compatibility - Updated config commands to support new keys - Updated loadConfig to properly load all config fields Usage: scope3 config set productionApiKey scope3 config set stagingApiKey scope3 --environment staging brand-agent list scope3 --environment production brand-agent list Benefits: - No need to swap API keys when switching environments - Can set default environment in config - Legacy single apiKey still works for backwards compatibility Testing: - Verified staging key works and caught API bug (no structuredContent) - Verified production key works correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/cli.ts | 52 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 5bfed39..e913d6e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -16,7 +16,9 @@ const TOOLS_CACHE_FILE = path.join(CONFIG_DIR, 'tools-cache.json'); const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours interface CliConfig { - apiKey?: string; + apiKey?: string; // Legacy single API key + productionApiKey?: string; // Production-specific API key + stagingApiKey?: string; // Staging-specific API key environment?: 'production' | 'staging'; baseUrl?: string; } @@ -45,6 +47,8 @@ function loadConfig(): CliConfig { try { const fileConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')); config.apiKey = fileConfig.apiKey; + config.productionApiKey = fileConfig.productionApiKey; + config.stagingApiKey = fileConfig.stagingApiKey; config.environment = fileConfig.environment; config.baseUrl = fileConfig.baseUrl; } catch (error) { @@ -382,19 +386,39 @@ function createClient( ): Scope3AgenticClient { const config = loadConfig(); - const finalApiKey = apiKey || config.apiKey; + // Determine final environment + const finalEnvironment = environment || config.environment || 'production'; + + // Select API key based on priority: + // 1. Explicitly passed apiKey (--api-key flag) + // 2. Environment-specific key from config (productionApiKey/stagingApiKey) + // 3. Legacy single apiKey from config + let finalApiKey = apiKey; + if (!finalApiKey) { + if (finalEnvironment === 'staging' && config.stagingApiKey) { + finalApiKey = config.stagingApiKey; + } else if (finalEnvironment === 'production' && config.productionApiKey) { + finalApiKey = config.productionApiKey; + } else { + finalApiKey = config.apiKey; // Fallback to legacy key + } + } + if (!finalApiKey) { console.error(chalk.red('Error: API key is required')); console.log('Set it via:'); console.log(' - Environment variable: export SCOPE3_API_KEY=your_key'); console.log(' - Config command: scope3 config set apiKey your_key'); + console.log(' - Or use environment-specific keys:'); + console.log(' scope3 config set productionApiKey your_production_key'); + console.log(' scope3 config set stagingApiKey your_staging_key'); console.log(' - Flag: --api-key your_key'); process.exit(1); } return new Scope3AgenticClient({ apiKey: finalApiKey, - environment: environment || config.environment, + environment: finalEnvironment, baseUrl: baseUrl || config.baseUrl, debug: debug || false, }); @@ -475,12 +499,19 @@ const configCmd = program.command('config').description('Manage CLI configuratio configCmd .command('set') .description('Set configuration value') - .argument('', 'Configuration key (apiKey, environment, or baseUrl)') + .argument( + '', + 'Configuration key (apiKey, productionApiKey, stagingApiKey, environment, or baseUrl)' + ) .argument('', 'Configuration value') .action((key: string, value: string) => { const config = loadConfig(); if (key === 'apiKey') { config.apiKey = value; + } else if (key === 'productionApiKey') { + config.productionApiKey = value; + } else if (key === 'stagingApiKey') { + config.stagingApiKey = value; } else if (key === 'environment') { if (value !== 'production' && value !== 'staging') { console.error(chalk.red(`Error: Invalid environment: ${value}`)); @@ -492,7 +523,7 @@ configCmd config.baseUrl = value; } else { console.error(chalk.red(`Error: Unknown config key: ${key}`)); - console.log('Valid keys: apiKey, environment, baseUrl'); + console.log('Valid keys: apiKey, productionApiKey, stagingApiKey, environment, baseUrl'); process.exit(1); } saveConfig(config); @@ -501,7 +532,10 @@ configCmd configCmd .command('get') .description('Get configuration value') - .argument('[key]', 'Configuration key (apiKey or baseUrl). If omitted, shows all config') + .argument( + '[key]', + 'Configuration key (apiKey, productionApiKey, stagingApiKey, environment, or baseUrl). If omitted, shows all config' + ) .action((key?: string) => { const config = loadConfig(); if (!key) { @@ -510,6 +544,12 @@ configCmd if (safeConfig.apiKey) { safeConfig.apiKey = safeConfig.apiKey.substring(0, 8) + '...[REDACTED]'; } + if (safeConfig.productionApiKey) { + safeConfig.productionApiKey = safeConfig.productionApiKey.substring(0, 8) + '...[REDACTED]'; + } + if (safeConfig.stagingApiKey) { + safeConfig.stagingApiKey = safeConfig.stagingApiKey.substring(0, 8) + '...[REDACTED]'; + } console.log(JSON.stringify(safeConfig, null, 2)); } else if (key in config) { console.log(config[key as keyof CliConfig]); From b7e883fb3a811c8527684a8562451b232b3e2039 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 8 Nov 2025 15:50:23 -0500 Subject: [PATCH 8/8] feat: Improve error messages for missing structuredContent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made error messages clearer and less verbose when the API doesn't return structured data as required by the specification. Changes: - Simplified error message to be user-friendly - Removed redundant JSON logger output (only shows in --debug mode) - Only show stack trace when --debug flag is used - Updated all tests to match new error messages Before: {"message":"MCP API VIOLATION: Missing structuredContent",...} {"message":"Tool execution failed",...} Error: MCP API returned response without structuredContent... After: Error: API Error: Missing structured data for "brand_agent_list" API returned text: "No brand agents found for BOK'a Cola." The API should return structured JSON data but returned only text. This is an API bug that needs to be fixed upstream. With --debug flag: Shows full JSON logs + stack trace for debugging Benefits: - Much easier to understand what went wrong - Clear indication it's an API bug, not a CLI issue - Less cluttered output in normal mode - Debug mode still provides full details when needed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/__tests__/client-mcp.test.ts | 16 ++++++------- src/cli.ts | 12 ++++++++-- src/client.ts | 41 +++++++++++++++++++------------- 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/src/__tests__/client-mcp.test.ts b/src/__tests__/client-mcp.test.ts index a0bc4ff..498dad4 100644 --- a/src/__tests__/client-mcp.test.ts +++ b/src/__tests__/client-mcp.test.ts @@ -173,10 +173,10 @@ describe('Scope3Client MCP Protocol', () => { }); await expect(client['callTool']('test_tool', {})).rejects.toThrow( - 'MCP API returned response without structuredContent' + 'API Error: Missing structured data' ); await expect(client['callTool']('test_tool', {})).rejects.toThrow( - 'This violates the Scope3 API specification' + 'This is an API bug that needs to be fixed upstream' ); }); @@ -187,7 +187,7 @@ describe('Scope3Client MCP Protocol', () => { }); await expect(client['callTool']('test_tool', {})).rejects.toThrow( - 'MCP API returned response without structuredContent' + 'API Error: Missing structured data' ); }); @@ -198,7 +198,7 @@ describe('Scope3Client MCP Protocol', () => { }); await expect(client['callTool']('test_tool', {})).rejects.toThrow( - 'MCP API returned response without structuredContent' + 'API Error: Missing structured data' ); }); @@ -208,7 +208,7 @@ describe('Scope3Client MCP Protocol', () => { }); await expect(client['callTool']('test_tool', {})).rejects.toThrow('test_tool'); - await expect(client['callTool']('test_tool', {})).rejects.toThrow('Debug info'); + await expect(client['callTool']('test_tool', {})).rejects.toThrow('API Error'); }); }); @@ -219,7 +219,7 @@ describe('Scope3Client MCP Protocol', () => { }); await expect(client['callTool']('test_tool', {})).rejects.toThrow( - 'MCP API returned response without structuredContent' + 'API Error: Missing structured data' ); }); @@ -229,7 +229,7 @@ describe('Scope3Client MCP Protocol', () => { }); await expect(client['callTool']('test_tool', {})).rejects.toThrow( - 'MCP API returned response without structuredContent' + 'API Error: Missing structured data' ); }); @@ -291,7 +291,7 @@ describe('Scope3Client MCP Protocol', () => { // Should throw before storing debug info await expect(client['callTool']('test_tool', {})).rejects.toThrow( - 'MCP API returned response without structuredContent' + 'API Error: Missing structured data' ); }); diff --git a/src/cli.ts b/src/cli.ts index e913d6e..72b5997 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -729,9 +729,17 @@ async function setupDynamicCommands() { } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); const errorStack = error instanceof Error ? error.stack : undefined; - logger.error('Tool execution failed', error, { toolName: tool.name }); + + // Only log to logger in debug mode + if (globalOpts.debug) { + logger.error('Tool execution failed', error, { toolName: tool.name }); + } + console.error(chalk.red('Error:'), errorMessage); - if (errorStack && process.env.DEBUG) { + + // Show stack trace only in debug mode + if (errorStack && globalOpts.debug) { + console.error(chalk.gray('\nStack trace:')); console.error(chalk.gray(errorStack)); } process.exit(1); diff --git a/src/client.ts b/src/client.ts index beb649d..4422d93 100644 --- a/src/client.ts +++ b/src/client.ts @@ -175,24 +175,33 @@ export class Scope3Client { // This helps catch API bugs that need upstream fixes const content = result.content as Array<{ type: string; text?: string }> | undefined; const firstContent = content && content.length > 0 ? content[0] : null; - const errorDetails = { - toolName, - hasContent: Boolean(content), - contentType: firstContent?.type, - textPreview: - firstContent?.type === 'text' && firstContent.text - ? firstContent.text.substring(0, 200) - : undefined, - }; + const textPreview = + firstContent?.type === 'text' && firstContent.text + ? firstContent.text.substring(0, 200) + : undefined; - logger.error('MCP API VIOLATION: Missing structuredContent', errorDetails); + // Only log detailed info in debug mode + if (this.debug) { + const errorDetails = { + toolName, + hasContent: Boolean(content), + contentType: firstContent?.type, + textPreview, + }; + logger.error('MCP API VIOLATION: Missing structuredContent', errorDetails); + } - throw new Error( - `MCP API returned response without structuredContent for tool "${toolName}". ` + - `This violates the Scope3 API specification. ` + - `The API must be fixed to include structuredContent in all responses. ` + - `Debug info: ${JSON.stringify(errorDetails)}` - ); + // User-friendly error message + const errorMessage = [ + `API Error: Missing structured data for "${toolName}"`, + textPreview ? `\nAPI returned text: "${textPreview}"` : '', + '\n\nThe API should return structured JSON data but returned only text. ', + 'This is an API bug that needs to be fixed upstream.', + ] + .filter(Boolean) + .join(''); + + throw new Error(errorMessage); } protected getClient(): Client {