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 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | 20x 20x 5x 5x 5x 15x 15x 1x 1x 1x 14x 14x 13x 13x 13x 2x 2x 2x 4x 4x 4x 2x 2x 2x 11x 1x 1x 4x | /**
* MCP Tool: get_plenary_sessions
*
* Retrieve European Parliament plenary session information
*
* **Intelligence Perspective:** Critical for legislative monitoring, session activity tracking,
* debate analysis, and identifying legislative priorities across parliamentary terms.
*
* **Business Perspective:** Enables real-time legislative tracking products for compliance
* teams, regulatory affairs departments, and policy monitoring services.
*
* **Marketing Perspective:** Showcases live parliamentary data access—compelling for
* journalists, media organizations, and civic tech platforms.
*
* ISMS Policy: SC-002 (Input Validation), AC-003 (Least Privilege)
*/
import { GetPlenarySessionsSchema, PlenarySessionSchema, 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_plenary_sessions MCP tool request.
*
* Retrieves European Parliament plenary sessions with optional filtering by date range and
* location. Supports single-session lookup by event ID. Critical for legislative monitoring,
* session activity tracking, debate analysis, and identifying legislative priorities.
*
* @param args - Raw tool arguments, validated against {@link GetPlenarySessionsSchema}
* @returns MCP tool result containing either a single plenary session record (when `eventId`
* is provided) or a paginated list of sessions with date, location, agenda items, voting
* records, and attendance statistics
* @throws - If `args` fails schema validation (e.g., date not in YYYY-MM-DD format,
* limit out of range 1–100)
* - If the European Parliament API is unreachable or returns an error response
*
* @example
* ```typescript
* // List sessions in a date range
* const result = await handleGetPlenarySessions({
* dateFrom: '2024-01-01',
* dateTo: '2024-12-31',
* limit: 20
* });
* // Returns up to 20 plenary sessions held in 2024
*
* // Fetch a single session by event ID
* const single = await handleGetPlenarySessions({ eventId: 'MTG-2024-01-15' });
* // Returns agenda, voting records, and attendance for the specified session
* ```
*
* @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 getPlenarySessionsToolMetadata} for MCP schema registration
* @see {@link handleGetPlenaryDocuments} for legislative documents associated with sessions
* @see {@link handleGetPlenarySessionDocuments} for session-specific agendas and minutes
*/
export async function handleGetPlenarySessions(
args: unknown
): Promise<ToolResult> {
// Validate input — ZodErrors here are client mistakes (non-retryable)
let params: ReturnType<typeof GetPlenarySessionsSchema.parse>;
try {
params = GetPlenarySessionsSchema.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_plenary_sessions',
operation: 'validateInput',
message: `Invalid parameters: ${fieldErrors}`,
isRetryable: false,
cause: error,
});
}
throw error;
}
try {
// Single meeting lookup by ID
if (params.eventId !== undefined) {
const result = await epClient.getMeetingById(params.eventId);
const validated = PlenarySessionSchema.parse(result);
return buildToolResponse(validated);
}
// Fetch plenary sessions from EP API (only pass defined properties)
const apiParams = {
limit: params.limit,
offset: params.offset,
...buildApiParams(params, [
{ from: 'year', to: 'year' },
{ from: 'dateFrom', to: 'dateFrom' },
{ from: 'dateTo', to: 'dateTo' },
{ from: 'location', to: 'location' },
]),
};
const result = await epClient.getPlenarySessions(apiParams);
// Validate output
const outputSchema = PaginatedResponseSchema(PlenarySessionSchema);
const validated = outputSchema.parse(result);
// Client-side post-filter for dateFrom / dateTo.
// The EP `/meetings` endpoint accepts the `date-from` / `date-to` query
// params but historically ignores them and returns sessions stretching
// back to January 2014. See Hack23/euparliamentmonitor 2026-04-24
// propositions audit, Defect #5 (`get_plenary_sessions returns historical
// sessions despite dateFrom`). Until the upstream is fixed, we apply a
// best-effort post-filter on the returned `date` field so callers get
// sessions that actually fall in their requested window.
//
// Pagination semantics: we preserve the upstream `total` and `hasMore`
// (which describe the unfiltered page returned by the EP API) and
// surface the post-filter outcome via separate `filteredTotal` /
// `filteredHasMore` fields so consumers can still drive cursor-style
// pagination correctly even when later pages contain in-range
// sessions that the current page filtered out.
if (params.dateFrom !== undefined || params.dateTo !== undefined) {
const minDate = params.dateFrom;
const maxDate = params.dateTo;
const filtered = validated.data.filter((session) => {
const d = session.date;
Iif (typeof d !== 'string' || d === '') return false;
if (minDate !== undefined && d < minDate) return false;
Iif (maxDate !== undefined && d > maxDate) return false;
return true;
});
return buildToolResponse({
...validated,
data: filtered,
filteredTotal: filtered.length,
filteredHasMore: validated.hasMore && filtered.length === validated.data.length,
});
}
return buildToolResponse(validated);
} 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_plenary_sessions',
operation: 'validateOutput',
message: `Unexpected EP API response format: ${fieldErrors}`,
isRetryable: false,
cause: error,
});
}
throw new ToolError({
toolName: 'get_plenary_sessions',
operation: 'fetchData',
message: 'Failed to retrieve plenary sessions',
isRetryable: true,
cause: error,
});
}
}
/**
* Tool metadata for MCP registration
*/
export const getPlenarySessionsToolMetadata = {
name: 'get_plenary_sessions',
description: 'Retrieve European Parliament plenary sessions/meetings. Supports single meeting lookup by eventId or list with year, date, and location filters. Returns session details including date, location, agenda items, voting records, and attendance statistics.',
inputSchema: {
type: 'object' as const,
properties: {
eventId: {
type: 'string',
description: 'Meeting event ID for single meeting lookup'
},
year: {
type: 'number',
description: 'Filter by calendar year (recommended for annual counts)',
minimum: 1900,
maximum: 2100
},
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}$'
},
location: {
type: 'string',
description: 'Session location (e.g., "Strasbourg", "Brussels")',
minLength: 1,
maxLength: 100
},
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
}
}
}
};
|