@digestseo/mcp-gsc 0.4.34 → 0.4.36

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/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.36] - 2026-09-24
11
+
12
+ ### Changed
13
+ - `analytics.query` now exposes an explicit `multimodal_report_isolatable: false` boundary and matching runtime guidance because Search Console's new web multimodal filter is currently UI-only; the documented Search Analytics API still exposes only web/image/video/news/discover/googleNews search types.
14
+
15
+ ### Fixed
16
+ - User-supplied Search Analytics ranges now reject future end dates against Search Console's Pacific calendar before reserving local safety budget, resolving Google credentials, or calling the provider; the same boundary now applies to explicit indexed-page ranges and weekly-digest input.
17
+
18
+ ## [0.4.35] - 2026-09-24
19
+
20
+ ### Fixed
21
+ - Search Analytics pagination now treats provider responses shorter than the requested page size as terminal, matching Google's documented contract and avoiding false continuations or unnecessary follow-up requests across direct and higher-level workflows.
22
+ - `insights.page_queries` now rejects exact page URLs outside the supplied Search Console property before reserving local Search Analytics safety budget, resolving Google credentials, or calling the provider, preventing misleading empty cross-property drilldowns.
23
+
10
24
  ## [0.4.34] - 2026-09-24
11
25
 
12
26
  ### Fixed
package/README.md CHANGED
@@ -66,6 +66,8 @@ Codex CLI and the Codex IDE extension share MCP configuration, so registering th
66
66
 
67
67
  When this repository is installed as a Cursor plugin, Cursor also auto-discovers three bundled Agent Skills that compose the MCP tools into evidence-safe workflows: `gsc-weekly-review`, `gsc-indexing-triage`, and `gsc-search-opportunities`. The **Add to Cursor** button above installs the MCP connection only; the bundled skills are part of the repository's Cursor plugin package.
68
68
 
69
+ The button and plugin MCP configuration above are for Cursor running on the same machine as the npm launcher. **Cursor Cloud Agents run in isolated cloud VMs, so they cannot reach `127.0.0.1` on your computer.** For Cloud Agents, deploy your own externally reachable HTTPS Worker and add its `/mcp` URL from the MCP controls at [cursor.com/agents](https://cursor.com/agents); Cursor supports HTTP MCP servers and OAuth there. Do not reuse the loopback URL for a Cloud Agent.
70
+
69
71
  **GitHub Copilot CLI**
70
72
 
71
73
  ```bash
@@ -89,7 +91,9 @@ The Gemini CLI extension bundles the verified local loopback MCP configuration p
89
91
 
90
92
  Start the npm launcher before using either install button or the Copilot plugin. Complete its Google OAuth flow on first use.
91
93
 
92
- **ChatGPT** — ChatGPT cannot connect directly to this loopback `127.0.0.1` endpoint. Use OpenAI's **Secure MCP Tunnel** for the local launcher, or deploy your own remote Cloudflare Worker and add that `/mcp` URL as a custom MCP app in Developer mode; see [Connect in your AI client](#connect-in-your-ai-client) below.
94
+ **ChatGPT** — ChatGPT cannot connect directly to this loopback `127.0.0.1` endpoint. For private/local use, create an OpenAI **Secure MCP Tunnel**, then run `tunnel-client` on the same machine as the launcher so it can reach `http://127.0.0.1:8080/mcp`; when creating the ChatGPT app, choose the **Tunnel** connection and select that tunnel. The tunnel setup requires an OpenAI `tunnel_id` and runtime API key; see OpenAI's [Secure MCP Tunnel guide](https://developers.openai.com/api/docs/guides/secure-mcp-tunnels). The tunnel keeps the MCP server private and is intended for private/developer-mode connectivity, not public plugin distribution.
95
+
96
+ Alternatively, deploy your own remote Cloudflare Worker and add its HTTPS `/mcp` URL as a custom MCP app in Developer mode; see [Connect in your AI client](#connect-in-your-ai-client) below.
93
97
 
94
98
  A self-hostable [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **Google Search Console**. Connect it to Claude.ai, Cursor, ChatGPT, or any MCP-compatible client and ask your AI assistant about your site's organic search performance — impressions, clicks, top queries, index status, and sitemap health — straight from your own Google account.
95
99
 
@@ -104,7 +108,7 @@ By default (`GSC_ACCESS_MODE=readwrite`), this server exposes 23 tools. Read-onl
104
108
  | **`server.capabilities`** | Read | List every tool this server exposes and report local Google credential state. `connected` means a stored refresh credential is present; the response also exposes `auth_status_basis` and `provider_auth_live_verified: false` because this discovery call does not probe Google. A later Google tool call can still require reconnection if the credential expired or was revoked. Takes no arguments. |
105
109
  | **`sites.list`** | Read | List the Search Console properties the connected Google account can access (`siteUrl`, `permissionLevel`). |
106
110
  | **`sites.get`** | Read | Retrieve one exact Search Console property and the connected account's permission level for it. |
107
- | **`analytics.query`** | Read | Impressions, clicks, CTR, and average position over a date range, with dimensions, filters, safe pagination, and selectable search type. Pass `dimensions: []` for true site totals; Google's aggregate row may omit `keys`. Search Analytics does not guarantee every data row and can return only top rows, so exhausting local pagination is not proof that the provider dataset is exhaustive. `data_state: "all"` can include fresh preliminary data, while `final` returns only finalized data; responses echo `data_state` plus `preliminary_data_possible`, and date/hour groupings preserve Google's incomplete-data boundary metadata when returned. Search appearance discovery is a two-step workflow: group by `searchAppearance` alone to discover the exact values Google returns for the property, then filter to one returned value in a separate query when grouping by page/query/country/device/date/hour. For `search_type: "discover"` or `"googleNews"`, `position_supported` is `false`; those reports expose clicks, impressions, and CTR but not average position, so returned rows can omit `position`, and query grouping/filtering is unavailable. Search Console's dedicated Generative AI performance reports are not exposed by the current documented Search Analytics API as a dedicated search type or filter selector; `generative_ai_report_isolatable` is therefore `false`. |
111
+ | **`analytics.query`** | Read | Impressions, clicks, CTR, and average position over a date range, with dimensions, filters, safe pagination, and selectable search type. Pass `dimensions: []` for true site totals; Google's aggregate row may omit `keys`. Search Analytics does not guarantee every data row and can return only top rows, so exhausting local pagination is not proof that the provider dataset is exhaustive. `data_state: "all"` can include fresh preliminary data, while `final` returns only finalized data; responses echo `data_state` plus `preliminary_data_possible`, and date/hour groupings preserve Google's incomplete-data boundary metadata when returned. Search appearance discovery is a two-step workflow: group by `searchAppearance` alone to discover the exact values Google returns for the property, then filter to one returned value in a separate query when grouping by page/query/country/device/date/hour. For `search_type: "discover"` or `"googleNews"`, `position_supported` is `false`; those reports expose clicks, impressions, and CTR but not average position, so returned rows can omit `position`, and query grouping/filtering is unavailable. Search Console's dedicated Generative AI performance reports are not exposed by the current documented Search Analytics API as a dedicated search type or filter selector; `generative_ai_report_isolatable` is therefore `false`. Search Console's new web multimodal UI filter is likewise not exposed as a documented Search Analytics API selector; `multimodal_report_isolatable` is therefore `false`. |
108
112
  | **`insights.page_queries`** / **`insights.query_pages`** | Read | Drill from one exact page to its Search Console queries, or from one exact query to the pages Google surfaced for it, with `row_limit` / `start_row` pagination. These query-centric helpers support web, image, video, and the Search results News tab; Discover and the Google News report are excluded because they do not expose query data. |
109
113
  | **`urls.inspect`** | Read | Google's URL Inspection report for the version of one page currently known in Google's index. It is not a live URL test; mobile-usability output is deprecated. |
110
114
  | **`urls.inspect_many`** | Read | Inspect Google's indexed versions of up to 10 URLs with bounded concurrency of 3 in one call while preserving input-order results. Each URL still consumes one Google URL Inspection request and one unit of the server's shared inspection safety budget; this does not run live URL tests. |
@@ -145,6 +149,8 @@ Query regexes are a manual approximation only. Search Console's native Branded/N
145
149
 
146
150
  Search Console also has dedicated Generative AI performance reports for Search and Discover. The current documented Search Analytics API does not expose a dedicated Generative AI search type or filter selector, so this MCP does not guess one. AI Overviews and AI Mode remain included in overall web Search performance data; use the Search Console UI for the dedicated Generative AI report until Google documents API access.
147
151
 
152
+ As of September 24, 2026, Search Console also has a web multimodal performance filter in the UI for searches such as Lens, Circle to Search, image uploads, and Chrome image search. The current Search Analytics API and Discovery document still expose only `web`, `image`, `video`, `news`, `discover`, and `googleNews` search types, so this MCP does not invent `search_type: "multimodal"` or guess a `searchAppearance` value. Use the Search Console UI for isolated multimodal reporting until Google documents API access.
153
+
148
154
  ## Connect in your AI client
149
155
 
150
156
  Once you've deployed the server (see **[SETUP.md](SETUP.md)**), connect it by pasting your Worker's `/mcp` URL into your client:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digestseo/mcp-gsc",
3
- "version": "0.4.34",
3
+ "version": "0.4.36",
4
4
  "private": false,
5
5
  "main": "scripts/glama-start.mjs",
6
6
  "bin": {
@@ -81,10 +81,14 @@ export function assertDateRange(
81
81
  }
82
82
  }
83
83
 
84
- export function assertDateNotInFuture(date: string, today: string): void {
84
+ export function assertDateNotInFuture(
85
+ date: string,
86
+ today: string,
87
+ dateName = 'End date',
88
+ ): void {
85
89
  if (date > today) {
86
90
  throw new Error(
87
- 'End date must be today or earlier. Google Search Console has no data for dates that have not happened yet.',
91
+ `${dateName} must be today or earlier (current Search Console Pacific date: ${today}). Google Search Console has no data for dates that have not happened yet.`,
88
92
  );
89
93
  }
90
94
  }
package/src/google.ts CHANGED
@@ -785,10 +785,9 @@ export async function querySearchAnalytics(
785
785
 
786
786
  /**
787
787
  * Fetches Search Analytics rows in 25,000-row pages for higher-level tools.
788
- * Google's documented pagination contract uses an explicit empty response as
789
- * the terminal page, so a short non-empty page is not treated as exhaustion.
790
- * A full final page conservatively reports localLimitReached because the API
791
- * can have more rows even when Search Console's own internal limits apply.
788
+ * Google's documented pagination contract treats a response shorter than the
789
+ * requested rowLimit as exhausted. An exactly full final page still requires
790
+ * a following empty-page probe unless the local safety ceiling is reached.
792
791
  */
793
792
  export async function querySearchAnalyticsPaginated(
794
793
  accessToken: string,
@@ -820,7 +819,7 @@ export async function querySearchAnalyticsPaginated(
820
819
 
821
820
  const acceptedRows = page.rows.slice(0, requestedRows);
822
821
  rows.push(...acceptedRows);
823
- if (page.rows.length === 0) {
822
+ if (page.rows.length < requestedRows) {
824
823
  return { rows, pagesFetched, localLimitReached: false };
825
824
  }
826
825
  if (rows.length >= maximumRows || page.rows.length > requestedRows) {
package/src/index.ts CHANGED
@@ -402,6 +402,10 @@ const SEARCH_ANALYTICS_OUTPUT_SCHEMA = {
402
402
  'False because the current documented Search Analytics API exposes no dedicated Generative AI report search type or filter selector.',
403
403
  ),
404
404
  generative_ai_note: z.string(),
405
+ multimodal_report_isolatable: z.literal(false).describe(
406
+ 'False because the current documented Search Analytics API exposes no dedicated web multimodal search type or filter selector.',
407
+ ),
408
+ multimodal_note: z.string(),
405
409
  next_start_row: z.number().int().nonnegative().optional(),
406
410
  has_more: z.boolean(),
407
411
  truncated: z.boolean(),
@@ -698,6 +702,9 @@ const SEARCH_ANALYTICS_PROVIDER_NOTE =
698
702
  const GENERATIVE_AI_REPORT_API_NOTE =
699
703
  "Search Console's dedicated Generative AI performance reports are not exposed by the current documented Search Analytics API as a dedicated search type or filter selector. AI Overviews and AI Mode remain included in overall web Search performance data. Do not infer isolated Generative AI metrics from analytics.query or guess a searchAppearance identifier.";
700
704
 
705
+ const MULTIMODAL_REPORT_API_NOTE =
706
+ "Search Console's web multimodal performance filter is currently available in the Search Console UI, but the documented Search Analytics API does not expose a multimodal search type or filter selector. Current API search types remain web, image, video, news, discover, and googleNews. Do not pass search_type='multimodal' or guess a searchAppearance identifier to isolate Lens, Circle to Search, image-upload, or Chrome image-search traffic.";
707
+
701
708
  function paginationMetadata(result: PaginatedSearchAnalyticsResult) {
702
709
  return {
703
710
  rows_fetched: result.rows.length,
@@ -1279,6 +1286,13 @@ class GscMcpRuntime {
1279
1286
  ' included in overall web Search performance data. Do not guess a',
1280
1287
  ' searchAppearance identifier or claim analytics.query isolates the',
1281
1288
  ' dedicated Generative AI report.',
1289
+ '- WEB MULTIMODAL REPORT: Search Console now has a web multimodal',
1290
+ ' performance filter in its UI for searches such as Lens, Circle to',
1291
+ ' Search, image uploads, and Chrome image search. The current',
1292
+ ' documented Search Analytics API still exposes only web, image,',
1293
+ ' video, news, discover, and googleNews search types, with no',
1294
+ ' multimodal selector. Do not pass search_type="multimodal" or guess',
1295
+ ' a searchAppearance identifier to isolate this UI report.',
1282
1296
  '- Use dimension_filter_groups to filter by country, device, query',
1283
1297
  ' content, page URL, or search feature. includingRegex and',
1284
1298
  ' excludingRegex use RE2 syntax. A query regex can provide a manual',
@@ -1358,7 +1372,7 @@ class GscMcpRuntime {
1358
1372
  search_type: z
1359
1373
  .enum(['web', 'image', 'video', 'news', 'discover', 'googleNews'])
1360
1374
  .default('web')
1361
- .describe('Which documented Search Analytics search index to query. Defaults to web. Google Discover and Google News do not support query grouping/filtering or average position. The current API does not expose a dedicated Generative AI performance-report search type.'),
1375
+ .describe('Which documented Search Analytics search index to query. Defaults to web. Google Discover and Google News do not support query grouping/filtering or average position. The current API does not expose a dedicated Generative AI performance-report search type or the Search Console UI web multimodal filter as a search type.'),
1362
1376
  aggregation_type: z
1363
1377
  .enum(['auto', 'byNewsShowcasePanel', 'byPage', 'byProperty'])
1364
1378
  .default('auto')
@@ -1388,6 +1402,11 @@ class GscMcpRuntime {
1388
1402
  dimension_filter_groups,
1389
1403
  }) => {
1390
1404
  assertDateRange(start_date, end_date);
1405
+ assertDateNotInFuture(
1406
+ end_date,
1407
+ getSearchConsoleCalendarDate(),
1408
+ 'end_date',
1409
+ );
1391
1410
  const sourceRowLimit = Math.min(row_limit, MAX_DIRECT_SOURCE_ROWS);
1392
1411
  const query = {
1393
1412
  startDate: start_date,
@@ -1408,11 +1427,10 @@ class GscMcpRuntime {
1408
1427
  if (rateLimitError) return rateLimitError;
1409
1428
  const accessToken = await this.getAccessToken(googleId);
1410
1429
  const response = await querySearchAnalytics(accessToken, site_url, query);
1411
- // Search Analytics pagination is complete only after Google returns an
1412
- // explicit empty page. A short non-empty page can still be followed by
1413
- // later rows, so preserve a continuation for dimensioned queries.
1430
+ // Google documents a short page as terminal. An exactly full page can
1431
+ // still have a following page, so expose continuation only then.
1414
1432
  const sourceMayHaveMore =
1415
- dimensions.length > 0 && response.rows.length > 0;
1433
+ dimensions.length > 0 && response.rows.length === sourceRowLimit;
1416
1434
  const bounded = windowSourceRows(
1417
1435
  response.rows,
1418
1436
  start_row,
@@ -1440,6 +1458,8 @@ class GscMcpRuntime {
1440
1458
  provider_note: SEARCH_ANALYTICS_PROVIDER_NOTE,
1441
1459
  generative_ai_report_isolatable: false,
1442
1460
  generative_ai_note: GENERATIVE_AI_REPORT_API_NOTE,
1461
+ multimodal_report_isolatable: false,
1462
+ multimodal_note: MULTIMODAL_REPORT_API_NOTE,
1443
1463
  has_more: bounded.resultPage.has_more,
1444
1464
  truncated: bounded.resultPage.truncated,
1445
1465
  byte_limit_reached: bounded.resultPage.byte_limit_reached,
@@ -1463,10 +1483,10 @@ class GscMcpRuntime {
1463
1483
  'insights.page_queries',
1464
1484
  {
1465
1485
  title: 'Find queries for a page',
1466
- description: 'For one exact page URL, return the Search Console queries that produced impressions for it over a date range. This wraps an exact page dimension filter so agents do not need to construct analytics.query filter groups manually. Exact page matching is case-sensitive in Search Console. Use row_limit and start_row to page through bounded results; Search Console can still omit anonymized queries. Google Discover and Google News are intentionally unavailable because those reports do not expose query data.',
1486
+ description: 'For one exact page URL within the supplied Search Console property, return the queries that produced impressions for it over a date range. This wraps an exact page dimension filter so agents do not need to construct analytics.query filter groups manually. Exact page matching is case-sensitive in Search Console. Use row_limit and start_row to page through bounded results; Search Console can still omit anonymized queries. Google Discover and Google News are intentionally unavailable because those reports do not expose query data.',
1467
1487
  inputSchema: {
1468
1488
  site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1469
- page_url: z.string().url().describe('Exact fully-qualified page URL to filter on, e.g. https://example.com/guides/seo/. Search Console exact page filters are case-sensitive.'),
1489
+ page_url: z.string().url().describe('Exact fully-qualified page URL to filter on, e.g. https://example.com/guides/seo/. The URL must belong to site_url; Search Console exact page filters are case-sensitive.'),
1470
1490
  start_date: SEARCH_CONSOLE_DATE_SCHEMA.describe(
1471
1491
  searchConsoleDateDescription('Start date (inclusive) in YYYY-MM-DD format.'),
1472
1492
  ),
@@ -1498,6 +1518,12 @@ class GscMcpRuntime {
1498
1518
  },
1499
1519
  async ({ site_url, page_url, start_date, end_date, search_type, row_limit = 100, start_row = 0 }) => {
1500
1520
  assertDateRange(start_date, end_date);
1521
+ assertDateNotInFuture(
1522
+ end_date,
1523
+ getSearchConsoleCalendarDate(),
1524
+ 'end_date',
1525
+ );
1526
+ assertUrlWithinSearchConsoleProperty(page_url, site_url, 'page_url');
1501
1527
  const googleId = this.requireGoogleId();
1502
1528
  const rateLimitError = await this.rateLimitError(googleId, 'insights.page_queries');
1503
1529
  if (rateLimitError) return rateLimitError;
@@ -1582,6 +1608,11 @@ class GscMcpRuntime {
1582
1608
  },
1583
1609
  async ({ site_url, query, start_date, end_date, search_type, row_limit = 100, start_row = 0 }) => {
1584
1610
  assertDateRange(start_date, end_date);
1611
+ assertDateNotInFuture(
1612
+ end_date,
1613
+ getSearchConsoleCalendarDate(),
1614
+ 'end_date',
1615
+ );
1585
1616
  const googleId = this.requireGoogleId();
1586
1617
  const rateLimitError = await this.rateLimitError(googleId, 'insights.query_pages');
1587
1618
  if (rateLimitError) return rateLimitError;
@@ -1776,6 +1807,11 @@ class GscMcpRuntime {
1776
1807
  },
1777
1808
  async ({ site_url, start_date, end_date, min_impressions, min_position, max_position, limit = DEFAULT_ANALYSIS_RESULT_LIMIT, start_row = 0 }) => {
1778
1809
  assertDateRange(start_date, end_date);
1810
+ assertDateNotInFuture(
1811
+ end_date,
1812
+ getSearchConsoleCalendarDate(),
1813
+ 'end_date',
1814
+ );
1779
1815
  const googleId = this.requireGoogleId();
1780
1816
  const rateLimitError = await this.rateLimitError(googleId, 'insights.quick_wins');
1781
1817
  if (rateLimitError) return rateLimitError;
@@ -1823,6 +1859,11 @@ class GscMcpRuntime {
1823
1859
  },
1824
1860
  async ({ site_url, start_date, end_date, min_impressions, min_page_percentage, limit = DEFAULT_ANALYSIS_RESULT_LIMIT, start_row = 0 }) => {
1825
1861
  assertDateRange(start_date, end_date);
1862
+ assertDateNotInFuture(
1863
+ end_date,
1864
+ getSearchConsoleCalendarDate(),
1865
+ 'end_date',
1866
+ );
1826
1867
  const googleId = this.requireGoogleId();
1827
1868
  const rateLimitError = await this.rateLimitError(googleId, 'insights.cannibalization');
1828
1869
  if (rateLimitError) return rateLimitError;
@@ -2124,7 +2165,7 @@ class GscMcpRuntime {
2124
2165
  pages,
2125
2166
  start_row,
2126
2167
  row_limit,
2127
- response.rows.length > 0,
2168
+ response.rows.length === sourceRowLimit,
2128
2169
  start_row + sourceRowLimit,
2129
2170
  );
2130
2171
 
@@ -2187,6 +2228,9 @@ class GscMcpRuntime {
2187
2228
  }) => {
2188
2229
  assertDateRange(start_date_a, end_date_a, 'start_date_a', 'end_date_a');
2189
2230
  assertDateRange(start_date_b, end_date_b, 'start_date_b', 'end_date_b');
2231
+ const today = getSearchConsoleCalendarDate();
2232
+ assertDateNotInFuture(end_date_a, today, 'end_date_a');
2233
+ assertDateNotInFuture(end_date_b, today, 'end_date_b');
2190
2234
  const googleId = this.requireGoogleId();
2191
2235
  const rateLimitError = await this.rateLimitError(googleId, 'analytics.compare');
2192
2236
  if (rateLimitError) return rateLimitError;
@@ -2248,12 +2292,12 @@ class GscMcpRuntime {
2248
2292
  annotations: READ_ONLY_ANNOTATIONS,
2249
2293
  },
2250
2294
  async ({ site_url, end_date }) => {
2295
+ const today = getSearchConsoleCalendarDate();
2296
+ const resolvedEndDate = resolveWeeklyDigestEndDate(end_date, today);
2297
+ assertDateNotInFuture(resolvedEndDate, today, 'end_date');
2251
2298
  const googleId = this.requireGoogleId();
2252
2299
  const rateLimitError = await this.rateLimitError(googleId, 'reports.weekly_digest');
2253
2300
  if (rateLimitError) return rateLimitError;
2254
- const today = getSearchConsoleCalendarDate();
2255
- const resolvedEndDate = resolveWeeklyDigestEndDate(end_date, today);
2256
- assertDateNotInFuture(resolvedEndDate, today);
2257
2301
 
2258
2302
  try {
2259
2303
  const markdown = await generateWeeklyDigest(
@@ -1,4 +1,4 @@
1
- import { assertDateRange } from './date-validation';
1
+ import { assertDateNotInFuture, assertDateRange } from './date-validation';
2
2
 
3
3
  export interface IndexedPagesDateRange {
4
4
  startDate: string;
@@ -26,6 +26,7 @@ export function resolveIndexedPagesDateRange(
26
26
  }
27
27
 
28
28
  if (!startDate) {
29
+ assertDateNotInFuture(endDate!, today, 'end_date');
29
30
  return {
30
31
  startDate: addUtcDays(endDate!, -29),
31
32
  endDate: endDate!,
@@ -50,5 +51,6 @@ export function resolveIndexedPagesDateRange(
50
51
  }
51
52
 
52
53
  assertDateRange(startDate, endDate);
54
+ assertDateNotInFuture(endDate, today, 'end_date');
53
55
  return { startDate, endDate };
54
56
  }