# All files / src/tools getLatestVotes.ts

100% Statements 18/18
90% Branches 9/10
100% Functions 5/5
100% Lines 18/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
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200

    7x
    7x

    3x

    4x

    11x

    13x
    13x

    8x
    7x

    8x
    8x

    7x

    1x

    5x
    5x

    4x

    1x

    4x

     /**
     * MCP Tool: get_latest_votes
     *
     * Retrieve the latest plenary votes from European Parliament DOCEO XML documents.
     * This provides more recent data than the standard EP Open Data API, which has
     * a publication delay of several weeks for roll-call voting data.
     *
     * **Data Sources:**
     * - RCV (Roll-Call Votes): Individual MEP positions by political group
     * - VOT (Vote Results): Aggregate tallies with vote outcomes
     *
     * **Intelligence Perspective:** Near-real-time voting intelligence enabling rapid
     * coalition analysis, political group cohesion tracking, and early detection of
     * political shifts within hours of votes being cast.
     *
     * **Business Perspective:** Premium data freshness differentiator — provides
     * vote data days/weeks before the EP Open Data API publishes it.
     *
     * ISMS Policy: SC-002 (Input Validation), AC-003 (Least Privilege)
     */

    import { z } from 'zod';
    import { doceoClient } from '../clients/ep/doceoClient.js';
    import { buildToolResponse } from './shared/responseBuilder.js';
    import { ToolError } from './shared/errors.js';
    import type { ToolResult } from './shared/types.js';

    /** Returns true when str is a real calendar date (rejects e.g. "2026-13-40"). */
    function isValidCalendarDate(str: string): boolean {
      const d = new Date(`${str}T00:00:00Z`);
      return !isNaN(d.getTime()) && d.toISOString().slice(0, 10) === str;
    }

    /** Returns true when str parses to a Monday (UTC). */
    function isMonday(str: string): boolean {
      return new Date(`${str}T00:00:00Z`).getUTCDay() === 1;
    }

    /**
     * Input schema for get_latest_votes tool.
     */
    export const GetLatestVotesSchema = z.object({
      date: z.string()
        .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in YYYY-MM-DD format')
        .refine(isValidCalendarDate, { message: 'date must be a valid calendar date' })
        .optional()
        .describe('Specific date to fetch votes for (YYYY-MM-DD). Mutually exclusive with weekStart. If omitted, fetches the requested or most recent plenary week.'),
      weekStart: z.string()
        .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in YYYY-MM-DD format')
        .refine(isValidCalendarDate, { message: 'weekStart must be a valid calendar date' })
        .refine(isMonday, { message: 'weekStart must be a Monday (the start of the plenary week)' })
        .optional()
        .describe('Monday of a specific plenary week (YYYY-MM-DD). Mutually exclusive with date. Fetches Mon-Thu of that week.'),
      term: z.number()
        .int()
        .min(1)
        .max(15)
        .optional()
        .describe('Parliamentary term number (defaults to 10 for current 2024-2029 term)'),
      includeIndividualVotes: z.boolean()
        .optional()
        .default(true)
        .describe('Include individual MEP vote positions from roll-call data (default: true)'),
      limit: z.number()
        .int()
        .min(1)
        .max(100)
        .default(50)
        .describe('Maximum number of vote records to return'),
      offset: z.number()
        .int()
        .min(0)
        .default(0)
        .describe('Pagination offset'),
    }).strict().refine(
      (data) => data.date === undefined || data.weekStart === undefined,
      {
        message: 'date and weekStart are mutually exclusive; provide only one, or omit both for the most recent plenary week',
        path: ['weekStart'],
      }
    );

    /**
     * Handles the get_latest_votes MCP tool request.
     *
     * Fetches the latest plenary vote data from the EP DOCEO XML system,
     * providing near-real-time access to roll-call votes and vote results.
     * This data source is fresher than the EP Open Data API which has a
     * multi-week publication delay.
     *
     * @param args - Raw tool arguments, validated against GetLatestVotesSchema
     * @returns MCP tool result with latest vote records
     *
     * @example
     * ```typescript
     * // Get votes from the most recent plenary week
     * const result = await handleGetLatestVotes({});
     *
     * // Get votes for a specific date
     * const result = await handleGetLatestVotes({ date: '2026-04-27' });
     *
     * // Get a specific plenary week without individual MEP positions
     * const result = await handleGetLatestVotes({
     *   weekStart: '2026-04-27',
     *   includeIndividualVotes: false
     * });
     * ```
     */
    export async function handleGetLatestVotes(args: unknown): Promise<ToolResult> {
      let params: z.infer<typeof GetLatestVotesSchema>;
      try {
        params = GetLatestVotesSchema.parse(args);
      } catch (error: unknown) {
        if (error instanceof z.ZodError) {
          const fieldErrors = error.issues
            .map(issue => {
              const pathStr = issue.path.length > 0 ? issue.path.join('.') : '(root)';
              return `${pathStr}: ${issue.message}`;
            })
            .join('; ');
          throw new ToolError({
            toolName: 'get_latest_votes',
            operation: 'validateInput',
            message: `Invalid parameters: ${fieldErrors}`,
            isRetryable: false,
            cause: error,
          });
        }
        throw error;
      }

      try {
        const result = await doceoClient.getLatestVotes({
          date: params.date,
          weekStart: params.weekStart,
          term: params.term,
          includeIndividualVotes: params.includeIndividualVotes,
          limit: params.limit,
          offset: params.offset,
        });

        return buildToolResponse(result);
      } catch (error: unknown) {
        throw new ToolError({
          toolName: 'get_latest_votes',
          operation: 'fetchData',
          message: 'Failed to retrieve latest votes from DOCEO',
          isRetryable: true,
          cause: error instanceof Error ? error : undefined,
        });
      }
    }

    /**
     * Tool metadata for MCP registration
     */
    export const getLatestVotesToolMetadata = {
      name: 'get_latest_votes',
      description: 'Retrieve the latest plenary votes from EP DOCEO XML documents. Provides near-real-time access to roll-call votes (individual MEP positions by political group) and vote results (aggregate tallies). This data source is fresher than the EP Open Data API which has a multi-week publication delay. Use for up-to-date political intelligence on voting patterns, coalition analysis, and group cohesion tracking.',
      inputSchema: {
        type: 'object' as const,
        properties: {
          date: {
            type: 'string',
            description: 'Specific date to fetch votes for (YYYY-MM-DD). Mutually exclusive with weekStart. If omitted, fetches the requested or most recent plenary week (Mon-Thu).',
            pattern: '^\\d{4}-\\d{2}-\\d{2}$'
          },
          weekStart: {
            type: 'string',
            description: 'Monday of a specific plenary week (YYYY-MM-DD). Mutually exclusive with date. Fetches Mon-Thu of that week.',
            pattern: '^\\d{4}-\\d{2}-\\d{2}$'
          },
          term: {
            type: 'number',
            description: 'Parliamentary term number (defaults to 10 for current 2024-2029 term)',
            minimum: 1,
            maximum: 15
          },
          includeIndividualVotes: {
            type: 'boolean',
            description: 'Include individual MEP vote positions from roll-call data (default: true). Set to false for aggregate-only results.',
            default: true
          },
          limit: {
            type: 'number',
            description: 'Maximum number of vote records to return (1-100)',
            minimum: 1,
            maximum: 100,
            default: 50
          },
          offset: {
            type: 'number',
            description: 'Pagination offset',
            minimum: 0,
            default: 0
          }
        }
      }
    };