Skip to content

Commit 9dc9591

Browse files
feat: add Claude Code compatibility workaround for MCP specification violation
Add XCODEBUILDMCP_CLAUDE_CODE_WORKAROUND environment variable to consolidate multiple content blocks into single text responses. This works around Claude Code''s MCP spec violation where it only shows the first content block, preventing users from seeing test results when stderr warnings are present. Changes: - Add consolidateContentForClaudeCode() utility function in validation.ts - Update build-utils.ts and test-common.ts to apply consolidation - Document new env var in CLAUDE.md - Preserve correct MCP format by default, consolidate only when enabled Fixes issue where test_sim_id_proj returns stderr warnings as errors, preventing access to test results in Claude Code. Co-authored-by: Cameron Cooke <cameroncooke@users.noreply.github.com>
1 parent 01f404d commit 9dc9591

File tree

4 files changed

+66
-10
lines changed

4 files changed

+66
-10
lines changed

CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ XcodeBuildMCP has two modes to manage its extensive toolset, controlled by the `
5252
- **Environment**: `XCODEBUILDMCP_DYNAMIC_TOOLS=true`.
5353
- **Behavior**: Only the `discover_tools` tool is available initially. You can use this tool by providing a natural language task description. The server then uses an LLM call (via MCP Sampling) to identify the most relevant workflow group and dynamically loads only those tools. This conserves context window space.
5454

55+
#### Claude Code Compatibility Workaround
56+
- **Environment**: `XCODEBUILDMCP_CLAUDE_CODE_WORKAROUND=true`.
57+
- **Purpose**: Workaround for Claude Code's MCP specification violation where it only displays the first content block in tool responses.
58+
- **Behavior**: When enabled, multiple content blocks are consolidated into a single text response, separated by `---` dividers. This ensures all information (including test results and stderr warnings) is visible to Claude Code users.
59+
5560
### Core Architecture Layers
5661
1. **MCP Transport**: stdio protocol communication
5762
2. **Plugin Discovery**: Automatic tool AND resource registration system

src/utils/build-utils.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { log } from './logger.js';
2121
import { XcodePlatform, constructDestinationString } from './xcode.js';
2222
import { CommandExecutor } from './command.js';
2323
import { ToolResponse, SharedBuildParams, PlatformBuildOptions } from '../types/common.js';
24-
import { createTextResponse } from './validation.js';
24+
import { createTextResponse, consolidateContentForClaudeCode } from './validation.js';
2525
import {
2626
isXcodemakeEnabled,
2727
isXcodemakeAvailable,
@@ -273,7 +273,7 @@ export async function executeXcodeBuildCommand(
273273
});
274274
}
275275

276-
return errorResponse;
276+
return consolidateContentForClaudeCode(errorResponse);
277277
}
278278

279279
log('info', `✅ ${platformOptions.logPrefix} ${buildAction} succeeded.`);
@@ -347,13 +347,15 @@ When done capturing logs, use: stop_and_get_simulator_log({ logSessionId: 'SESSI
347347
});
348348
}
349349

350-
return successResponse;
350+
return consolidateContentForClaudeCode(successResponse);
351351
} catch (error) {
352352
const errorMessage = error instanceof Error ? error.message : String(error);
353353
log('error', `Error during ${platformOptions.logPrefix} ${buildAction}: ${errorMessage}`);
354-
return createTextResponse(
355-
`Error during ${platformOptions.logPrefix} ${buildAction}: ${errorMessage}`,
356-
true,
354+
return consolidateContentForClaudeCode(
355+
createTextResponse(
356+
`Error during ${platformOptions.logPrefix} ${buildAction}: ${errorMessage}`,
357+
true,
358+
),
357359
);
358360
}
359361
}

src/utils/test-common.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { join } from 'path';
2020
import { log } from './logger.js';
2121
import { XcodePlatform } from './xcode.js';
2222
import { executeXcodeBuildCommand } from './build-utils.js';
23-
import { createTextResponse } from './validation.js';
23+
import { createTextResponse, consolidateContentForClaudeCode } from './validation.js';
2424
import { ToolResponse } from '../types/common.js';
2525
import { CommandExecutor } from './command.js';
2626

@@ -214,7 +214,7 @@ export async function handleTestLogic(
214214
await rm(tempDir, { recursive: true, force: true });
215215

216216
// Return combined result - preserve isError from testResult (test failures should be marked as errors)
217-
return {
217+
const combinedResponse: ToolResponse = {
218218
content: [
219219
...(testResult.content || []),
220220
{
@@ -224,6 +224,9 @@ export async function handleTestLogic(
224224
],
225225
isError: testResult.isError,
226226
};
227+
228+
// Apply Claude Code workaround if enabled
229+
return consolidateContentForClaudeCode(combinedResponse);
227230
} catch (parseError) {
228231
// If parsing fails, return original test result
229232
log('warn', `Failed to parse xcresult bundle: ${parseError}`);
@@ -235,11 +238,13 @@ export async function handleTestLogic(
235238
log('warn', `Failed to clean up temporary directory: ${cleanupError}`);
236239
}
237240

238-
return testResult;
241+
return consolidateContentForClaudeCode(testResult);
239242
}
240243
} catch (error) {
241244
const errorMessage = error instanceof Error ? error.message : String(error);
242245
log('error', `Error during test run: ${errorMessage}`);
243-
return createTextResponse(`Error during test run: ${errorMessage}`, true);
246+
return consolidateContentForClaudeCode(
247+
createTextResponse(`Error during test run: ${errorMessage}`, true),
248+
);
244249
}
245250
}

src/utils/validation.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,5 +207,49 @@ export function validateEnumParam<T>(
207207
return { isValid: true };
208208
}
209209

210+
/**
211+
* Consolidates multiple content blocks into a single text response for Claude Code compatibility
212+
*
213+
* Claude Code violates the MCP specification by only showing the first content block.
214+
* This function provides a workaround by concatenating all text content into a single block.
215+
*
216+
* @param response The original ToolResponse with multiple content blocks
217+
* @returns A new ToolResponse with consolidated content
218+
*/
219+
export function consolidateContentForClaudeCode(response: ToolResponse): ToolResponse {
220+
// Check environment variable to enable/disable this workaround
221+
const shouldConsolidate = process.env.XCODEBUILDMCP_CLAUDE_CODE_WORKAROUND === 'true';
222+
223+
if (!shouldConsolidate || !response.content || response.content.length <= 1) {
224+
return response;
225+
}
226+
227+
// Extract all text content and concatenate with separators
228+
const textParts: string[] = [];
229+
230+
response.content.forEach((item, index) => {
231+
if (item.type === 'text') {
232+
// Add a separator between content blocks (except for the first one)
233+
if (index > 0 && textParts.length > 0) {
234+
textParts.push('\n---\n');
235+
}
236+
textParts.push(item.text);
237+
}
238+
// Note: Image content is not handled in this workaround as it requires special formatting
239+
});
240+
241+
const consolidatedText = textParts.join('');
242+
243+
return {
244+
...response,
245+
content: [
246+
{
247+
type: 'text',
248+
text: consolidatedText,
249+
},
250+
],
251+
};
252+
}
253+
210254
// Export the ToolResponse type for use in other files
211255
export { ToolResponse, ValidationResult };

0 commit comments

Comments
 (0)