From 2e627a7530272ba4cd201862d058f342b37afdd5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:18:25 +0000 Subject: [PATCH 1/5] feat: add get_voting_group and search_ledamoter MCP tool integrations to breaking-news Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- scripts/mcp-client.ts | 1 + scripts/mcp-client/client.ts | 11 ++++++++ scripts/mcp-client/index.ts | 6 +++++ scripts/news-types/breaking-news.ts | 36 +++++++++++++++----------- scripts/types/mcp.ts | 10 +++++++ tests/news-types/breaking-news.test.ts | 34 +++++++++++++++++++++++- 6 files changed, 82 insertions(+), 16 deletions(-) diff --git a/scripts/mcp-client.ts b/scripts/mcp-client.ts index faacb531..a41906b3 100644 --- a/scripts/mcp-client.ts +++ b/scripts/mcp-client.ts @@ -29,6 +29,7 @@ export { searchSpeeches, fetchMPs, fetchVotingRecords, + fetchVotingGroup, fetchGovernmentDocuments, fetchDocumentDetails, enrichDocumentsWithContent, diff --git a/scripts/mcp-client/client.ts b/scripts/mcp-client/client.ts index 3d78b1a8..716fdbc7 100644 --- a/scripts/mcp-client/client.ts +++ b/scripts/mcp-client/client.ts @@ -17,6 +17,7 @@ import type { SearchSpeechesParams, FetchMPsFilters, FetchVotingFilters, + FetchVotingGroupParams, GovDocSearchParams, RiksdagDocument, } from '../types/mcp.js'; @@ -467,6 +468,16 @@ export class MCPClient { return (response['votes'] ?? []) as unknown[]; } + async fetchVotingGroup(params: FetchVotingGroupParams = {}): Promise { + const response = await this.request( + 'get_voting_group', + params as unknown as Record, + ); + // MCP server returns 'groups' when groupBy is provided (grouped results), + // or 'votes' when no grouping is applied (flat voting list fallback) + return (response['groups'] ?? response['votes'] ?? []) as unknown[]; + } + async fetchGovernmentDocuments(searchParams: GovDocSearchParams): Promise { const response = await this.request( 'search_regering', diff --git a/scripts/mcp-client/index.ts b/scripts/mcp-client/index.ts index f2f6617e..06d4bb28 100644 --- a/scripts/mcp-client/index.ts +++ b/scripts/mcp-client/index.ts @@ -82,6 +82,12 @@ export async function fetchVotingRecords( return getDefaultClient().fetchVotingRecords(...args); } +export async function fetchVotingGroup( + ...args: Parameters +): ReturnType { + return getDefaultClient().fetchVotingGroup(...args); +} + export async function fetchGovernmentDocuments( ...args: Parameters ): ReturnType { diff --git a/scripts/news-types/breaking-news.ts b/scripts/news-types/breaking-news.ts index 1fe73bfb..eb78d5d5 100644 --- a/scripts/news-types/breaking-news.ts +++ b/scripts/news-types/breaking-news.ts @@ -39,10 +39,8 @@ * - search_ledamoter: Member information for quote attribution * * **IMPLEMENTATION STATUS:** - * - Actual implementation calls: search_voteringar, search_anforanden - * - TODO implementation: get_voting_group, search_ledamoter - * Note: This causes validation warnings but allows tests to pass. Full - * implementation should complete all four tool integrations. + * - All four MCP tools implemented: search_voteringar, get_voting_group, + * search_anforanden, search_ledamoter (all conditional on event data) * * **INTELLIGENCE ANALYSIS FRAMEWORK:** * Breaking news articles incorporate: @@ -168,15 +166,11 @@ import type { /** * Required MCP tools for breaking news articles * - * CURRENT IMPLEMENTATION STATUS: - * - search_voteringar: ✅ Implemented (conditional, line 72) - * - search_anforanden: ✅ Implemented (conditional, line 79) - * - get_voting_group: ❌ TODO - Not yet implemented - * - search_ledamoter: ❌ TODO - Not yet implemented - * - * NOTE: REQUIRED_TOOLS lists the full specification for validation. - * Current implementation calls a subset. This causes validation warnings - * but allows tests to pass. Full implementation should add the missing tools. + * IMPLEMENTATION STATUS: + * - search_voteringar: ✅ Implemented (conditional on voteId) + * - get_voting_group: ✅ Implemented (conditional on voteId) + * - search_anforanden: ✅ Implemented (conditional on topic) + * - search_ledamoter: ✅ Implemented (conditional on topic) */ export const REQUIRED_TOOLS: readonly string[] = [ 'search_voteringar', @@ -233,18 +227,30 @@ export async function generateBreakingNews(options: BreakingNewsOptions = {}): P }; } - // Example: Fetch related votes if event involves a vote + // Fetch related votes and party breakdown if event involves a vote if (eventData.voteId) { console.log(' 🔄 Fetching voting details...'); const votes: unknown = await client.fetchVotingRecords({ punkt: eventData.voteId }); mcpCalls.push({ tool: 'search_voteringar', result: votes }); + + // Fetch party-level voting breakdown + console.log(' 🔄 Fetching party voting breakdown...'); + const votingGroup: unknown = await client.fetchVotingGroup({ punkt: eventData.voteId, groupBy: 'parti' }); + mcpCalls.push({ tool: 'get_voting_group', result: votingGroup }); } - // Example: Fetch related speeches + // Fetch related speeches and MP context if topic provided if (eventData.topic) { console.log(' 🔄 Fetching related speeches...'); const speeches: unknown = await client.searchSpeeches({ sok: eventData.topic }); mcpCalls.push({ tool: 'search_anforanden', result: speeches }); + + // Fetch MP profiles for speaker context using names from speeches + console.log(' 🔄 Fetching MP profiles for speaker context...'); + const speechList = Array.isArray(speeches) ? speeches as Array> : []; + const speakerName = speechList[0]?.['talare'] as string | undefined; + const mps: unknown = await client.fetchMPs(speakerName ? { namn: speakerName, limit: 1 } : { limit: 5 }); + mcpCalls.push({ tool: 'search_ledamoter', result: mps }); } const today = new Date(); diff --git a/scripts/types/mcp.ts b/scripts/types/mcp.ts index d4969879..82b68552 100644 --- a/scripts/types/mcp.ts +++ b/scripts/types/mcp.ts @@ -92,6 +92,16 @@ export interface FetchVotingFilters { [key: string]: unknown; } +/** Parameters for party-level voting group lookup */ +export interface FetchVotingGroupParams { + bet?: string; + punkt?: string; + groupBy?: 'parti' | 'valkrets' | 'namn'; + rm?: string; + limit?: number; + [key: string]: unknown; +} + /** Parameters for government document search */ export interface GovDocSearchParams { title?: string; diff --git a/tests/news-types/breaking-news.test.ts b/tests/news-types/breaking-news.test.ts index e5303aca..3fc369bd 100644 --- a/tests/news-types/breaking-news.test.ts +++ b/tests/news-types/breaking-news.test.ts @@ -11,6 +11,8 @@ import type { Language } from '../../scripts/types/language.js'; interface MockMCPClientShape { fetchVotingRecords: Mock<(...args: unknown[]) => Promise>; searchSpeeches: Mock<(...args: unknown[]) => Promise>; + fetchVotingGroup: Mock<(...args: unknown[]) => Promise>; + fetchMPs: Mock<(...args: unknown[]) => Promise>; } /** Validation input */ @@ -44,7 +46,9 @@ interface BreakingNewsModule { const { mockClientInstance, MockMCPClient } = vi.hoisted(() => { const mockClientInstance: MockMCPClientShape = { fetchVotingRecords: vi.fn().mockResolvedValue([]) as MockMCPClientShape['fetchVotingRecords'], - searchSpeeches: vi.fn().mockResolvedValue([]) as MockMCPClientShape['searchSpeeches'] + searchSpeeches: vi.fn().mockResolvedValue([]) as MockMCPClientShape['searchSpeeches'], + fetchVotingGroup: vi.fn().mockResolvedValue([]) as MockMCPClientShape['fetchVotingGroup'], + fetchMPs: vi.fn().mockResolvedValue([]) as MockMCPClientShape['fetchMPs'] }; function MockMCPClient(): MockMCPClientShape { @@ -134,6 +138,34 @@ describe('Breaking News Article Generation', () => { expect(mockClientInstance.searchSpeeches).toHaveBeenCalled(); }); + + it('should fetch voting group if voteId provided', async () => { + const eventData: BreakingEventData = { + voteId: 'v123', + slug: 'test' + }; + + await breakingNewsModule.generateBreakingNews({ + languages: ['en'], + eventData + }); + + expect(mockClientInstance.fetchVotingGroup).toHaveBeenCalled(); + }); + + it('should fetch MP profiles if topic provided', async () => { + const eventData: BreakingEventData = { + topic: 'Budget debate', + slug: 'test' + }; + + await breakingNewsModule.generateBreakingNews({ + languages: ['en'], + eventData + }); + + expect(mockClientInstance.fetchMPs).toHaveBeenCalled(); + }); }); describe('Article Structure', () => { From c8aa415968e18223a584aece3ca4c0e94c1972a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:37:23 +0000 Subject: [PATCH 2/5] fix: call all 4 REQUIRED_TOOLS unconditionally with try/catch in breaking-news Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- scripts/news-types/breaking-news.ts | 47 ++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/scripts/news-types/breaking-news.ts b/scripts/news-types/breaking-news.ts index eb78d5d5..ddd7edcf 100644 --- a/scripts/news-types/breaking-news.ts +++ b/scripts/news-types/breaking-news.ts @@ -227,30 +227,49 @@ export async function generateBreakingNews(options: BreakingNewsOptions = {}): P }; } - // Fetch related votes and party breakdown if event involves a vote - if (eventData.voteId) { + // Tool 1: search_voteringar — vote records (enriched with voteId when available) + try { console.log(' 🔄 Fetching voting details...'); - const votes: unknown = await client.fetchVotingRecords({ punkt: eventData.voteId }); + const votes: unknown = await client.fetchVotingRecords(eventData.voteId ? { punkt: eventData.voteId } : { limit: 5 }); mcpCalls.push({ tool: 'search_voteringar', result: votes }); - - // Fetch party-level voting breakdown + } catch (err) { + console.warn(' ⚠ search_voteringar unavailable:', (err as Error).message); + mcpCalls.push({ tool: 'search_voteringar', result: [] }); + } + + // Tool 2: get_voting_group — party-level voting breakdown + try { console.log(' 🔄 Fetching party voting breakdown...'); - const votingGroup: unknown = await client.fetchVotingGroup({ punkt: eventData.voteId, groupBy: 'parti' }); + const votingGroup: unknown = await client.fetchVotingGroup( + eventData.voteId ? { punkt: eventData.voteId, groupBy: 'parti' } : { groupBy: 'parti', limit: 5 } + ); mcpCalls.push({ tool: 'get_voting_group', result: votingGroup }); + } catch (err) { + console.warn(' ⚠ get_voting_group unavailable:', (err as Error).message); + mcpCalls.push({ tool: 'get_voting_group', result: [] }); } - - // Fetch related speeches and MP context if topic provided - if (eventData.topic) { + + // Tool 3: search_anforanden — speeches providing context and reactions + let speechList: Array> = []; + try { console.log(' 🔄 Fetching related speeches...'); - const speeches: unknown = await client.searchSpeeches({ sok: eventData.topic }); - mcpCalls.push({ tool: 'search_anforanden', result: speeches }); - - // Fetch MP profiles for speaker context using names from speeches + const speeches = await client.searchSpeeches(eventData.topic ? { text: eventData.topic, limit: 10 } : { limit: 5 }); + speechList = Array.isArray(speeches) ? speeches as Array> : []; + mcpCalls.push({ tool: 'search_anforanden', result: speechList }); + } catch (err) { + console.warn(' ⚠ search_anforanden unavailable:', (err as Error).message); + mcpCalls.push({ tool: 'search_anforanden', result: [] }); + } + + // Tool 4: search_ledamoter — MP profiles for speaker context + try { console.log(' 🔄 Fetching MP profiles for speaker context...'); - const speechList = Array.isArray(speeches) ? speeches as Array> : []; const speakerName = speechList[0]?.['talare'] as string | undefined; const mps: unknown = await client.fetchMPs(speakerName ? { namn: speakerName, limit: 1 } : { limit: 5 }); mcpCalls.push({ tool: 'search_ledamoter', result: mps }); + } catch (err) { + console.warn(' ⚠ search_ledamoter unavailable:', (err as Error).message); + mcpCalls.push({ tool: 'search_ledamoter', result: [] }); } const today = new Date(); From fb8366d42c46eea41637850ec54fcd6f578e3e51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:55:12 +0000 Subject: [PATCH 3/5] test: add unconditional tool coverage and rename misleading test titles Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- tests/news-types/breaking-news.test.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/news-types/breaking-news.test.ts b/tests/news-types/breaking-news.test.ts index 3fc369bd..894e6bb0 100644 --- a/tests/news-types/breaking-news.test.ts +++ b/tests/news-types/breaking-news.test.ts @@ -139,7 +139,7 @@ describe('Breaking News Article Generation', () => { expect(mockClientInstance.searchSpeeches).toHaveBeenCalled(); }); - it('should fetch voting group if voteId provided', async () => { + it('should always fetch voting group (enriched with voteId when provided)', async () => { const eventData: BreakingEventData = { voteId: 'v123', slug: 'test' @@ -153,7 +153,7 @@ describe('Breaking News Article Generation', () => { expect(mockClientInstance.fetchVotingGroup).toHaveBeenCalled(); }); - it('should fetch MP profiles if topic provided', async () => { + it('should always fetch MP profiles (enriched with speaker name when topic provided)', async () => { const eventData: BreakingEventData = { topic: 'Budget debate', slug: 'test' @@ -166,6 +166,22 @@ describe('Breaking News Article Generation', () => { expect(mockClientInstance.fetchMPs).toHaveBeenCalled(); }); + + it('should call all 4 required tools even with minimal event data', async () => { + const eventData: BreakingEventData = { + slug: 'test' + }; + + await breakingNewsModule.generateBreakingNews({ + languages: ['en'], + eventData + }); + + expect(mockClientInstance.fetchVotingRecords).toHaveBeenCalled(); + expect(mockClientInstance.fetchVotingGroup).toHaveBeenCalled(); + expect(mockClientInstance.searchSpeeches).toHaveBeenCalled(); + expect(mockClientInstance.fetchMPs).toHaveBeenCalled(); + }); }); describe('Article Structure', () => { From 89c37531035f65242aec642e509af23a44d1f806 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:11:22 +0000 Subject: [PATCH 4/5] docs+test: fix doc comments to reflect unconditional tool calls; assert fetchVotingGroup params Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- scripts/news-types/breaking-news.ts | 11 ++++++----- tests/news-types/breaking-news.test.ts | 7 ++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/scripts/news-types/breaking-news.ts b/scripts/news-types/breaking-news.ts index ddd7edcf..e4ac6981 100644 --- a/scripts/news-types/breaking-news.ts +++ b/scripts/news-types/breaking-news.ts @@ -40,7 +40,8 @@ * * **IMPLEMENTATION STATUS:** * - All four MCP tools implemented: search_voteringar, get_voting_group, - * search_anforanden, search_ledamoter (all conditional on event data) + * search_anforanden, search_ledamoter (all called unconditionally; + * parameters are enriched when voteId/topic are present in event data) * * **INTELLIGENCE ANALYSIS FRAMEWORK:** * Breaking news articles incorporate: @@ -167,10 +168,10 @@ import type { * Required MCP tools for breaking news articles * * IMPLEMENTATION STATUS: - * - search_voteringar: ✅ Implemented (conditional on voteId) - * - get_voting_group: ✅ Implemented (conditional on voteId) - * - search_anforanden: ✅ Implemented (conditional on topic) - * - search_ledamoter: ✅ Implemented (conditional on topic) + * - search_voteringar: ✅ Implemented (always called; parameters enriched when voteId is present) + * - get_voting_group: ✅ Implemented (always called; parameters enriched when voteId is present) + * - search_anforanden: ✅ Implemented (always called; parameters enriched when topic is present) + * - search_ledamoter: ✅ Implemented (always called; parameters enriched when topic is present) */ export const REQUIRED_TOOLS: readonly string[] = [ 'search_voteringar', diff --git a/tests/news-types/breaking-news.test.ts b/tests/news-types/breaking-news.test.ts index 894e6bb0..07e68da9 100644 --- a/tests/news-types/breaking-news.test.ts +++ b/tests/news-types/breaking-news.test.ts @@ -150,7 +150,12 @@ describe('Breaking News Article Generation', () => { eventData }); - expect(mockClientInstance.fetchVotingGroup).toHaveBeenCalled(); + expect(mockClientInstance.fetchVotingGroup).toHaveBeenCalledWith( + expect.objectContaining({ + punkt: eventData.voteId, + groupBy: 'parti' + }) + ); }); it('should always fetch MP profiles (enriched with speaker name when topic provided)', async () => { From 4812ca3def41e52e042964922315a193556c413d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:48:39 +0000 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20rename=20FetchVotingGroupParams?= =?UTF-8?q?=E2=86=92Filters;=20fix=20ledamoter=20doc;=20test=20MP=20enrich?= =?UTF-8?q?ment=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- scripts/mcp-client/client.ts | 4 ++-- scripts/news-types/breaking-news.ts | 5 +++-- scripts/types/mcp.ts | 4 ++-- tests/news-types/breaking-news.test.ts | 14 ++++++++++++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/scripts/mcp-client/client.ts b/scripts/mcp-client/client.ts index 716fdbc7..34d52c32 100644 --- a/scripts/mcp-client/client.ts +++ b/scripts/mcp-client/client.ts @@ -17,7 +17,7 @@ import type { SearchSpeechesParams, FetchMPsFilters, FetchVotingFilters, - FetchVotingGroupParams, + FetchVotingGroupFilters, GovDocSearchParams, RiksdagDocument, } from '../types/mcp.js'; @@ -468,7 +468,7 @@ export class MCPClient { return (response['votes'] ?? []) as unknown[]; } - async fetchVotingGroup(params: FetchVotingGroupParams = {}): Promise { + async fetchVotingGroup(params: FetchVotingGroupFilters = {}): Promise { const response = await this.request( 'get_voting_group', params as unknown as Record, diff --git a/scripts/news-types/breaking-news.ts b/scripts/news-types/breaking-news.ts index e4ac6981..6fa8f031 100644 --- a/scripts/news-types/breaking-news.ts +++ b/scripts/news-types/breaking-news.ts @@ -41,7 +41,8 @@ * **IMPLEMENTATION STATUS:** * - All four MCP tools implemented: search_voteringar, get_voting_group, * search_anforanden, search_ledamoter (all called unconditionally; - * parameters are enriched when voteId/topic are present in event data) + * parameters are enriched when voteId is present for voting tools, and + * when a speaker name is found in speech results for search_ledamoter) * * **INTELLIGENCE ANALYSIS FRAMEWORK:** * Breaking news articles incorporate: @@ -171,7 +172,7 @@ import type { * - search_voteringar: ✅ Implemented (always called; parameters enriched when voteId is present) * - get_voting_group: ✅ Implemented (always called; parameters enriched when voteId is present) * - search_anforanden: ✅ Implemented (always called; parameters enriched when topic is present) - * - search_ledamoter: ✅ Implemented (always called; parameters enriched when topic is present) + * - search_ledamoter: ✅ Implemented (always called; parameters enriched when a speaker name is found in search_anforanden results) */ export const REQUIRED_TOOLS: readonly string[] = [ 'search_voteringar', diff --git a/scripts/types/mcp.ts b/scripts/types/mcp.ts index 82b68552..2a426a62 100644 --- a/scripts/types/mcp.ts +++ b/scripts/types/mcp.ts @@ -92,8 +92,8 @@ export interface FetchVotingFilters { [key: string]: unknown; } -/** Parameters for party-level voting group lookup */ -export interface FetchVotingGroupParams { +/** Filters for party-level voting group lookup */ +export interface FetchVotingGroupFilters { bet?: string; punkt?: string; groupBy?: 'parti' | 'valkrets' | 'namn'; diff --git a/tests/news-types/breaking-news.test.ts b/tests/news-types/breaking-news.test.ts index 07e68da9..d31d75b4 100644 --- a/tests/news-types/breaking-news.test.ts +++ b/tests/news-types/breaking-news.test.ts @@ -158,18 +158,28 @@ describe('Breaking News Article Generation', () => { ); }); - it('should always fetch MP profiles (enriched with speaker name when topic provided)', async () => { + it('should always fetch MP profiles (enriched with speaker name from speech results)', async () => { const eventData: BreakingEventData = { topic: 'Budget debate', slug: 'test' }; + + const mockSpeakerName = 'Jane Doe'; + mockClientInstance.searchSpeeches.mockResolvedValueOnce([ + { talare: mockSpeakerName } as unknown as Record + ]); await breakingNewsModule.generateBreakingNews({ languages: ['en'], eventData }); - expect(mockClientInstance.fetchMPs).toHaveBeenCalled(); + expect(mockClientInstance.fetchMPs).toHaveBeenCalledWith( + expect.objectContaining({ + namn: mockSpeakerName, + limit: 1 + }) + ); }); it('should call all 4 required tools even with minimal event data', async () => {