# All files / src/tools getMEPsFeed.ts

96% Statements 24/25
75% Branches 9/12
100% Functions 3/3
95.23% Lines 20/21

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

    4x

    10x
    10x
    10x
    1x

    13x
    13x

    1x
    1x
    1x

    12x
    12x
    12x
    12x
    12x

    10x
    10x

    2x
    1x

    4x

     /**
     * MCP Tool: get_meps_feed
     *
     * Get recently updated MEPs from the European Parliament feed.
     *
     * **EP API Endpoint:**
     * - `GET /meps/feed`
     *
     * **Oversized-payload detection:** the Hack23/euparliamentmonitor
     * 2026-04-24 breaking audit §1.5 documented that the upstream feed is
     * occasionally failing open to a full-census dump (33.6 MB / ~700 items)
     * instead of returning a delta. When the response item count exceeds
     * {@link OVERSIZED_PAYLOAD_THRESHOLD} we surface an `OVERSIZED_PAYLOAD`
     * warning so consumers can detect this mechanically without parsing prose.
     *
     * ISMS Policy: SC-002 (Input Validation), AC-003 (Least Privilege)
     */

    import { GetMEPsFeedSchema } from '../schemas/europeanParliament.js';
    import { epClient } from '../clients/europeanParliamentClient.js';
    import { ToolError } from './shared/errors.js';
    import { isUpstream404, buildEmptyFeedResponse, buildFeedSuccessResponse } from './shared/feedUtils.js';
    import { z } from 'zod';
    import type { ToolResult } from './shared/types.js';

    /**
     * Item-count threshold above which a `/meps/feed` response is considered a
     * full-census dump rather than a delta. The total seated MEP count is ≤ 720,
     * and a healthy delta on a single day rarely exceeds ~50 mandate changes.
     * 200 is a deliberately conservative threshold that flags the failure mode
     * without firing on legitimate term-start ramps.
     *
     * @internal
     */
    const OVERSIZED_PAYLOAD_THRESHOLD = 200;

    /**
     * Build OVERSIZED_PAYLOAD warning entries when the feed item count crosses
     * {@link OVERSIZED_PAYLOAD_THRESHOLD}. Mechanically observable signal of the
     * upstream delta-pagination failure mode.
     *
     * @internal
     */
    function buildOversizedPayloadWarnings(result: unknown): readonly string[] {
      const source = (result ?? {}) as Record<string, unknown>;
      const items = Array.isArray(source['data']) ? (source['data'] as unknown[]) : [];
      if (items.length <= OVERSIZED_PAYLOAD_THRESHOLD) return [];
      return [
        `OVERSIZED_PAYLOAD: EP /meps/feed returned ${String(items.length)} item(s) — ` +
          `this exceeds the delta-vs-census threshold (${String(OVERSIZED_PAYLOAD_THRESHOLD)}) and indicates the ` +
          `upstream delta-pagination has likely failed open to a full-census dump. The data is structurally valid ` +
          `but does not represent a delta of recent mandate changes. Consider using get_meps for census queries ` +
          `instead, and re-querying get_meps_feed with a narrower timeframe.`,
      ];
    }

    /**
     * Handles the get_meps_feed MCP tool request.
     *
     * @param args - Raw tool arguments, validated against {@link GetMEPsFeedSchema}
     * @returns MCP tool result containing recently updated MEP data
     * @security Input is validated with Zod before any API call.
     */
    export async function handleGetMEPsFeed(args: unknown): Promise<ToolResult> {
      let params: ReturnType<typeof GetMEPsFeedSchema.parse>;
      try {
        params = GetMEPsFeedSchema.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_meps_feed',
            operation: 'validateInput',
            message: `Invalid parameters: ${fieldErrors}`,
            isRetryable: false,
            cause: error,
          });
        }
        throw error;
      }

      try {
        const apiParams: Record<string, unknown> = {};
        apiParams['timeframe'] = params.timeframe;
        if (params.startDate !== undefined) apiParams['startDate'] = params.startDate;
        const result = await epClient.getMEPsFeed(
          apiParams
        );
        const oversizedWarnings = buildOversizedPayloadWarnings(result);
        return buildFeedSuccessResponse(result, oversizedWarnings);
      } catch (error: unknown) {
        if (isUpstream404(error)) return buildEmptyFeedResponse();
        throw new ToolError({
          toolName: 'get_meps_feed',
          operation: 'fetchData',
          message: 'Failed to retrieve MEPs feed',
          isRetryable: true,
          cause: error,
        });
      }
    }
    /** Tool metadata for get_meps_feed */
    export const getMEPsFeedToolMetadata = {
      name: 'get_meps_feed',
      description:
        'Get recently updated MEPs from the European Parliament feed. Returns MEPs published or updated during the specified timeframe. Data source: European Parliament Open Data Portal. NOTE: when the upstream returns more than 200 items (a known failure mode where delta-pagination falls back to a full-census dump) the response surfaces an OVERSIZED_PAYLOAD entry in dataQualityWarnings so consumers can detect the regression mechanically.',
      inputSchema: {
        type: 'object' as const,
        properties: {
          timeframe: {
            type: 'string',
            description: 'Timeframe for the feed (today, one-day, one-week, one-month, custom)',
            enum: ['today', 'one-day', 'one-week', 'one-month', 'custom'],
            default: 'one-week',
          },
          startDate: {
            type: 'string',
            description: 'Start date (YYYY-MM-DD) — required when timeframe is "custom"',
          },
        },
      },
    };