From f5051a65e095ebb4790d2793f40b934fbffa2cb3 Mon Sep 17 00:00:00 2001 From: Ambient Code Bot Date: Wed, 15 Apr 2026 11:47:12 -0400 Subject: [PATCH 1/5] feat: add get_release_schedule tool for direct milestone lookup Queries both release_schedule and release_milestone tables with fuzzy matching on product, version, and milestone type. Returns past and future dates with days_away annotation. Eliminates the need for schema discovery when asking release date questions. Co-Authored-By: Claude Opus 4.6 (1M context) --- mcp_server.py | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/mcp_server.py b/mcp_server.py index 88a92d6..50adee8 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -478,6 +478,114 @@ def get_feature_status(issue_key: str | None = None, title: str | None = None) - return json.dumps({"features": results, "count": len(results)}, default=str) +@mcp.tool( + annotations={"readOnlyHint": True, "openWorldHint": False}, +) +def get_release_schedule( + product: str | None = None, + version: str | None = None, + milestone: str | None = None, +) -> str: + """Get release schedule dates for any product. + + All parameters are optional and fuzzy-matched (case-insensitive, partial). + Returns matching milestones ordered by date, including past dates. + + Args: + product: Product name. Examples: "AcmeProduct", "acme". + version: Version number. "1.0" also matches "1.0.1", "1.0 EA1", etc. + milestone: Milestone type. Examples: "code freeze", "ga", "release", "planning". + + Examples: + get_release_schedule(product="acme", version="1.0", milestone="code freeze") + get_release_schedule(version="2.0") + get_release_schedule(milestone="ga") + """ + conn = _get_conn() + today = date.today() + + # --- Query release_schedule table --- + sched_conditions: list[str] = [] + sched_params: list[str] = [] + if product: + sched_conditions.append("release LIKE ?") + sched_params.append(f"%{product}%") + if version: + sched_conditions.append("release LIKE ?") + sched_params.append(f"%{version}%") + if milestone: + sched_conditions.append("task LIKE ?") + sched_params.append(f"%{milestone}%") + + sched_where = " AND ".join(sched_conditions) if sched_conditions else "1=1" + sched_rows = conn.execute( + f"""SELECT release, task AS milestone, date_start, date_finish + FROM release_schedule + WHERE {sched_where} + ORDER BY date_start""", + sched_params, + ).fetchall() + + # --- Query release_milestone table --- + mile_conditions: list[str] = [] + mile_params: list[str] = [] + if product: + mile_conditions.append("product LIKE ?") + mile_params.append(f"%{product}%") + if version: + mile_conditions.append("version LIKE ?") + mile_params.append(f"%{version}%") + if milestone: + mile_conditions.append("event_type LIKE ?") + mile_params.append(f"%{milestone}%") + + mile_where = " AND ".join(mile_conditions) if mile_conditions else "1=1" + mile_rows = conn.execute( + f"""SELECT product, version, event_type AS milestone, event_date + FROM release_milestone + WHERE {mile_where} + ORDER BY event_date""", + mile_params, + ).fetchall() + + schedule = _rows_to_dicts(sched_rows) + milestones_list = _rows_to_dicts(mile_rows) + + # Annotate schedule entries with past/future + for entry in schedule: + d = entry.get("date_finish") or entry.get("date_start") + if d: + parsed = _parse_date(d) + if parsed: + entry["status"] = "past" if parsed < today else "upcoming" + entry["days_away"] = (parsed - today).days + + if not schedule and not milestones_list: + hints = [] + releases = conn.execute( + "SELECT DISTINCT release FROM release_schedule ORDER BY release" + ).fetchall() + if releases: + hints = [r["release"] for r in releases] + return json.dumps( + { + "error": "No matching releases found", + "available_releases": hints, + } + ) + + return json.dumps( + { + "schedule": schedule, + "milestones": milestones_list, + "schedule_count": len(schedule), + "milestone_count": len(milestones_list), + "as_of": today.isoformat(), + }, + default=str, + ) + + @mcp.tool( annotations={"readOnlyHint": True, "openWorldHint": False}, ) From 362bb85deecc598213fe26fcf10b111e2b2141e9 Mon Sep 17 00:00:00 2001 From: Ambient Code Bot Date: Wed, 15 Apr 2026 11:53:49 -0400 Subject: [PATCH 2/5] fix: annotate milestone entries with status/days_away Milestone entries from release_milestone table were missing status (past/upcoming) and days_away annotations that schedule entries already had, creating an asymmetry for LLM consumers. Co-Authored-By: Claude Opus 4.6 (1M context) --- mcp_server.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mcp_server.py b/mcp_server.py index 50adee8..6bc9624 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -560,6 +560,15 @@ def get_release_schedule( entry["status"] = "past" if parsed < today else "upcoming" entry["days_away"] = (parsed - today).days + # Annotate milestone entries with past/future + for entry in milestones_list: + d = entry.get("event_date") + if d: + parsed = _parse_date(d) + if parsed: + entry["status"] = "past" if parsed < today else "upcoming" + entry["days_away"] = (parsed - today).days + if not schedule and not milestones_list: hints = [] releases = conn.execute( From 11d4ae93f9fd215417e491bf9c1429cdb91676a0 Mon Sep 17 00:00:00 2001 From: Ambient Code Bot Date: Wed, 15 Apr 2026 11:55:11 -0400 Subject: [PATCH 3/5] fix: release_risk_summary includes recent past milestones Add lookback_days parameter (default 30) so milestones that just passed are still visible. Previously, a code freeze from yesterday would silently disappear from results. Co-Authored-By: Claude Opus 4.6 (1M context) --- mcp_server.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/mcp_server.py b/mcp_server.py index 6bc9624..7cbe67b 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -20,7 +20,7 @@ import os import re import sqlite3 -from datetime import date, datetime +from datetime import date, datetime, timedelta from difflib import get_close_matches from pathlib import Path @@ -598,11 +598,15 @@ def get_release_schedule( @mcp.tool( annotations={"readOnlyHint": True, "openWorldHint": False}, ) -def release_risk_summary(release: str | None = None) -> str: +def release_risk_summary(release: str | None = None, lookback_days: int = 30) -> str: """Assess release risk by comparing milestone dates against feature completion. - If no release specified, analyzes all releases with upcoming milestones. + If no release specified, analyzes all releases with recent or upcoming milestones. Flags features under 80% complete when milestone is within 30 days. + + Args: + release: Filter to a specific release (fuzzy match on version). Omit for all. + lookback_days: Include milestones up to this many days in the past (default 30). """ today = date.today() conn = _get_conn() @@ -621,7 +625,7 @@ def release_risk_summary(release: str | None = None) -> str: releases_info = {} for m in milestones: parsed = _parse_date(m["event_date"], today.year) - if not parsed or parsed < today: + if not parsed or parsed < today - timedelta(days=lookback_days): continue key = f"{m['product']} {m['version']}" if key not in releases_info: From 1ef7842b37d76e59f19c97345e74ef7ea12f1791 Mon Sep 17 00:00:00 2001 From: Ambient Code Bot Date: Wed, 15 Apr 2026 11:56:38 -0400 Subject: [PATCH 4/5] test: add smoke tests for get_release_schedule tool Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/test.sh | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/scripts/test.sh b/scripts/test.sh index a12e134..66d5832 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -73,6 +73,41 @@ sys.exit(0 if count > 0 else 1) " done +# 5b. Tool smoke tests +echo "" +echo "--- Tools ---" +run "get_release_schedule(all)" uv run python3 -c " +import json, sys +sys.path.insert(0, '$REPO_ROOT') +import mcp_server +r = json.loads(mcp_server.get_release_schedule()) +assert r.get('schedule_count', 0) > 0, 'no schedule rows' +" + +run "get_release_schedule(filtered)" uv run python3 -c " +import json, sys +sys.path.insert(0, '$REPO_ROOT') +import mcp_server +r = json.loads(mcp_server.get_release_schedule(product='Acme', version='1.0', milestone='freeze')) +assert r.get('schedule_count', 0) > 0, 'no filtered rows' +" + +run "get_release_schedule(no match)" uv run python3 -c " +import json, sys +sys.path.insert(0, '$REPO_ROOT') +import mcp_server +r = json.loads(mcp_server.get_release_schedule(product='zzz_no_match')) +assert 'error' in r, 'expected error for no match' +" + +run "release_risk_summary" uv run python3 -c " +import json, sys +sys.path.insert(0, '$REPO_ROOT') +import mcp_server +r = json.loads(mcp_server.release_risk_summary()) +assert 'releases' in r or 'message' in r, 'unexpected response shape' +" + # 6. Schema diff echo "" echo "--- Schema ---" From 13c8030712323190394e44fc5985ed828eb638cd Mon Sep 17 00:00:00 2001 From: Ambient Code Bot Date: Wed, 15 Apr 2026 11:57:06 -0400 Subject: [PATCH 5/5] style: ruff format mcp_server.py Co-Authored-By: Claude Opus 4.6 (1M context) --- mcp_server.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mcp_server.py b/mcp_server.py index 7cbe67b..d212254 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -571,9 +571,7 @@ def get_release_schedule( if not schedule and not milestones_list: hints = [] - releases = conn.execute( - "SELECT DISTINCT release FROM release_schedule ORDER BY release" - ).fetchall() + releases = conn.execute("SELECT DISTINCT release FROM release_schedule ORDER BY release").fetchall() if releases: hints = [r["release"] for r in releases] return json.dumps(