All files / src/tools getVotingRecords.ts

80% Statements 16/20
66.66% Branches 4/6
66.66% Functions 2/3
83.33% Lines 15/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184                                                                                                                            27x 27x   4x 4x 4x                     23x   23x                       23x     19x 19x     19x                 27x   4x                   4x                         4x                                                                                                    
/**
 * MCP Tool: get_voting_records
 * 
 * Retrieve voting records from European Parliament plenary sessions
 * 
 * **Intelligence Perspective:** Core intelligence product for voting pattern analysis,
 * political group cohesion measurement, cross-party alliance detection, and MEP
 * loyalty/independence scoring through structured analytic techniques.
 * 
 * **Business Perspective:** High-value data product for political risk assessment firms,
 * policy analysis consultancies, and corporate government affairs departments.
 * 
 * **Marketing Perspective:** Most compelling data for data journalism partnerships,
 * academic research collaborations, and transparency advocacy organizations.
 * 
 * ISMS Policy: SC-002 (Input Validation), AC-003 (Least Privilege)
 */
 
import { GetVotingRecordsSchema, VotingRecordSchema, PaginatedResponseSchema } from '../schemas/europeanParliament.js';
import { epClient } from '../clients/europeanParliamentClient.js';
import { buildToolResponse } from './shared/responseBuilder.js';
import { buildApiParams } from './shared/paramBuilder.js';
import { ToolError } from './shared/errors.js';
import { z } from 'zod';
import type { ToolResult } from './shared/types.js';
 
/**
 * Handles the get_voting_records MCP tool request.
 *
 * Retrieves voting records from European Parliament plenary sessions, supporting
 * filtering by session, topic, and date range. Returns aggregate vote tallies
 * (for/against/abstain) and final results. The `mepId` parameter is accepted but
 * has no effect — the EP API only provides aggregate vote counts, not individual
 * MEP positions.
 *
 * @param args - Raw tool arguments, validated against {@link GetVotingRecordsSchema}
 * @returns MCP tool result containing a paginated list of voting records with vote counts and results
 * @throws - If `args` fails schema validation (e.g., missing required fields or invalid format)
 * - If the European Parliament API is unreachable or returns an error response
 *
 * @example
 * ```typescript
 * const result = await handleGetVotingRecords({
 *   sessionId: 'PLENARY-2024-01',
 *   topic: 'Climate Change',
 *   limit: 20
 * });
 * // Returns voting records for the January 2024 plenary session on climate topics
 * ```
 *
 * @security - Input is validated with Zod before any API call.
 * - Personal data in responses is minimised per GDPR Article 5(1)(c).
 * - All requests are rate-limited and audit-logged per ISMS Policy AU-002.
 * @since 0.8.0
 * @see {@link getVotingRecordsToolMetadata} for MCP schema registration
 * @see [handleGetMeetingDecisions](../../getMeetingDecisions/functions/handleGetMeetingDecisions.md) for retrieving decisions linked to a specific sitting
 */
export async function handleGetVotingRecords(
  args: unknown
): Promise<ToolResult> {
  // Validate input — ZodErrors here are client mistakes (non-retryable)
  let params: ReturnType<typeof GetVotingRecordsSchema.parse>;
  try {
    params = GetVotingRecordsSchema.parse(args);
  } catch (error: unknown) {
    Eif (error instanceof z.ZodError) {
      const fieldErrors = error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join('; ');
      throw new ToolError({
        toolName: 'get_voting_records',
        operation: 'validateInput',
        message: `Invalid parameters: ${fieldErrors}`,
        isRetryable: false,
        cause: error,
      });
    }
    throw error;
  }
 
  try {
    // Fetch voting records from EP API (only pass defined properties)
    const apiParams = {
      limit: params.limit,
      offset: params.offset,
      ...buildApiParams(params, [
        { from: 'sessionId', to: 'sessionId' },
        { from: 'mepId', to: 'mepId' },
        { from: 'topic', to: 'topic' },
        { from: 'dateFrom', to: 'dateFrom' },
        { from: 'dateTo', to: 'dateTo' },
      ]),
    };
    
    const result = await epClient.getVotingRecords(apiParams as Parameters<typeof epClient.getVotingRecords>[0]);
    
    // Validate output
    const outputSchema = PaginatedResponseSchema(VotingRecordSchema);
    const validated = outputSchema.parse(result);
 
    // Add deprecation warning when mepId is provided (EP API limitation)
    const responsePayload = {
      ...validated,
      _warning:
        params.mepId !== undefined
          ? 'The mepId parameter is not supported by the EP API and has no effect on results. ' +
            'The EP votes endpoint only returns aggregate vote counts, not per-MEP positions.'
          : undefined
    };
    
    return buildToolResponse(responsePayload);
  } catch (error: unknown) {
    Iif (error instanceof z.ZodError) {
      const fieldErrors = error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join('; ');
      throw new ToolError({
        toolName: 'get_voting_records',
        operation: 'validateOutput',
        message: `Unexpected EP API response format: ${fieldErrors}`,
        isRetryable: false,
        cause: error,
      });
    }
    throw new ToolError({
      toolName: 'get_voting_records',
      operation: 'fetchData',
      message: 'Failed to retrieve voting records',
      isRetryable: true,
      cause: error,
    });
  }
}
 
/**
 * Tool metadata for MCP registration
 */
export const getVotingRecordsToolMetadata = {
  name: 'get_voting_records',
  description: 'Retrieve voting records from European Parliament plenary sessions. Filter by session, topic, or date range. Returns aggregate vote counts (for/against/abstain) and final result. The mepId parameter is accepted but has no effect — the EP API only provides aggregate vote tallies, not individual MEP positions. NOTE: The EP publishes roll-call voting data with a delay of several weeks, so queries for the most recent 1-2 months may return empty results — this is expected EP API behavior, not an error.',
  inputSchema: {
    type: 'object' as const,
    properties: {
      sessionId: {
        type: 'string',
        description: 'Plenary session identifier',
        minLength: 1,
        maxLength: 100
      },
      mepId: {
        type: 'string',
        description: 'MEP identifier (accepted but ignored — the EP API only provides aggregate vote tallies, not individual MEP positions)',
        minLength: 1,
        maxLength: 100
      },
      topic: {
        type: 'string',
        description: 'Vote topic or keyword to search',
        minLength: 1,
        maxLength: 200
      },
      dateFrom: {
        type: 'string',
        description: 'Start date filter (YYYY-MM-DD format)',
        pattern: '^\\d{4}-\\d{2}-\\d{2}$'
      },
      dateTo: {
        type: 'string',
        description: 'End date filter (YYYY-MM-DD format)',
        pattern: '^\\d{4}-\\d{2}-\\d{2}$'
      },
      limit: {
        type: 'number',
        description: 'Maximum number of results to return (1-100)',
        minimum: 1,
        maximum: 100,
        default: 50
      },
      offset: {
        type: 'number',
        description: 'Pagination offset',
        minimum: 0,
        default: 0
      }
    }
  }
};