# All files / src/tools getExternalDocumentsFeed.ts

95% Statements 38/40
83.33% Branches 20/24
100% Functions 6/6
96.87% Lines 31/32

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

    4x

    2x

    2x
    2x

    2x

    11x
    11x

    11x
    1x

    11x
    1x

    1x
    1x

    15x
    15x

    1x
    1x
    1x

    14x
    14x
    14x
    14x
    14x
    14x

    12x
    12x
    1x

    11x

    2x
    1x

    4x

     /**
     * MCP Tool: get_external_documents_feed
     *
     * Get recently updated external documents (non-EP documents) from the feed.
     *
     * **EP API Endpoint:**
     * - `GET /external-documents/feed`
     *
     * ISMS Policy: SC-002 (Input Validation), AC-003 (Least Privilege)
     */

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

    const EXTERNAL_DOCUMENTS_EMPTY_REASON =
      'EP external-documents feed returned zero items for the requested window; this is ambiguous between a true empty window and feed freshness/ordering lag.';

    interface ExternalDocumentsFeedDiagnostics {
      endpoint: 'external-documents/feed';
      requestedWindow: {
        timeframe: string;
        startDate?: string;
        workType?: string;
      };
      emptyResultAmbiguity: 'true-empty-or-feed-freshness-lag';
      freshnessStatus: 'unknown';
      fallbackTool: 'get_external_documents';
      fallbackArguments: {
        limit: 20;
      };
    }

    type ExternalDocumentsFeedParams = ReturnType<typeof GetExternalDocumentsFeedSchema.parse>;

    function buildExternalDocumentsFeedDiagnostics(
      params: ExternalDocumentsFeedParams
    ): ExternalDocumentsFeedDiagnostics {
      const requestedWindow: ExternalDocumentsFeedDiagnostics['requestedWindow'] = {
        timeframe: params.timeframe,
      };
      if (params.startDate !== undefined) requestedWindow.startDate = params.startDate;
      if (params.workType !== undefined) requestedWindow.workType = params.workType;

      return {
        endpoint: 'external-documents/feed',
        requestedWindow,
        emptyResultAmbiguity: 'true-empty-or-feed-freshness-lag',
        freshnessStatus: 'unknown',
        fallbackTool: 'get_external_documents',
        fallbackArguments: { limit: 20 },
      };
    }

    function hasFeedItems(result: unknown): boolean {
      const source = (result ?? {}) as Record<string, unknown>;
      return Array.isArray(source['data']) && source['data'].length > 0;
    }

    function withEmptyFeedDiagnostics(result: unknown, params: ExternalDocumentsFeedParams): unknown {
      if (hasFeedItems(result)) return result;
      const source = (result ?? {}) as Record<string, unknown>;
      // Error-in-body responses are upstream enrichment failures, not ambiguous
      // true-empty/freshness-lag windows — do not attach diagnostics.
      Iif (isErrorInBody(source)) return result;
      return {
        ...source,
        dataQualityDiagnostics: buildExternalDocumentsFeedDiagnostics(params),
      };
    }

    function buildExternalDocumentsUnavailableResponse(params: ExternalDocumentsFeedParams): ToolResult {
      const items: unknown[] = [];
      return buildToolResponse({
        status: 'unavailable',
        generatedAt: new Date().toISOString(),
        items,
        itemCount: 0,
        reason: EXTERNAL_DOCUMENTS_EMPTY_REASON,
        data: items,
        '@context': [],
        dataQualityWarnings: [EXTERNAL_DOCUMENTS_EMPTY_REASON],
        dataQualityDiagnostics: buildExternalDocumentsFeedDiagnostics(params),
      });
    }

    /**
     * Handles the get_external_documents_feed MCP tool request.
     *
     * @param args - Raw tool arguments, validated against {@link GetExternalDocumentsFeedSchema}
     * @returns MCP tool result containing recently updated external document data
     * @security Input is validated with Zod before any API call.
     */
    export async function handleGetExternalDocumentsFeed(args: unknown): Promise<ToolResult> {
      let params: ReturnType<typeof GetExternalDocumentsFeedSchema.parse>;
      try {
        params = GetExternalDocumentsFeedSchema.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_external_documents_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;
        if (params.workType !== undefined) apiParams['workType'] = params.workType;
        const result = await epClient.getExternalDocumentsFeed(
          apiParams
        );
        const source = result as Record<string, unknown>;
        if (isErrorInBody(source)) {
          return buildEmptyFeedResponse(
            'EP API returned an error-in-body response for get_external_documents_feed — the upstream enrichment step may have failed.',
          );
        }
        return buildFeedSuccessResponse(
          withEmptyFeedDiagnostics(result, params),
          [],
          EXTERNAL_DOCUMENTS_EMPTY_REASON,
        );
      } catch (error: unknown) {
        if (isUpstream404(error)) return buildExternalDocumentsUnavailableResponse(params);
        throw new ToolError({
          toolName: 'get_external_documents_feed',
          operation: 'fetchData',
          message: 'Failed to retrieve external documents feed',
          isRetryable: true,
          cause: error,
        });
      }
    }
    /** Tool metadata for get_external_documents_feed */
    export const getExternalDocumentsFeedToolMetadata = {
      name: 'get_external_documents_feed',
      description:
        'Get recently updated external documents (non-EP documents) from the feed. Returns external documents published or updated during the specified timeframe. Data source: European Parliament Open Data Portal.',
      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"',
          },
          workType: { type: 'string', description: 'Work type filter' },
        },
      },
    };