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 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | 4x 4x 13x 28x 25x 25x 11x 24x 24x 11x 24x 24x 52x 62x 114x 52x 15x 15x 15x 11x 11x 2x 15x 15x 13x 24x 24x 15x 24x 24x 24x 15x 15x 15x 2x 2x 4x | /**
* MCP Tool: compare_political_groups
*
* Cross-group comparison of voting discipline, activity levels, policy focus
* areas, internal cohesion, and legislative effectiveness.
*
* **Intelligence Perspective:** Comparative analysis tool enabling side-by-side
* assessment of political groups across multiple dimensions—supports SWOT/PESTLE
* analysis frameworks for EU parliamentary dynamics.
*
* ISMS Policy: SC-002 (Input Validation), AC-003 (Least Privilege)
*/
import { ComparePoliticalGroupsSchema } from '../schemas/europeanParliament.js';
import { epClient } from '../clients/europeanParliamentClient.js';
import { buildToolResponse } from './shared/responseBuilder.js';
import type { ToolResult } from './shared/types.js';
interface GroupComparisonMetrics {
groupId: string;
memberCount: number;
dimensions: {
votingDiscipline: number;
activityLevel: number;
legislativeOutput: number;
attendance: number;
cohesion: number;
};
computedAttributes: {
overallPerformanceScore: number;
relativeStrength: number;
seatShare: number;
effectivenessPerMember: number;
engagementIntensity: number;
};
}
interface PoliticalGroupComparison {
period: { from: string; to: string };
groupCount: number;
groups: GroupComparisonMetrics[];
rankings: { dimension: string; ranking: { groupId: string; score: number }[] }[];
computedAttributes: {
mostDisciplined: string;
mostActive: string;
highestAttendance: string;
mostCohesive: string;
strongestOverall: string;
parliamentaryBalance: number;
competitiveIndex: number;
};
confidenceLevel: string;
dataFreshness: string;
sourceAttribution: string;
methodology: string;
}
const ALL_DIMENSIONS = ['votingDiscipline', 'activityLevel', 'legislativeOutput', 'attendance', 'cohesion'] as const;
type DimensionName = typeof ALL_DIMENSIONS[number];
const DIMENSION_NAME_MAP: Record<string, DimensionName> = {
'voting_discipline': 'votingDiscipline',
'activity_level': 'activityLevel',
'legislative_output': 'legislativeOutput',
'attendance': 'attendance',
'cohesion': 'cohesion'
};
/**
* Fetch and build group metrics using real EP API data.
* Note: The EP API /meps/{id} endpoint does not provide per-MEP voting
* statistics, so voting-related dimensions report zero. Member counts
* and group composition are real.
*/
async function buildGroupMetrics(groupIds: string[]): Promise<GroupComparisonMetrics[]> {
const groups = await Promise.all(
groupIds.map(async (groupId): Promise<GroupComparisonMetrics> => {
const mepsResult = await epClient.getMEPs({ group: groupId, limit: 100 });
// Per-MEP voting statistics are not available from the EP API,
// so voting-related dimensions report zero.
const memberCount = mepsResult.data.length;
return {
groupId,
memberCount,
dimensions: {
votingDiscipline: 0,
activityLevel: 0,
legislativeOutput: 0,
attendance: 0,
cohesion: 0
},
computedAttributes: {
overallPerformanceScore: 0,
relativeStrength: 0,
seatShare: 0,
effectivenessPerMember: 0,
engagementIntensity: 0
}
};
})
);
return groups;
}
/**
* Update relative metrics after all groups computed
*/
function updateRelativeMetrics(groups: GroupComparisonMetrics[]): void {
const totalMembers = groups.reduce((sum, g) => sum + g.memberCount, 0);
const maxScore = Math.max(...groups.map(g => g.computedAttributes.overallPerformanceScore));
for (const group of groups) {
group.computedAttributes.seatShare = totalMembers > 0
? Math.round((group.memberCount / totalMembers) * 100 * 100) / 100
: 0;
group.computedAttributes.relativeStrength = maxScore > 0
? Math.round((group.computedAttributes.overallPerformanceScore / maxScore) * 100 * 100) / 100
: 0;
}
}
/**
* Build rankings from group data
*/
function buildRankings(
groups: GroupComparisonMetrics[],
requestedDimensions: readonly DimensionName[]
): PoliticalGroupComparison['rankings'] {
return requestedDimensions.map(dim => ({
dimension: dim,
ranking: [...groups]
.sort((a, b) => b.dimensions[dim] - a.dimensions[dim])
.map(g => ({ groupId: g.groupId, score: g.dimensions[dim] }))
}));
}
/**
* Find top group for a dimension
*/
function topByDimension(groups: GroupComparisonMetrics[], dim: DimensionName): string {
return [...groups].sort((a, b) => b.dimensions[dim] - a.dimensions[dim])[0]?.groupId ?? 'N/A';
}
/**
* Handles the compare_political_groups MCP tool request.
*
* Compares European Parliament political groups across configurable dimensions
* including voting discipline, activity level, legislative output, attendance,
* and internal cohesion. Produces ranked comparisons and an overall performance
* score for each group.
*
* @param args - Raw tool arguments, validated against {@link ComparePoliticalGroupsSchema}
* @returns MCP tool result containing per-group dimension scores, rankings,
* seat-share distribution, and a computed parliamentary balance index
* @throws - If `args` fails schema validation (e.g., missing required fields or invalid format)
* - If the European Parliament API is unreachable or returns an error response
*
* @example
* ```typescript
* const result = await handleComparePoliticalGroups({
* groupIds: ['EPP', 'S&D', 'Renew', 'Greens/EFA'],
* dimensions: ['voting_discipline', 'activity_level', 'cohesion'],
* dateFrom: '2024-01-01',
* dateTo: '2024-12-31'
* });
* // Returns ranked group comparison with per-dimension scores
* // and overall performance leaderboard
* ```
*
* @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 comparePoliticalGroupsToolMetadata} for MCP schema registration
* @see {@link handleAnalyzeCoalitionDynamics} for pairwise coalition cohesion analysis
*/
export async function handleComparePoliticalGroups(
args: unknown
): Promise<ToolResult> {
const params = ComparePoliticalGroupsSchema.parse(args);
try {
const groups = await buildGroupMetrics(params.groupIds);
updateRelativeMetrics(groups);
const requestedDimensions: readonly DimensionName[] = params.dimensions
? params.dimensions.map(d => DIMENSION_NAME_MAP[d]).filter((d): d is DimensionName => d !== undefined)
: ALL_DIMENSIONS;
const rankings = buildRankings(groups, requestedDimensions);
const topOverall = [...groups]
.sort((a, b) => b.computedAttributes.overallPerformanceScore - a.computedAttributes.overallPerformanceScore)[0]?.groupId ?? 'N/A';
const seatShares = groups.map(g => g.computedAttributes.seatShare / 100);
const herfindahl = seatShares.reduce((sum, s) => sum + s * s, 0);
const balance = 1 - herfindahl;
const scores = groups.map(g => g.computedAttributes.overallPerformanceScore);
const avgScore = scores.reduce((s, v) => s + v, 0) / scores.length;
const variance = scores.reduce((s, v) => s + (v - avgScore) ** 2, 0) / scores.length;
const competitiveIndex = Math.round((1 - Math.min(1, variance / 1000)) * 100) / 100;
const comparison: PoliticalGroupComparison = {
period: { from: params.dateFrom ?? '2024-01-01', to: params.dateTo ?? '2024-12-31' },
groupCount: groups.length,
groups,
rankings,
computedAttributes: {
mostDisciplined: topByDimension(groups, 'votingDiscipline'),
mostActive: topByDimension(groups, 'activityLevel'),
highestAttendance: topByDimension(groups, 'attendance'),
mostCohesive: topByDimension(groups, 'cohesion'),
strongestOverall: topOverall,
parliamentaryBalance: Math.round(balance * 100) / 100,
competitiveIndex
},
confidenceLevel: 'LOW',
dataFreshness: 'Real-time EP API data — political group composition from current MEP records',
sourceAttribution: 'European Parliament Open Data Portal - data.europarl.europa.eu',
methodology: 'Multi-dimensional comparative analysis using real EP Open Data MEP records. '
+ 'Per-MEP voting statistics are not available from the EP API /meps/{id} endpoint; '
+ 'voting discipline, activity level, attendance, and cohesion dimensions report zero. '
+ 'Member counts and group composition are real. '
+ 'Data source: European Parliament Open Data Portal.'
};
return buildToolResponse(comparison);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new Error(`Failed to compare political groups: ${errorMessage}`);
}
}
/**
* Tool metadata for MCP registration
*/
export const comparePoliticalGroupsToolMetadata = {
name: 'compare_political_groups',
description: 'Compare political groups across multiple dimensions: voting discipline, activity level, legislative output, attendance, and cohesion. Returns rankings, computed performance scores, seat share, effectiveness per member, parliamentary balance index, and competitive index.',
inputSchema: {
type: 'object' as const,
properties: {
groupIds: {
type: 'array',
items: { type: 'string' },
description: 'Political group identifiers to compare (minimum 2)',
minItems: 2,
maxItems: 10
},
dateFrom: {
type: 'string',
description: 'Analysis start date (YYYY-MM-DD format)',
pattern: '^\\d{4}-\\d{2}-\\d{2}$'
},
dateTo: {
type: 'string',
description: 'Analysis end date (YYYY-MM-DD format)',
pattern: '^\\d{4}-\\d{2}-\\d{2}$'
},
dimensions: {
type: 'array',
items: {
type: 'string',
enum: ['voting_discipline', 'activity_level', 'legislative_output', 'attendance', 'cohesion']
},
description: 'Comparison dimensions (omit for all)'
}
},
required: ['groupIds']
}
};
|