euparliamentmonitor 0.8.56 → 0.8.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -136,7 +136,7 @@ The published site is the audience-facing companion to this npm/TypeScript packa
136
136
 
137
137
  **MCP Server Integration**: The project uses the
138
138
  [European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server)
139
- v1.2.20 for accessing real EU Parliament data via the Model Context Protocol.
139
+ v1.2.21 for accessing real EU Parliament data via the Model Context Protocol.
140
140
 
141
141
  - **MCP Server Status**: ✅ Fully operational — 60+ EP data tools available
142
142
  (feeds, direct lookups, analytical tools, intelligence correlation)
@@ -432,7 +432,7 @@ import type { ArticleCategory, LanguageCode } from 'euparliamentmonitor/types';
432
432
 
433
433
  ## 🔌 Data Sources
434
434
 
435
- **Primary — European Parliament MCP Server** ([Hack23/European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server) v1.2.20+, fully operational):
435
+ **Primary — European Parliament MCP Server** ([Hack23/European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server) v1.2.21+, fully operational):
436
436
 
437
437
  - 🗳️ Plenary sessions, voting records, roll-call votes
438
438
  - 📜 Adopted texts, motions, resolutions, urgency files
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "euparliamentmonitor",
3
- "version": "0.8.56",
3
+ "version": "0.8.58",
4
4
  "type": "module",
5
5
  "description": "European Parliament Intelligence Platform - Monitor political activity with systematic transparency",
6
6
  "main": "scripts/index.js",
@@ -140,8 +140,8 @@
140
140
  "@types/markdown-it": "^14.1.2",
141
141
  "@types/node": "25.6.0",
142
142
  "@types/papaparse": "5.5.2",
143
- "@typescript-eslint/eslint-plugin": "8.59.1",
144
- "@typescript-eslint/parser": "8.59.1",
143
+ "@typescript-eslint/eslint-plugin": "8.59.2",
144
+ "@typescript-eslint/parser": "8.59.2",
145
145
  "@vitest/coverage-v8": "4.1.5",
146
146
  "@vitest/ui": "4.1.5",
147
147
  "chart.js": "4.5.1",
@@ -171,7 +171,7 @@
171
171
  "node": ">=25"
172
172
  },
173
173
  "dependencies": {
174
- "european-parliament-mcp-server": "1.2.20",
174
+ "european-parliament-mcp-server": "1.2.21",
175
175
  "markdown-it": "^14.1.1",
176
176
  "markdown-it-anchor": "^9.2.0",
177
177
  "markdown-it-attrs": "^4.3.1",
@@ -18,9 +18,9 @@
18
18
  * `11.14.0`); regenerating articles after a Mermaid bump invalidates
19
19
  * browser and CloudFront caches automatically.
20
20
  */
21
- import { BASE_URL, MERMAID_VERSION } from '../constants/config.js';
21
+ import { BASE_URL, BUILD_SHORT, MERMAID_VERSION } from '../constants/config.js';
22
22
  import { buildHeadFreshnessTags } from '../constants/build-info-meta.js';
23
- import { ALL_LANGUAGES, LANGUAGE_NAMES, LANGUAGE_FLAGS, PAGE_TITLES, SKIP_LINK_TEXTS, TOC_ARIA_LABELS, UPDATE_AVAILABLE_LABELS, UPDATE_REFRESH_CTA_LABELS, UPDATE_DISMISS_LABELS, getLocalizedString, getTextDirection, } from '../constants/languages.js';
23
+ import { ALL_LANGUAGES, LANGUAGE_NAMES, LANGUAGE_FLAGS, PAGE_TITLES, SKIP_LINK_TEXTS, TOC_ARIA_LABELS, getLocalizedString, getTextDirection, } from '../constants/languages.js';
24
24
  import { escapeHTML } from '../utils/file-utils.js';
25
25
  import { buildSiteFooter, buildSiteHeader, buildPageBanner, } from '../templates/section-builders.js';
26
26
  import { READER_GUIDE_SECTION_ID } from './reader-guide-constants.js';
@@ -233,10 +233,7 @@ ${hreflangLinks}
233
233
  <link rel="apple-touch-icon" sizes="180x180" href="../images/apple-touch-icon.png">
234
234
  <link rel="manifest" href="../site.webmanifest">
235
235
  <meta name="theme-color" content="#003399">
236
- <link rel="stylesheet" href="../styles.css">
237
- <meta name="ep-i18n-update-text" content="${escapeHTML(getLocalizedString(UPDATE_AVAILABLE_LABELS, safeLang))}">
238
- <meta name="ep-i18n-update-cta" content="${escapeHTML(getLocalizedString(UPDATE_REFRESH_CTA_LABELS, safeLang))}">
239
- <meta name="ep-i18n-dismiss" content="${escapeHTML(getLocalizedString(UPDATE_DISMISS_LABELS, safeLang))}">
236
+ <link rel="stylesheet" href="../styles.css?v=${BUILD_SHORT}">
240
237
  ${buildHeadFreshnessTags('../')}
241
238
  <script type="application/ld+json">${jsonLdString}</script>
242
239
  <script type="module" src="../js/mermaid-init.js?v=${MERMAID_VERSION}" defer></script>
@@ -24,12 +24,24 @@ export interface ArtifactSection {
24
24
  readonly artifacts: readonly string[];
25
25
  }
26
26
  /**
27
- * Canonical ordering. Order matches the plan:
28
- * 1. Executive brief 2. Synthesis 3. Significance 4. Actors & forces
29
- * 5. Coalitions 6. Stakeholders 7. PESTLE 8. Economic
30
- * 9. Risk 10. Threat 11. Scenarios 12. Continuity
31
- * 13. Deep analysis 14. Documents 15. Extended 16. MCP audit
32
- * 17. Quality & reflection.
27
+ * Canonical ordering. Order is shaped for a political-intelligence
28
+ * journalist: news-arc first (story stakes → forecast), then deep
29
+ * structural context, then provenance/audit appendices:
30
+ *
31
+ * 1. Executive brief 2. Synthesis 3. Significance
32
+ * 4. Actors & forces 5. Coalitions 6. Stakeholders
33
+ * 7. Economic 8. Risk 9. Threat
34
+ * 10. Scenarios 11. Forward (What to Watch) 12. Electoral arc
35
+ * 13. PESTLE & history 14. Continuity 15. Deep analysis
36
+ * 16. Documents 17. Extended 18. MCP audit
37
+ * 19. Quality & reflection.
38
+ *
39
+ * Rationale: Stakeholder Map (who is affected) flows directly into
40
+ * Economic Context (macro stakes) without the structural-frame
41
+ * detour of PESTLE. Risk → Threat → Scenarios → Forward Projection →
42
+ * Electoral Arc forms a coherent "consequences and forecast" block.
43
+ * PESTLE & historical-baseline then opens the deep-context section
44
+ * for readers who want structural backdrop after the news.
33
45
  */
34
46
  export declare const ARTIFACT_SECTIONS: readonly ArtifactSection[];
35
47
  /** Id of the catch-all bucket for artifacts not matched by any section. */
@@ -1,12 +1,24 @@
1
1
  // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  /**
4
- * Canonical ordering. Order matches the plan:
5
- * 1. Executive brief 2. Synthesis 3. Significance 4. Actors & forces
6
- * 5. Coalitions 6. Stakeholders 7. PESTLE 8. Economic
7
- * 9. Risk 10. Threat 11. Scenarios 12. Continuity
8
- * 13. Deep analysis 14. Documents 15. Extended 16. MCP audit
9
- * 17. Quality & reflection.
4
+ * Canonical ordering. Order is shaped for a political-intelligence
5
+ * journalist: news-arc first (story stakes → forecast), then deep
6
+ * structural context, then provenance/audit appendices:
7
+ *
8
+ * 1. Executive brief 2. Synthesis 3. Significance
9
+ * 4. Actors & forces 5. Coalitions 6. Stakeholders
10
+ * 7. Economic 8. Risk 9. Threat
11
+ * 10. Scenarios 11. Forward (What to Watch) 12. Electoral arc
12
+ * 13. PESTLE & history 14. Continuity 15. Deep analysis
13
+ * 16. Documents 17. Extended 18. MCP audit
14
+ * 19. Quality & reflection.
15
+ *
16
+ * Rationale: Stakeholder Map (who is affected) flows directly into
17
+ * Economic Context (macro stakes) without the structural-frame
18
+ * detour of PESTLE. Risk → Threat → Scenarios → Forward Projection →
19
+ * Electoral Arc forms a coherent "consequences and forecast" block.
20
+ * PESTLE & historical-baseline then opens the deep-context section
21
+ * for readers who want structural backdrop after the news.
10
22
  */
11
23
  export const ARTIFACT_SECTIONS = [
12
24
  {
@@ -58,11 +70,6 @@ export const ARTIFACT_SECTIONS = [
58
70
  title: 'Stakeholder Map',
59
71
  artifacts: ['intelligence/stakeholder-map.md', 'existing/stakeholder-impact.md'],
60
72
  },
61
- {
62
- id: 'pestle-context',
63
- title: 'PESTLE & Context',
64
- artifacts: ['intelligence/pestle-analysis.md', 'intelligence/historical-baseline.md'],
65
- },
66
73
  {
67
74
  id: 'economic-context',
68
75
  title: 'Economic Context',
@@ -120,6 +127,11 @@ export const ARTIFACT_SECTIONS = [
120
127
  'intelligence/commission-wp-alignment.md',
121
128
  ],
122
129
  },
130
+ {
131
+ id: 'pestle-context',
132
+ title: 'PESTLE & Context',
133
+ artifacts: ['intelligence/pestle-analysis.md', 'intelligence/historical-baseline.md'],
134
+ },
123
135
  {
124
136
  id: 'continuity',
125
137
  title: 'Cross-Run Continuity',
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Build the shared freshness/PWA `<head>` block.
2
+ * Build the shared build-identity + PWA `<head>` block.
3
3
  *
4
4
  * @param pathPrefix - Asset path prefix (`''` for root pages, `'../'`
5
5
  * for `news/` pages).
@@ -2,13 +2,17 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  /**
4
4
  * @module Constants/BuildInfoMeta
5
- * @description Shared helper that emits the `<head>` freshness tags every
6
- * generator must include so the PWA layer (`js/pwa-register.js`) can:
5
+ * @description Shared helper that emits the `<head>` build-identity and PWA
6
+ * script tags every generator must include.
7
7
  *
8
- * - Read the embedded build commit SHA + timestamp from `<meta>` tags.
9
- * - Load the same-origin service-worker registration script.
10
- * - Tell browsers + intermediate proxies to revalidate every navigation
11
- * (`Cache-Control: no-cache`, `Pragma: no-cache`).
8
+ * The `build-id` / `build-time` meta tags are informational only they
9
+ * identify which deploy produced the page (useful for debugging / cache
10
+ * audits) but are **not** consumed by any client-side JavaScript.
11
+ *
12
+ * The `pwa-register.js` script tag loads the service-worker registration
13
+ * and relative-time formatting IIFE. A `?v=<BUILD_SHORT>` query string
14
+ * is appended so that immutable-cached old versions are automatically
15
+ * evicted when a new build is deployed.
12
16
  *
13
17
  * Every value is HTML-escaped — `BUILD_ID`/`BUILD_TIME` are tightly
14
18
  * formatted (40-char hex / ISO 8601) but defence-in-depth is cheap.
@@ -18,10 +22,10 @@
18
22
  * 'self'` because the only emitted `<script>` references a same-origin
19
23
  * file with a `defer` attribute.
20
24
  */
21
- import { BUILD_ID, BUILD_TIME } from './config.js';
25
+ import { BUILD_ID, BUILD_SHORT, BUILD_TIME } from './config.js';
22
26
  import { escapeHTML } from '../utils/file-utils.js';
23
27
  /**
24
- * Build the shared freshness/PWA `<head>` block.
28
+ * Build the shared build-identity + PWA `<head>` block.
25
29
  *
26
30
  * @param pathPrefix - Asset path prefix (`''` for root pages, `'../'`
27
31
  * for `news/` pages).
@@ -31,15 +35,14 @@ import { escapeHTML } from '../utils/file-utils.js';
31
35
  export function buildHeadFreshnessTags(pathPrefix) {
32
36
  const safeBuildId = escapeHTML(BUILD_ID);
33
37
  const safeBuildTime = escapeHTML(BUILD_TIME);
38
+ const safeBuildShort = escapeHTML(BUILD_SHORT);
34
39
  // Path prefix is built from controlled string literals (`''` or `'../'`),
35
40
  // but escape it anyway to keep the helper safe under future callers.
36
41
  const safePrefix = escapeHTML(pathPrefix);
37
42
  return [
38
43
  ` <meta name="build-id" content="${safeBuildId}">`,
39
44
  ` <meta name="build-time" content="${safeBuildTime}">`,
40
- ` <meta http-equiv="Cache-Control" content="no-cache">`,
41
- ` <meta http-equiv="Pragma" content="no-cache">`,
42
- ` <script src="${safePrefix}js/pwa-register.js" defer></script>`,
45
+ ` <script src="${safePrefix}js/pwa-register.js?v=${safeBuildShort}" defer></script>`,
43
46
  ].join('\n');
44
47
  }
45
48
  //# sourceMappingURL=build-info-meta.js.map
@@ -9,10 +9,10 @@
9
9
  */
10
10
  import path, { resolve } from 'path';
11
11
  import { pathToFileURL } from 'url';
12
- import { PROJECT_ROOT, APP_VERSION, NEWS_DIR, BASE_URL } from '../constants/config.js';
12
+ import { PROJECT_ROOT, APP_VERSION, BUILD_SHORT, NEWS_DIR, BASE_URL } from '../constants/config.js';
13
13
  import { getNewsIndexSeo } from './seo-copy.js';
14
14
  import { buildHeadFreshnessTags } from '../constants/build-info-meta.js';
15
- import { ALL_LANGUAGES, LANGUAGE_NAMES, LANGUAGE_FLAGS, PAGE_TITLES, PAGE_DESCRIPTIONS, SECTION_HEADINGS, NO_ARTICLES_MESSAGES, SKIP_LINK_TEXTS, AI_SECTION_CONTENT, FILTER_LABELS, ARTICLE_TYPE_LABELS, HEADER_SUBTITLE_LABELS, UPDATE_AVAILABLE_LABELS, UPDATE_REFRESH_CTA_LABELS, UPDATE_DISMISS_LABELS, getLocalizedString, getTextDirection, } from '../constants/languages.js';
15
+ import { ALL_LANGUAGES, LANGUAGE_NAMES, LANGUAGE_FLAGS, PAGE_TITLES, PAGE_DESCRIPTIONS, SECTION_HEADINGS, NO_ARTICLES_MESSAGES, SKIP_LINK_TEXTS, AI_SECTION_CONTENT, FILTER_LABELS, ARTICLE_TYPE_LABELS, HEADER_SUBTITLE_LABELS, getLocalizedString, getTextDirection, } from '../constants/languages.js';
16
16
  import { buildSiteFooter, buildSiteHeader } from '../templates/section-builders.js';
17
17
  import { getNewsArticles, groupArticlesByLanguage, formatSlug, parseArticleFilename, extractArticleMeta, escapeHTML, atomicWrite, } from '../utils/file-utils.js';
18
18
  import { writeMetadataDatabase } from '../utils/news-metadata.js';
@@ -294,10 +294,7 @@ export function generateIndexHTML(lang, articles, metaMap = new Map()) {
294
294
  <link rel="manifest" href="site.webmanifest">
295
295
  <meta name="theme-color" content="#003399">
296
296
  <link rel="alternate" type="application/rss+xml" title="EU Parliament Monitor RSS" href="rss.xml">
297
- <link rel="stylesheet" href="styles.css">
298
- <meta name="ep-i18n-update-text" content="${escapeHTML(getLocalizedString(UPDATE_AVAILABLE_LABELS, lang))}">
299
- <meta name="ep-i18n-update-cta" content="${escapeHTML(getLocalizedString(UPDATE_REFRESH_CTA_LABELS, lang))}">
300
- <meta name="ep-i18n-dismiss" content="${escapeHTML(getLocalizedString(UPDATE_DISMISS_LABELS, lang))}">
297
+ <link rel="stylesheet" href="styles.css?v=${BUILD_SHORT}">
301
298
  ${buildHeadFreshnessTags('')}
302
299
  <script type="application/ld+json">${websiteJsonLd}</script>
303
300
  <script type="application/ld+json">${organizationJsonLd}</script>
@@ -12,9 +12,9 @@
12
12
  * link points at GitHub blob/tree URLs so readers can audit the raw
13
13
  * tradecraft behind every published article.
14
14
  */
15
- import { BASE_URL, THEME_TOGGLE_SCRIPT } from '../../constants/config.js';
15
+ import { BASE_URL, BUILD_SHORT, THEME_TOGGLE_SCRIPT } from '../../constants/config.js';
16
16
  import { buildHeadFreshnessTags } from '../../constants/build-info-meta.js';
17
- import { ALL_LANGUAGES, LANGUAGE_FLAGS, LANGUAGE_NAMES, PAGE_TITLES, SKIP_LINK_TEXTS, UPDATE_AVAILABLE_LABELS, UPDATE_REFRESH_CTA_LABELS, UPDATE_DISMISS_LABELS, getLocalizedString, getTextDirection, } from '../../constants/languages.js';
17
+ import { ALL_LANGUAGES, LANGUAGE_FLAGS, LANGUAGE_NAMES, PAGE_TITLES, SKIP_LINK_TEXTS, getLocalizedString, getTextDirection, } from '../../constants/languages.js';
18
18
  import { FOOTER_SITEMAP_LABELS } from '../../constants/language-ui.js';
19
19
  import { buildSiteFooter, buildSiteHeader, buildPageBanner, } from '../../templates/section-builders.js';
20
20
  import { escapeHTML } from '../../utils/file-utils.js';
@@ -418,10 +418,7 @@ ${hreflangLinks}
418
418
  <link rel="apple-touch-icon" sizes="180x180" href="images/apple-touch-icon.png">
419
419
  <link rel="manifest" href="site.webmanifest">
420
420
  <meta name="theme-color" content="#003399">
421
- <link rel="stylesheet" href="styles.css">
422
- <meta name="ep-i18n-update-text" content="${escapeHTML(getLocalizedString(UPDATE_AVAILABLE_LABELS, lang))}">
423
- <meta name="ep-i18n-update-cta" content="${escapeHTML(getLocalizedString(UPDATE_REFRESH_CTA_LABELS, lang))}">
424
- <meta name="ep-i18n-dismiss" content="${escapeHTML(getLocalizedString(UPDATE_DISMISS_LABELS, lang))}">
421
+ <link rel="stylesheet" href="styles.css?v=${BUILD_SHORT}">
425
422
  ${buildHeadFreshnessTags('')}
426
423
  <script type="application/ld+json">${websiteJsonLd}</script>
427
424
  <script type="application/ld+json">${organizationJsonLd}</script>
@@ -19,10 +19,10 @@
19
19
  * `test/unit/sitemap-byte-equality.test.js` (compares against the
20
20
  * golden snapshots taken from `npm run prebuild`).
21
21
  */
22
- import { BASE_URL, THEME_TOGGLE_SCRIPT } from '../../constants/config.js';
22
+ import { BASE_URL, BUILD_SHORT, THEME_TOGGLE_SCRIPT } from '../../constants/config.js';
23
23
  import { buildHeadFreshnessTags } from '../../constants/build-info-meta.js';
24
24
  import { getSitemapSeo } from '../seo-copy.js';
25
- import { ALL_LANGUAGES, LANGUAGE_NAMES, LANGUAGE_FLAGS, PAGE_TITLES, PAGE_DESCRIPTIONS, SKIP_LINK_TEXTS, UPDATE_AVAILABLE_LABELS, UPDATE_REFRESH_CTA_LABELS, UPDATE_DISMISS_LABELS, getLocalizedString, getTextDirection, } from '../../constants/languages.js';
25
+ import { ALL_LANGUAGES, LANGUAGE_NAMES, LANGUAGE_FLAGS, PAGE_TITLES, PAGE_DESCRIPTIONS, SKIP_LINK_TEXTS, getLocalizedString, getTextDirection, } from '../../constants/languages.js';
26
26
  import { escapeHTML } from '../../utils/file-utils.js';
27
27
  import { detectCategory } from '../../utils/article-category.js';
28
28
  import { ARTICLE_TYPE_LABELS, FOOTER_POLITICAL_INTELLIGENCE_LABELS, } from '../../constants/language-ui.js';
@@ -311,10 +311,7 @@ ${hreflangLinks}
311
311
  <link rel="apple-touch-icon" sizes="180x180" href="images/apple-touch-icon.png">
312
312
  <link rel="manifest" href="site.webmanifest">
313
313
  <meta name="theme-color" content="#003399">
314
- <link rel="stylesheet" href="styles.css">
315
- <meta name="ep-i18n-update-text" content="${escapeHTML(getLocalizedString(UPDATE_AVAILABLE_LABELS, lang))}">
316
- <meta name="ep-i18n-update-cta" content="${escapeHTML(getLocalizedString(UPDATE_REFRESH_CTA_LABELS, lang))}">
317
- <meta name="ep-i18n-dismiss" content="${escapeHTML(getLocalizedString(UPDATE_DISMISS_LABELS, lang))}">
314
+ <link rel="stylesheet" href="styles.css?v=${BUILD_SHORT}">
318
315
  ${buildHeadFreshnessTags('')}
319
316
  <script type="application/ld+json">${websiteJsonLd}</script>
320
317
  <script type="application/ld+json">${organizationJsonLd}</script>
@@ -7,7 +7,7 @@ import { MCPConnection } from './mcp-connection.js';
7
7
  import type { MCPClientOptions, MCPToolResult, GetMEPsOptions, GetPlenarySessionsOptions, SearchDocumentsOptions, GetParliamentaryQuestionsOptions, GetCommitteeInfoOptions, MonitorLegislativePipelineOptions, AssessMEPInfluenceOptions, AnalyzeCoalitionDynamicsOptions, DetectVotingAnomaliesOptions, ComparePoliticalGroupsOptions, VotingRecordsOptions, VotingPatternsOptions, GenerateReportOptions, AnalyzeLegislativeEffectivenessOptions, AnalyzeCommitteeActivityOptions, TrackMEPAttendanceOptions, AnalyzeCountryDelegationOptions, GeneratePoliticalLandscapeOptions, GetCurrentMEPsOptions, GetSpeechesOptions, GetProceduresOptions, GetAdoptedTextsOptions, GetEventsOptions, GetMeetingActivitiesOptions, GetMeetingDecisionsOptions, GetMEPDeclarationsOptions, GetIncomingMEPsOptions, GetOutgoingMEPsOptions, GetHomonymMEPsOptions, GetPlenaryDocumentsOptions, GetCommitteeDocumentsOptions, GetPlenarySessionDocumentsOptions, GetPlenarySessionDocumentItemsOptions, GetControlledVocabulariesOptions, GetExternalDocumentsOptions, GetMeetingForeseenActivitiesOptions, GetProcedureEventsOptions, GetMeetingPlenarySessionDocumentsOptions, GetMeetingPlenarySessionDocumentItemsOptions, NetworkAnalysisOptions, SentimentTrackerOptions, EarlyWarningSystemOptions, ComparativeIntelligenceOptions, CorrelateIntelligenceOptions, GetAllGeneratedStatsOptions, GetMEPsFeedOptions, GetEventsFeedOptions, GetProceduresFeedOptions, GetAdoptedTextsFeedOptions, GetMEPDeclarationsFeedOptions, GetDocumentsFeedOptions, GetPlenaryDocumentsFeedOptions, GetCommitteeDocumentsFeedOptions, GetPlenarySessionDocumentsFeedOptions, GetExternalDocumentsFeedOptions, GetParliamentaryQuestionsFeedOptions, GetCorporateBodiesFeedOptions, GetControlledVocabulariesFeedOptions, GetProcedureEventByIdOptions, GetFreshProceduresOptions } from '../types/index.js';
8
8
  /**
9
9
  * Canonical list of tools exposed by the European Parliament MCP gateway
10
- * (`european-parliament-mcp-server@1.2.20`). The news workflows, prompt
10
+ * (`european-parliament-mcp-server@1.2.21`). The news workflows, prompt
11
11
  * library (`.github/prompts/07-mcp-reference.md`), and the integration test
12
12
  * suite all reference this list so a regression that adds/removes a tool
13
13
  * fails a single drift guard
@@ -22,7 +22,7 @@ export declare const EP_MCP_TOOLS: readonly string[];
22
22
  * covering the two shapes historically emitted by the EP MCP server.
23
23
  *
24
24
  * 1. **Uniform envelope** (all feeds as of
25
- * `european-parliament-mcp-server@1.2.20`) —
25
+ * `european-parliament-mcp-server@1.2.21`) —
26
26
  * `{status:"unavailable", items:[], generatedAt:"..."}` established by
27
27
  * Hack23/European-Parliament-MCP-Server#301 and extended to
28
28
  * `get_events_feed`/`get_procedures_feed` by
@@ -206,9 +206,9 @@ export declare class EuropeanParliamentMCPClient extends MCPConnection {
206
206
  *
207
207
  * @remarks
208
208
  * This repository is currently documented/configured against
209
- * `european-parliament-mcp-server@1.2.20`.
209
+ * `european-parliament-mcp-server@1.2.21`.
210
210
  *
211
- * **Upstream date-filter contract (v1.2.14+, active on the pinned v1.2.20 server):** the upstream server
211
+ * **Upstream date-filter contract (v1.2.14+, active on the pinned v1.2.21 server):** the upstream server
212
212
  * applies a server-side post-filter on `dateFrom`/`dateTo` before serialisation, because the
213
213
  * EP Open Data Portal `/meetings` endpoint silently ignores its `date-from`/`date-to` query
214
214
  * parameters (Defect #5). Under this contract:
@@ -217,7 +217,7 @@ export declare class EuropeanParliamentMCPClient extends MCPConnection {
217
217
  * - Per-window session counts are reproducible because the EP-side regression is masked by
218
218
  * the upstream post-filter.
219
219
  *
220
- * No local post-filter is applied here. The repository is pinned to v1.2.20, so the
220
+ * No local post-filter is applied here. The repository is pinned to v1.2.21, so the
221
221
  * date-filter guarantees above apply; consumers running against an older server image
222
222
  * (pre-v1.2.14) must not assume them.
223
223
  */
@@ -11,7 +11,7 @@ import { recordPendingDocument, markDocumentResolved, getPendingDocumentsForRepr
11
11
  import { EP_NEXT_ELECTION_START, EP_NEXT_ELECTION_END, EP_CURRENT_TERM, EP_NEXT_TERM, } from '../constants/config.js';
12
12
  /**
13
13
  * Canonical list of tools exposed by the European Parliament MCP gateway
14
- * (`european-parliament-mcp-server@1.2.20`). The news workflows, prompt
14
+ * (`european-parliament-mcp-server@1.2.21`). The news workflows, prompt
15
15
  * library (`.github/prompts/07-mcp-reference.md`), and the integration test
16
16
  * suite all reference this list so a regression that adds/removes a tool
17
17
  * fails a single drift guard
@@ -115,7 +115,7 @@ const CONTENT_NOT_YET_AVAILABLE_SUBSTRING = 'document indexed but content not ye
115
115
  /**
116
116
  * Classify an error message into a diagnostic error category.
117
117
  *
118
- * Maps EP MCP Server v1.2.20 structured error codes and generic HTTP/network
118
+ * Maps EP MCP Server v1.2.21 structured error codes and generic HTTP/network
119
119
  * errors into one of six broad categories used for logging and retry decisions:
120
120
  *
121
121
  * Returned categories (priority order):
@@ -131,7 +131,7 @@ const CONTENT_NOT_YET_AVAILABLE_SUBSTRING = 'document indexed but content not ye
131
131
  */
132
132
  function classifyToolError(message) {
133
133
  const lowerMsg = message.toLowerCase();
134
- // EP MCP Server v1.2.20 structured error codes (matched case-insensitively)
134
+ // EP MCP Server v1.2.21 structured error codes (matched case-insensitively)
135
135
  if (lowerMsg.includes('internal_error')) {
136
136
  return 'INTERNAL_ERROR';
137
137
  }
@@ -190,7 +190,7 @@ function _parseResultPayload(result) {
190
190
  * covering the two shapes historically emitted by the EP MCP server.
191
191
  *
192
192
  * 1. **Uniform envelope** (all feeds as of
193
- * `european-parliament-mcp-server@1.2.20`) —
193
+ * `european-parliament-mcp-server@1.2.21`) —
194
194
  * `{status:"unavailable", items:[], generatedAt:"..."}` established by
195
195
  * Hack23/European-Parliament-MCP-Server#301 and extended to
196
196
  * `get_events_feed`/`get_procedures_feed` by
@@ -591,9 +591,9 @@ export class EuropeanParliamentMCPClient extends MCPConnection {
591
591
  *
592
592
  * @remarks
593
593
  * This repository is currently documented/configured against
594
- * `european-parliament-mcp-server@1.2.20`.
594
+ * `european-parliament-mcp-server@1.2.21`.
595
595
  *
596
- * **Upstream date-filter contract (v1.2.14+, active on the pinned v1.2.20 server):** the upstream server
596
+ * **Upstream date-filter contract (v1.2.14+, active on the pinned v1.2.21 server):** the upstream server
597
597
  * applies a server-side post-filter on `dateFrom`/`dateTo` before serialisation, because the
598
598
  * EP Open Data Portal `/meetings` endpoint silently ignores its `date-from`/`date-to` query
599
599
  * parameters (Defect #5). Under this contract:
@@ -602,7 +602,7 @@ export class EuropeanParliamentMCPClient extends MCPConnection {
602
602
  * - Per-window session counts are reproducible because the EP-side regression is masked by
603
603
  * the upstream post-filter.
604
604
  *
605
- * No local post-filter is applied here. The repository is pinned to v1.2.20, so the
605
+ * No local post-filter is applied here. The repository is pinned to v1.2.21, so the
606
606
  * date-filter guarantees above apply; consumers running against an older server image
607
607
  * (pre-v1.2.14) must not assume them.
608
608
  */
@@ -81,6 +81,10 @@ export interface IMFClientOptions extends MCPClientOptions {
81
81
  timeoutMs?: number;
82
82
  /** Optional `fetch` implementation injection for testing. */
83
83
  fetchImpl?: typeof fetch;
84
+ /** MCP fetch-proxy gateway URL (bypasses AWF Squid proxy). */
85
+ fetchProxyGatewayUrl?: string;
86
+ /** API key for the MCP gateway. */
87
+ fetchProxyApiKey?: string;
84
88
  }
85
89
  /**
86
90
  * Count observations in an IMF SDMX-JSON data payload.
@@ -106,6 +110,8 @@ export declare class IMFMCPClient {
106
110
  private readonly _apiBaseUrl;
107
111
  private readonly _timeoutMs;
108
112
  private readonly _fetchImpl;
113
+ private readonly _fetchProxyGatewayUrl;
114
+ private readonly _fetchProxyApiKey;
109
115
  private _connected;
110
116
  constructor(options?: IMFClientOptions);
111
117
  /**
@@ -214,6 +220,8 @@ export declare class IMFMCPClient {
214
220
  }): Promise<MCPToolResult>;
215
221
  /**
216
222
  * Build a full URL and GET it as text, enforcing the client-wide timeout.
223
+ * Tries the MCP fetch-proxy gateway first (bypasses AWF Squid proxy in
224
+ * agentic workflow sandbox), then falls back to direct fetch.
217
225
  *
218
226
  * @param path - Path (already URL-encoded) to append to the base URL.
219
227
  * @returns Response body (`text/*` or `application/*`) as a string.
@@ -222,6 +230,15 @@ export declare class IMFMCPClient {
222
230
  * @internal
223
231
  */
224
232
  private _getText;
233
+ /**
234
+ * Fetch a URL via the MCP fetch-proxy gateway (JSON-RPC 2.0 over HTTP).
235
+ * The fetch-proxy server runs in a container that bypasses the AWF Squid proxy.
236
+ *
237
+ * @param url - Fully-qualified URL to fetch.
238
+ * @returns Response text, or null if the gateway call fails.
239
+ * @internal
240
+ */
241
+ private _fetchViaGateway;
225
242
  /**
226
243
  * GET a URL and parse the response body as JSON.
227
244
  *
@@ -4,7 +4,7 @@
4
4
  /** Default base URL for the IMF SDMX 3.0 REST API. */
5
5
  const DEFAULT_IMF_API_BASE_URL = 'https://dataservices.imf.org/REST/SDMX_3.0';
6
6
  /** Default per-request timeout (milliseconds). */
7
- const DEFAULT_IMF_API_TIMEOUT_MS = 30_000;
7
+ const DEFAULT_IMF_API_TIMEOUT_MS = 90_000;
8
8
  /** Fallback payload shape when an IMF call fails or the server is offline. */
9
9
  const IMF_FALLBACK = {
10
10
  content: [{ type: 'text', text: '' }],
@@ -191,6 +191,8 @@ export class IMFMCPClient {
191
191
  _apiBaseUrl;
192
192
  _timeoutMs;
193
193
  _fetchImpl;
194
+ _fetchProxyGatewayUrl;
195
+ _fetchProxyApiKey;
194
196
  _connected = false;
195
197
  constructor(options = {}) {
196
198
  const envBase = process.env['IMF_API_BASE_URL'];
@@ -211,6 +213,11 @@ export class IMFMCPClient {
211
213
  ? parsedEnvTimeout
212
214
  : DEFAULT_IMF_API_TIMEOUT_MS;
213
215
  this._fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
216
+ // MCP fetch-proxy gateway for AWF sandbox (bypasses Squid proxy)
217
+ this._fetchProxyGatewayUrl =
218
+ options.fetchProxyGatewayUrl ?? process.env['FETCH_MCP_GATEWAY_URL'] ?? undefined;
219
+ this._fetchProxyApiKey =
220
+ options.fetchProxyApiKey ?? process.env['EP_MCP_GATEWAY_API_KEY'] ?? undefined;
214
221
  }
215
222
  /**
216
223
  * Base URL currently in use (read-only — set at construction time).
@@ -469,6 +476,8 @@ export class IMFMCPClient {
469
476
  // ─── private transport helpers ─────────────────────────────────────────────
470
477
  /**
471
478
  * Build a full URL and GET it as text, enforcing the client-wide timeout.
479
+ * Tries the MCP fetch-proxy gateway first (bypasses AWF Squid proxy in
480
+ * agentic workflow sandbox), then falls back to direct fetch.
472
481
  *
473
482
  * @param path - Path (already URL-encoded) to append to the base URL.
474
483
  * @returns Response body (`text/*` or `application/*`) as a string.
@@ -478,6 +487,18 @@ export class IMFMCPClient {
478
487
  */
479
488
  async _getText(path) {
480
489
  const url = `${this._apiBaseUrl}${path.startsWith('/') ? path : `/${path}`}`;
490
+ // Strategy 1: MCP fetch-proxy gateway (bypasses AWF Squid proxy)
491
+ if (this._fetchProxyGatewayUrl && this._fetchProxyApiKey) {
492
+ try {
493
+ const result = await this._fetchViaGateway(url);
494
+ if (result !== null)
495
+ return result;
496
+ }
497
+ catch {
498
+ // Gateway unavailable — fall through to direct fetch
499
+ }
500
+ }
501
+ // Strategy 2: Direct fetch (works outside AWF sandbox)
481
502
  const controller = new AbortController();
482
503
  const timer = setTimeout(() => controller.abort(), this._timeoutMs);
483
504
  try {
@@ -495,6 +516,61 @@ export class IMFMCPClient {
495
516
  clearTimeout(timer);
496
517
  }
497
518
  }
519
+ /**
520
+ * Fetch a URL via the MCP fetch-proxy gateway (JSON-RPC 2.0 over HTTP).
521
+ * The fetch-proxy server runs in a container that bypasses the AWF Squid proxy.
522
+ *
523
+ * @param url - Fully-qualified URL to fetch.
524
+ * @returns Response text, or null if the gateway call fails.
525
+ * @internal
526
+ */
527
+ async _fetchViaGateway(url) {
528
+ const rpcRequest = {
529
+ jsonrpc: '2.0',
530
+ id: Date.now(),
531
+ method: 'tools/call',
532
+ params: {
533
+ name: 'fetch_url',
534
+ arguments: { url },
535
+ },
536
+ };
537
+ const headers = {
538
+ 'Content-Type': 'application/json',
539
+ Accept: 'application/json, text/event-stream',
540
+ };
541
+ if (this._fetchProxyApiKey) {
542
+ headers['Authorization'] = `Bearer ${this._fetchProxyApiKey}`;
543
+ }
544
+ const controller = new AbortController();
545
+ const timer = setTimeout(() => controller.abort(), this._timeoutMs);
546
+ try {
547
+ const response = await this._fetchImpl(this._fetchProxyGatewayUrl, {
548
+ method: 'POST',
549
+ headers,
550
+ body: JSON.stringify(rpcRequest),
551
+ signal: controller.signal,
552
+ });
553
+ if (!response.ok)
554
+ return null;
555
+ let body = await response.text();
556
+ // Handle SSE format (data: lines)
557
+ if (body.trimStart().startsWith('data:')) {
558
+ const lines = body.split('\n').filter((l) => l.startsWith('data:'));
559
+ body = lines.map((l) => l.slice(5).trim()).join('');
560
+ }
561
+ const parsed = JSON.parse(body);
562
+ if (parsed.error)
563
+ return null;
564
+ const text = parsed.result?.content?.[0]?.text;
565
+ return text && text.length > 0 ? text : null;
566
+ }
567
+ catch {
568
+ return null;
569
+ }
570
+ finally {
571
+ clearTimeout(timer);
572
+ }
573
+ }
498
574
  /**
499
575
  * GET a URL and parse the response body as JSON.
500
576
  *
@@ -300,7 +300,7 @@ export function buildSiteHeader(options) {
300
300
  <a href="${escapeHTML(homeHref)}" class="site-header__brand" aria-label="${safeTitle}">
301
301
  <picture class="site-header__logo-picture">
302
302
  <source srcset="${pathPrefix}images/banner.webp" type="image/webp">
303
- <img class="site-header__logo site-header__logo--banner" src="${pathPrefix}images/banner.jpg" alt="${safeTitle}" width="240" height="80" loading="eager">
303
+ <img class="site-header__logo site-header__logo--banner" src="${pathPrefix}images/banner.jpg" alt="${safeTitle}" width="180" height="60" loading="eager">
304
304
  </picture>
305
305
  <span class="site-header__brand-text">
306
306
  <span class="site-header__title">${safeTitle}</span>