/**
* Population Tool
*
* Application layer tool for fetching US Census population data.
* Orchestrates domain logic and infrastructure services.
* Uses Zod for runtime type validation and schema generation.
*/
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { ToolDefinition, ToolResult } from '../../domain/types.js';
import {
buildStateQuery,
parseCensusResponse,
formatCensusData,
createSuccessResult,
} from '../../domain/census-service.js';
import { ICensusApiClient } from '../../infrastructure/clients/census-api-client.js';
import { ILogger } from '../../infrastructure/logging/logger.js';
/**
* Tool name constant
*/
export const POPULATION_TOOL_NAME = 'get-population';
/**
* Zod schema for population tool input
* Provides runtime validation and type inference
*/
export const PopulationToolInputSchema = z.object({
states: z
.array(z.number().int().min(0).max(56))
.min(1)
.describe('Array of FIPS state codes (e.g., [1, 6, 36]). Use [0] for all states.'),
});
/**
* Infer TypeScript type from Zod schema
*/
export type PopulationToolInput = z.infer<typeof PopulationToolInputSchema>;
/**
* Get tool definition for MCP server
* Uses Zod schema converted to JSON Schema
*
* @returns Tool definition with schema generated from Zod
*/
export function getPopulationToolDefinition(): ToolDefinition {
// Convert Zod schema to JSON Schema
const jsonSchema = zodToJsonSchema(PopulationToolInputSchema, {
name: 'PopulationToolInput',
$refStrategy: 'none',
});
// Extract the actual object schema from the definitions
// zodToJsonSchema returns a root schema with $ref and definitions
// MCP expects the inputSchema to be the actual object schema
const actualSchema = (jsonSchema as any).definitions?.PopulationToolInput || jsonSchema;
return {
name: POPULATION_TOOL_NAME,
description:
'Get population data for US states by FIPS state code. Pass an array of state codes (e.g., [1, 6, 36] for Alabama, California, New York) or use [0] to get all states.',
inputSchema: actualSchema as ToolDefinition['inputSchema'],
};
}
/**
* Population tool handler
*/
export class PopulationTool {
private readonly censusClient: ICensusApiClient;
private readonly logger: ILogger;
constructor(censusClient: ICensusApiClient, logger: ILogger) {
this.censusClient = censusClient;
this.logger = logger;
}
/**
* Execute the population tool
* Uses Zod for runtime validation
*
* @param args - Tool arguments (validated against Zod schema)
* @returns Tool result with formatted data
*/
async execute(args: unknown): Promise<ToolResult> {
this.logger.info('Executing population tool', { args });
// Validate and parse using Zod schema
const parseResult = PopulationToolInputSchema.safeParse(args);
if (!parseResult.success) {
const errorMessages = parseResult.error.errors
.map((err) => `${err.path.join('.')}: ${err.message}`)
.join(', ');
this.logger.warn('Invalid population request', {
error: errorMessages,
args,
zodErrors: parseResult.error.errors,
});
return this.createErrorToolResult(
`Invalid request parameters: ${errorMessages}`
);
}
// Type-safe validated input
const validatedInput: PopulationToolInput = parseResult.data;
// Build query and fetch data
const stateQuery = buildStateQuery(validatedInput.states);
this.logger.debug('Fetching population data', { stateQuery });
const rawData = await this.censusClient.getPopulationData(stateQuery);
if (!rawData) {
this.logger.error('Failed to retrieve census data', undefined, {
stateQuery,
});
return this.createErrorToolResult('Failed to retrieve census data');
}
if (rawData.length === 0) {
this.logger.warn('No census data available', { stateQuery });
return this.createErrorToolResult('No census data available');
}
// Parse and format data
try {
const parsedData = parseCensusResponse(rawData);
const formattedOutput = formatCensusData(parsedData, rawData);
const result = createSuccessResult(parsedData, formattedOutput);
this.logger.info('Population data retrieved successfully', {
recordCount: parsedData.length,
stateQuery,
states: validatedInput.states,
});
return {
content: [
{
type: 'text',
text: result.formattedOutput || '',
},
],
};
} catch (error) {
this.logger.error(
'Failed to parse census data',
error instanceof Error ? error : new Error(String(error)),
{ stateQuery }
);
return this.createErrorToolResult('Failed to parse census data');
}
}
/**
* Create error tool result
*
* @param message - Error message
* @returns Tool result with error
*/
private createErrorToolResult(message: string): ToolResult {
return {
content: [
{
type: 'text',
text: message,
},
],
};
}
}
/**
* Factory function to create population tool
*/
export function createPopulationTool(
censusClient: ICensusApiClient,
logger: ILogger
): PopulationTool {
return new PopulationTool(censusClient, logger);
}