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 | 4x 12x 12x 12x 12x 12x 11x 11x 11x 11x 11x 12x 12x 12x 2x 1x 1x 1x 12x 12x 12x 10x 10x 10x 16x 16x 1x 1x 1x 15x 15x 15x 15x 15x 15x 13x 13x 16x 16x 12x 12x 1x 2x 1x 4x | /**
* MCP Tool: get_adopted_texts_feed
*
* Get recently updated European Parliament adopted texts from the feed.
*
* **EP API Endpoint:**
* - `GET /adopted-texts/feed`
*
* **Freshness fallback (PRIORITY):** The EP `/adopted-texts/feed` endpoint
* has been observed returning historical backfill (TA-9-2024 / TA-10-2025)
* even under `timeframe: "today"` since at least 2026-04-14, with no
* current-year items reaching consumers. See
* Hack23/euparliamentmonitor 2026-04-24 breaking audit §1.2 and
* 2026-04-24 propositions audit Defect #7. To make recent documents
* findable while the upstream feed is degraded, this handler augments the
* feed payload with the current calendar year of `/adopted-texts` (which
* accepts a `year` filter) whenever the feed contains no current-year
* items, and surfaces a `dataQualityWarning` so callers can distinguish
* fallback augmentation from a healthy feed response.
*
* ISMS Policy: SC-002 (Input Validation), AC-003 (Least Privilege)
*/
import { GetAdoptedTextsFeedSchema } 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';
/**
* Maximum number of current-year `/adopted-texts` items to merge into the
* feed payload when the feed itself returned no fresh items. Bounded so
* the augmented response stays the same order of magnitude as a normal
* feed reply.
*/
const FRESHNESS_FALLBACK_LIMIT = 50;
/**
* Returns `true` when the supplied feed item carries a current-year
* marker. We inspect the canonical `dateAdopted` field, the generated
* TA-identifier (`TA-{term}-{year}-{nnnn}`), and the `reference` field
* because the EP feed payload is JSON-LD and individual items may omit
* some of these markers. Anything else (missing string, malformed) is
* treated as "not current-year", so this function returns `false` and
* the freshness fallback may run.
*
* @internal
*/
function isCurrentYearItem(item: unknown, currentYear: number): boolean {
Iif (item === null || typeof item !== 'object') return false;
const obj = item as Record<string, unknown>;
const yearStr = String(currentYear);
const dateAdopted = obj['dateAdopted'];
if (typeof dateAdopted === 'string' && dateAdopted.startsWith(yearStr)) return true;
const id = obj['id'];
Iif (typeof id === 'string' && id.includes(`-${yearStr}-`)) return true;
const reference = obj['reference'];
Iif (typeof reference === 'string' && reference.includes(`(${yearStr})`)) return true;
return false;
}
/**
* Best-effort freshness augmentation: pull the current calendar year of
* `/adopted-texts` and merge into the feed payload when no current-year
* items were returned. Failures are swallowed (the feed response is
* still returned) but a warning is added.
*
* @internal
*/
async function augmentWithCurrentYear(
feedResult: Record<string, unknown>,
currentYear: number,
): Promise<{ result: Record<string, unknown>; warnings: string[] }> {
const warnings: string[] = [];
try {
const recent = await epClient.getAdoptedTexts({
year: currentYear,
limit: FRESHNESS_FALLBACK_LIMIT,
offset: 0,
});
if (recent.data.length === 0) {
warnings.push(
`FRESHNESS_FALLBACK_FAILED: EP /adopted-texts/feed returned no items from the current year (${String(currentYear)}), ` +
`/adopted-texts?year=${String(currentYear)} also returned 0 items, and the original feed response is being ` +
`returned unchanged — consider retrying later or widening the search.`,
);
return { result: feedResult, warnings };
}
const existingData = Array.isArray(feedResult['data']) ? (feedResult['data'] as unknown[]) : [];
const augmented = {
...feedResult,
data: [...recent.data, ...existingData],
};
warnings.push(
`FRESHNESS_FALLBACK: EP /adopted-texts/feed returned no items from the current year (${String(currentYear)}). ` +
`Augmented response with ${String(recent.data.length)} item(s) from /adopted-texts?year=${String(currentYear)} ` +
`(non-feed endpoint, sorted by EP API default). Items prefixed before any existing feed items.`,
);
return { result: augmented, warnings };
} catch (error: unknown) {
const msg = error instanceof Error ? error.message : 'unknown error';
warnings.push(
`FRESHNESS_FALLBACK_FAILED: EP /adopted-texts/feed returned no current-year items and the freshness ` +
`fallback against /adopted-texts?year=${String(currentYear)} also failed (${msg}). ` +
`Caller should retry or widen the timeframe.`,
);
return { result: feedResult, warnings };
}
}
/**
* Handles the get_adopted_texts_feed MCP tool request.
*
* @param args - Raw tool arguments, validated against {@link GetAdoptedTextsFeedSchema}
* @returns MCP tool result containing recently updated adopted text data
* @security Input is validated with Zod before any API call.
*/
export async function handleGetAdoptedTextsFeed(args: unknown): Promise<ToolResult> {
// Validate input — ZodErrors here are client mistakes (non-retryable)
let params: ReturnType<typeof GetAdoptedTextsFeedSchema.parse>;
try {
params = GetAdoptedTextsFeedSchema.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_adopted_texts_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.getAdoptedTextsFeed(apiParams);
// Freshness check: when the feed payload contains no current-year items
// (a known degraded-upstream pattern observed since 2026-04-14), augment
// with the current calendar year of /adopted-texts so callers can still
// discover recent documents.
const currentYear = new Date().getUTCFullYear();
const items = Array.isArray(result['data']) ? (result['data'] as unknown[]) : [];
const hasCurrentYear = items.some((item) => isCurrentYearItem(item, currentYear));
if (!hasCurrentYear) {
const { result: augmented, warnings } = await augmentWithCurrentYear(result, currentYear);
return buildFeedSuccessResponse(augmented, warnings);
}
return buildFeedSuccessResponse(result);
} catch (error: unknown) {
if (isUpstream404(error)) return buildEmptyFeedResponse();
throw new ToolError({
toolName: 'get_adopted_texts_feed',
operation: 'fetchData',
message: 'Failed to retrieve adopted texts feed',
isRetryable: true,
cause: error,
});
}
}
/** Tool metadata for get_adopted_texts_feed */
export const getAdoptedTextsFeedToolMetadata = {
name: 'get_adopted_texts_feed',
description:
'Get recently updated European Parliament adopted texts from the feed. Returns adopted texts published or updated during the specified timeframe. Data source: European Parliament Open Data Portal. NOTE: When the EP /adopted-texts/feed endpoint returns no items from the current calendar year (a known degraded-upstream pattern), the response is automatically augmented with /adopted-texts?year={currentYear} so callers can still discover recent documents — a FRESHNESS_FALLBACK warning is surfaced in dataQualityWarnings whenever this happens.',
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' },
},
},
};
|