// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2025 Pierre Fitness Intelligence
// ABOUTME: Comprehensive response shape validation for ALL 47 MCP tools
// ABOUTME: Ensures response formats are stable and match documented schemas
const { ensureServerRunning } = require('../helpers/server');
const { MockMCPClient } = require('../helpers/mock-client');
const { MCPMessages, TestConfig } = require('../helpers/fixtures');
const { generateTestToken } = require('../helpers/token-generator');
const { clearKeychainTokens } = require('../helpers/keychain-cleanup');
const path = require('path');
describe('All Tools Response Validation', () => {
let serverHandle;
let bridgeClient;
const bridgePath = path.join(__dirname, '../../dist/cli.js');
const serverUrl = `http://localhost:${TestConfig.defaultServerPort}`;
beforeAll(async () => {
serverHandle = await ensureServerRunning({
port: TestConfig.defaultServerPort,
database: TestConfig.testDatabase,
encryptionKey: TestConfig.testEncryptionKey
});
}, 60000);
beforeEach(async () => {
await clearKeychainTokens();
});
afterEach(async () => {
if (bridgeClient) {
await bridgeClient.stop();
bridgeClient = null;
}
});
afterAll(async () => {
if (serverHandle?.cleanup) {
await serverHandle.cleanup();
}
});
// Helper to verify MCP response format
const verifyMcpResponseFormat = (response) => {
// All MCP responses should have jsonrpc version
expect(response.jsonrpc).toBe('2.0');
// Must have either result or error
expect(response.result !== undefined || response.error !== undefined).toBe(true);
if (response.error) {
// Error responses must have code and message
expect(response.error.code).toBeDefined();
expect(response.error.message).toBeDefined();
expect(typeof response.error.message).toBe('string');
}
if (response.result) {
// Success responses should have content array (MCP protocol)
// Some responses return the content directly
expect(response.result).toBeDefined();
}
};
// ============================================================================
// Tools List Verification
// ============================================================================
test('tools/list returns all expected tools', async () => {
const testToken = generateTestToken('list-test', 'list@example.com', 3600);
bridgeClient = new MockMCPClient('node', [
bridgePath,
'--server', serverUrl,
'--token', testToken.access_token
]);
await bridgeClient.start();
await bridgeClient.send(MCPMessages.initialize);
await new Promise(resolve => setTimeout(resolve, 2000));
const response = await bridgeClient.send(MCPMessages.toolsList);
verifyMcpResponseFormat(response);
expect(response.result).toBeDefined();
expect(response.result.tools).toBeDefined();
expect(Array.isArray(response.result.tools)).toBe(true);
const toolNames = response.result.tools.map(t => t.name);
// Core tools (must exist)
const coreTools = [
'get_activities',
'get_athlete',
'get_stats',
'get_activity_intelligence'
];
// Connection tools
// Note: connect_to_pierre removed - SDK bridge handles authentication locally via RFC 8414 discovery
const connectionTools = [
'connect_provider',
'get_connection_status',
'disconnect_provider'
];
// Analytics tools
const analyticsTools = [
'analyze_activity',
'calculate_metrics',
'analyze_performance_trends',
'compare_activities',
'detect_patterns',
'generate_recommendations',
'calculate_fitness_score',
'predict_performance',
'analyze_training_load'
];
// Goal tools
const goalTools = [
'set_goal',
'track_progress',
'suggest_goals',
'analyze_goal_feasibility'
];
// Configuration tools
const configTools = [
'get_configuration_catalog',
'get_configuration_profiles',
'get_user_configuration',
'update_user_configuration',
'calculate_personalized_zones',
'validate_configuration',
'get_fitness_config',
'set_fitness_config',
'list_fitness_configs',
'delete_fitness_config'
];
// Sleep & Recovery tools
const sleepTools = [
'analyze_sleep_quality',
'calculate_recovery_score',
'suggest_rest_day',
'track_sleep_trends',
'optimize_sleep_schedule'
];
// Nutrition tools
const nutritionTools = [
'calculate_daily_nutrition',
'get_nutrient_timing',
'search_food',
'get_food_details',
'analyze_meal_nutrition'
];
// Recipe tools
const recipeTools = [
'get_recipe_constraints',
'validate_recipe',
'save_recipe',
'list_recipes',
'get_recipe',
'delete_recipe',
'search_recipes'
];
// Verify all tool categories
const allExpectedTools = [
...coreTools,
...connectionTools,
...analyticsTools,
...goalTools,
...configTools,
...sleepTools,
...nutritionTools,
...recipeTools
];
for (const tool of allExpectedTools) {
expect(toolNames).toContain(tool);
}
// Log count for visibility
console.log(`✅ Verified ${allExpectedTools.length} expected tools are present`);
console.log(` Total tools in response: ${toolNames.length}`);
}, 30000);
// ============================================================================
// Response Shape Tests for Non-Auth Tools
// ============================================================================
describe('Non-Authentication Tools Response Shapes', () => {
let testToken;
beforeEach(async () => {
testToken = generateTestToken('shape-test', 'shape@example.com', 3600);
bridgeClient = new MockMCPClient('node', [
bridgePath,
'--server', serverUrl,
'--token', testToken.access_token
]);
await bridgeClient.start();
await bridgeClient.send(MCPMessages.initialize);
await new Promise(resolve => setTimeout(resolve, 2000));
});
test('get_configuration_catalog returns catalog shape', async () => {
const response = await bridgeClient.send({
jsonrpc: '2.0',
id: 1,
method: 'tools/call',
params: {
name: 'get_configuration_catalog',
arguments: {}
}
}, 10000);
verifyMcpResponseFormat(response);
// This tool should succeed without auth
if (response.result) {
console.log('✅ get_configuration_catalog: Response received');
}
}, 20000);
test('get_configuration_profiles returns profiles shape', async () => {
const response = await bridgeClient.send({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'get_configuration_profiles',
arguments: {}
}
}, 10000);
verifyMcpResponseFormat(response);
if (response.result) {
console.log('✅ get_configuration_profiles: Response received');
}
}, 20000);
test('get_recipe_constraints returns constraints shape', async () => {
const response = await bridgeClient.send({
jsonrpc: '2.0',
id: 3,
method: 'tools/call',
params: {
name: 'get_recipe_constraints',
arguments: {
meal_timing: 'post_training',
calories: 500
}
}
}, 10000);
verifyMcpResponseFormat(response);
if (response.result) {
// Verify expected fields in recipe constraints
const content = response.result.content || response.result;
expect(content).toBeDefined();
console.log('✅ get_recipe_constraints: Constraints returned');
}
}, 20000);
test('calculate_daily_nutrition returns nutrition shape', async () => {
const response = await bridgeClient.send({
jsonrpc: '2.0',
id: 4,
method: 'tools/call',
params: {
name: 'calculate_daily_nutrition',
arguments: {
weight_kg: 70,
height_cm: 175,
age: 30,
gender: 'male',
activity_level: 'moderately_active',
training_goal: 'maintenance'
}
}
}, 10000);
verifyMcpResponseFormat(response);
if (response.result) {
console.log('✅ calculate_daily_nutrition: Nutrition calculated');
}
}, 20000);
test('get_nutrient_timing returns timing shape', async () => {
const response = await bridgeClient.send({
jsonrpc: '2.0',
id: 5,
method: 'tools/call',
params: {
name: 'get_nutrient_timing',
arguments: {
weight_kg: 70,
daily_protein_g: 140,
workout_intensity: 'moderate'
}
}
}, 10000);
verifyMcpResponseFormat(response);
if (response.result) {
console.log('✅ get_nutrient_timing: Timing recommendations returned');
}
}, 20000);
test('list_recipes returns empty array for new user', async () => {
const response = await bridgeClient.send({
jsonrpc: '2.0',
id: 6,
method: 'tools/call',
params: {
name: 'list_recipes',
arguments: {}
}
}, 10000);
verifyMcpResponseFormat(response);
if (response.result) {
console.log('✅ list_recipes: List returned');
}
}, 20000);
test('search_recipes returns results shape', async () => {
const response = await bridgeClient.send({
jsonrpc: '2.0',
id: 7,
method: 'tools/call',
params: {
name: 'search_recipes',
arguments: {
query: 'chicken'
}
}
}, 10000);
verifyMcpResponseFormat(response);
if (response.result) {
console.log('✅ search_recipes: Search results returned');
}
}, 20000);
test('validate_configuration returns validation shape', async () => {
const response = await bridgeClient.send({
jsonrpc: '2.0',
id: 8,
method: 'tools/call',
params: {
name: 'validate_configuration',
arguments: {
configuration: {
max_heart_rate: 185,
resting_heart_rate: 55
}
}
}
}, 10000);
verifyMcpResponseFormat(response);
if (response.result) {
console.log('✅ validate_configuration: Validation result returned');
} else if (response.error) {
// May require specific config format
console.log('⚠️ validate_configuration: Expected validation error');
}
}, 20000);
});
// ============================================================================
// Error Response Shape Tests
// ============================================================================
describe('Error Response Shapes', () => {
let testToken;
beforeEach(async () => {
testToken = generateTestToken('error-test', 'error@example.com', 3600);
bridgeClient = new MockMCPClient('node', [
bridgePath,
'--server', serverUrl,
'--token', testToken.access_token
]);
await bridgeClient.start();
await bridgeClient.send(MCPMessages.initialize);
await new Promise(resolve => setTimeout(resolve, 2000));
});
test('Provider-required tools return auth errors correctly', async () => {
const providerTools = [
{ name: 'get_activities', args: { activity_provider: 'strava' } },
{ name: 'get_athlete', args: { activity_provider: 'strava' } },
{ name: 'analyze_activity', args: { activity_provider: 'strava', activity_id: '123' } }
];
for (const { name, args } of providerTools) {
const response = await bridgeClient.send({
jsonrpc: '2.0',
id: 100,
method: 'tools/call',
params: { name, arguments: args }
}, 10000);
verifyMcpResponseFormat(response);
// Should fail with auth error, not parsing error
if (response.error) {
expect(response.error.message).not.toMatch(/parse/i);
expect(response.error.message).not.toMatch(/unknown field/i);
console.log(`✅ ${name}: Proper auth error returned`);
} else {
console.log(`✅ ${name}: Unexpected success (may have cached auth)`);
}
}
}, 60000);
test('Missing required parameters returns proper error', async () => {
// Try calling set_goal without required parameters
const response = await bridgeClient.send({
jsonrpc: '2.0',
id: 101,
method: 'tools/call',
params: {
name: 'set_goal',
arguments: {} // Missing required params
}
}, 10000);
verifyMcpResponseFormat(response);
// Should fail with parameter error
if (response.error) {
expect(response.error.message).toBeDefined();
console.log('✅ set_goal with missing params: Proper error returned');
}
}, 20000);
test('Unknown tool returns method not found', async () => {
const response = await bridgeClient.send({
jsonrpc: '2.0',
id: 102,
method: 'tools/call',
params: {
name: 'definitely_not_a_real_tool_xyz',
arguments: {}
}
}, 10000);
verifyMcpResponseFormat(response);
// Server may return error at top level OR in result.isError
const hasError = response.error ||
(response.result && response.result.isError) ||
(response.result && response.result.content && response.result.content[0]?.type === 'text' &&
response.result.content[0]?.text?.toLowerCase().includes('error'));
expect(hasError).toBeTruthy();
console.log('✅ Unknown tool: Error returned correctly');
}, 20000);
test('Invalid parameter types handled gracefully', async () => {
const response = await bridgeClient.send({
jsonrpc: '2.0',
id: 103,
method: 'tools/call',
params: {
name: 'calculate_daily_nutrition',
arguments: {
weight_kg: 'not_a_number', // Should be number
height_cm: 175,
age: 30,
gender: 'male',
activity_level: 'moderately_active',
training_goal: 'maintenance'
}
}
}, 10000);
verifyMcpResponseFormat(response);
// Should fail gracefully
if (response.error || (response.result && !response.result.success)) {
console.log('✅ Invalid parameter type: Handled gracefully');
}
}, 20000);
});
// ============================================================================
// Tool Schema Consistency Tests
// ============================================================================
describe('Tool Schema Consistency', () => {
let testToken;
beforeEach(async () => {
testToken = generateTestToken('schema-test', 'schema@example.com', 3600);
bridgeClient = new MockMCPClient('node', [
bridgePath,
'--server', serverUrl,
'--token', testToken.access_token
]);
await bridgeClient.start();
await bridgeClient.send(MCPMessages.initialize);
await new Promise(resolve => setTimeout(resolve, 2000));
});
test('All tools have valid schemas', async () => {
const response = await bridgeClient.send(MCPMessages.toolsList);
expect(response.result.tools).toBeDefined();
for (const tool of response.result.tools) {
// Each tool must have required fields
expect(tool.name).toBeDefined();
expect(typeof tool.name).toBe('string');
expect(tool.name.length).toBeGreaterThan(0);
expect(tool.description).toBeDefined();
expect(typeof tool.description).toBe('string');
expect(tool.description.length).toBeGreaterThan(0);
// Input schema must be valid JSON Schema
expect(tool.inputSchema).toBeDefined();
expect(tool.inputSchema.type).toBe('object');
// Properties may be undefined for tools with no parameters, or an empty object
if (tool.inputSchema.properties !== undefined) {
expect(typeof tool.inputSchema.properties).toBe('object');
}
}
console.log(`✅ All ${response.result.tools.length} tools have valid schemas`);
}, 30000);
test('Tool names follow naming convention', async () => {
const response = await bridgeClient.send(MCPMessages.toolsList);
const invalidNames = [];
for (const tool of response.result.tools) {
// Names should be snake_case
if (!/^[a-z][a-z0-9_]*$/.test(tool.name)) {
invalidNames.push(tool.name);
}
}
expect(invalidNames.length).toBe(0);
console.log('✅ All tool names follow snake_case convention');
}, 30000);
});
// ============================================================================
// Cross-Provider Tool Tests
// ============================================================================
describe('Cross-Provider Tool Compatibility', () => {
let testToken;
beforeEach(async () => {
testToken = generateTestToken('provider-test', 'provider@example.com', 3600);
bridgeClient = new MockMCPClient('node', [
bridgePath,
'--server', serverUrl,
'--token', testToken.access_token
]);
await bridgeClient.start();
await bridgeClient.send(MCPMessages.initialize);
await new Promise(resolve => setTimeout(resolve, 2000));
});
const crossProviderTools = [
'get_activities',
'get_athlete',
'get_stats',
'analyze_activity',
'analyze_training_load',
'calculate_recovery_score'
];
test.each(crossProviderTools)(
'%s accepts activity_provider parameter',
async (toolName) => {
const response = await bridgeClient.send({
jsonrpc: '2.0',
id: 200,
method: 'tools/call',
params: {
name: toolName,
arguments: {
activity_provider: 'strava'
}
}
}, 10000);
verifyMcpResponseFormat(response);
// Should NOT fail with "unknown field: activity_provider"
if (response.error) {
expect(response.error.message).not.toMatch(/unknown field.*activity_provider/i);
expect(response.error.message).not.toMatch(/unknown field.*provider/i);
}
console.log(`✅ ${toolName}: Accepts activity_provider parameter`);
},
20000
);
});
});