m365-roadmap-mcp 0.2.1__tar.gz → 0.3.1__tar.gz
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.
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/PKG-INFO +8 -1
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/README.md +7 -0
- m365_roadmap_mcp-0.3.1/icon.png +0 -0
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/pyproject.toml +1 -1
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/feeds/m365_api.py +17 -0
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/models/feature.py +16 -1
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/server.py +11 -2
- m365_roadmap_mcp-0.3.1/src/m365_roadmap_mcp/tools/cloud.py +70 -0
- m365_roadmap_mcp-0.3.1/src/m365_roadmap_mcp/tools/details.py +35 -0
- m365_roadmap_mcp-0.3.1/src/m365_roadmap_mcp/tools/recent.py +50 -0
- m365_roadmap_mcp-0.3.1/src/m365_roadmap_mcp/tools/search.py +293 -0
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/tests/test_feeds.py +26 -0
- m365_roadmap_mcp-0.3.1/tests/test_tools.py +661 -0
- m365_roadmap_mcp-0.2.1/src/m365_roadmap_mcp/tools/search.py +0 -148
- m365_roadmap_mcp-0.2.1/tests/test_tools.py +0 -244
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/.github/workflows/publish.yml +0 -0
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/.gitignore +0 -0
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/LICENSE +0 -0
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/server.json +0 -0
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/__init__.py +0 -0
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/__main__.py +0 -0
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/feeds/__init__.py +0 -0
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/models/__init__.py +0 -0
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/tools/__init__.py +0 -0
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/tests/__init__.py +0 -0
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/tests/api_snapshot.json +0 -0
- {m365_roadmap_mcp-0.2.1 → m365_roadmap_mcp-0.3.1}/tests/rss_feed_snapshot.xml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: m365-roadmap-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: MCP server for querying the Microsoft 365 Roadmap
|
|
5
5
|
Project-URL: Homepage, https://github.com/jonnybottles/M365-roadmap-mcp-server
|
|
6
6
|
Project-URL: Repository, https://github.com/jonnybottles/M365-roadmap-mcp-server
|
|
@@ -24,6 +24,8 @@ Requires-Dist: pytest; extra == 'dev'
|
|
|
24
24
|
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
|
|
27
|
+
### Disclaimer: This is an independent, self-built project and is not an official Microsoft tool or service.
|
|
28
|
+
|
|
27
29
|
# M365 Roadmap MCP Server
|
|
28
30
|
|
|
29
31
|
<!-- mcp-name: io.github.jonnybottles/m365-roadmap -->
|
|
@@ -187,6 +189,11 @@ Provides a single **`search_roadmap`** tool that handles all M365 roadmap querie
|
|
|
187
189
|
- **Cloud instance filter** -- Filter by cloud instance (GCC, GCC High, DoD)
|
|
188
190
|
- **Feature lookup** -- Retrieve full metadata for a specific roadmap ID
|
|
189
191
|
- **Recent additions** -- List features added within the last N days
|
|
192
|
+
- **Release phase filter** -- Filter by release phase (General Availability, Preview, Targeted Release)
|
|
193
|
+
- **Platform filter** -- Filter by platform (Web, Desktop, iOS, Android, Mac)
|
|
194
|
+
- **Rollout date filter** -- Filter by rollout start date (e.g., "December 2026", "2026")
|
|
195
|
+
- **Preview date filter** -- Filter by preview availability date (e.g., "July 2026")
|
|
196
|
+
- **Recently modified** -- List features modified within the last N days
|
|
190
197
|
|
|
191
198
|
## Data Source
|
|
192
199
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
### Disclaimer: This is an independent, self-built project and is not an official Microsoft tool or service.
|
|
2
|
+
|
|
1
3
|
# M365 Roadmap MCP Server
|
|
2
4
|
|
|
3
5
|
<!-- mcp-name: io.github.jonnybottles/m365-roadmap -->
|
|
@@ -161,6 +163,11 @@ Provides a single **`search_roadmap`** tool that handles all M365 roadmap querie
|
|
|
161
163
|
- **Cloud instance filter** -- Filter by cloud instance (GCC, GCC High, DoD)
|
|
162
164
|
- **Feature lookup** -- Retrieve full metadata for a specific roadmap ID
|
|
163
165
|
- **Recent additions** -- List features added within the last N days
|
|
166
|
+
- **Release phase filter** -- Filter by release phase (General Availability, Preview, Targeted Release)
|
|
167
|
+
- **Platform filter** -- Filter by platform (Web, Desktop, iOS, Android, Mac)
|
|
168
|
+
- **Rollout date filter** -- Filter by rollout start date (e.g., "December 2026", "2026")
|
|
169
|
+
- **Preview date filter** -- Filter by preview availability date (e.g., "July 2026")
|
|
170
|
+
- **Recently modified** -- List features modified within the last N days
|
|
164
171
|
|
|
165
172
|
## Data Source
|
|
166
173
|
|
|
Binary file
|
|
@@ -55,6 +55,20 @@ def _parse_item(item: dict) -> RoadmapFeature | None:
|
|
|
55
55
|
if "tagName" in c
|
|
56
56
|
]
|
|
57
57
|
|
|
58
|
+
# Extract release phases from tagsContainer
|
|
59
|
+
release_phases = [
|
|
60
|
+
r["tagName"]
|
|
61
|
+
for r in tags_container.get("releasePhase", [])
|
|
62
|
+
if "tagName" in r
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
# Extract platforms from tagsContainer
|
|
66
|
+
platforms = [
|
|
67
|
+
p["tagName"]
|
|
68
|
+
for p in tags_container.get("platforms", [])
|
|
69
|
+
if "tagName" in p
|
|
70
|
+
]
|
|
71
|
+
|
|
58
72
|
return RoadmapFeature(
|
|
59
73
|
id=str(item.get("id", "")),
|
|
60
74
|
title=item.get("title", ""),
|
|
@@ -62,7 +76,10 @@ def _parse_item(item: dict) -> RoadmapFeature | None:
|
|
|
62
76
|
status=item.get("status"),
|
|
63
77
|
tags=products,
|
|
64
78
|
cloud_instances=cloud_instances,
|
|
79
|
+
release_phases=release_phases,
|
|
80
|
+
platforms=platforms,
|
|
65
81
|
public_disclosure_date=item.get("publicDisclosureAvailabilityDate"),
|
|
82
|
+
public_preview_date=item.get("publicPreviewDate"),
|
|
66
83
|
created=item.get("created"),
|
|
67
84
|
modified=item.get("modified"),
|
|
68
85
|
)
|
|
@@ -21,9 +21,21 @@ class RoadmapFeature(BaseModel):
|
|
|
21
21
|
default_factory=list,
|
|
22
22
|
description="Cloud availability (e.g., Worldwide, GCC, GCC High, DoD)",
|
|
23
23
|
)
|
|
24
|
+
release_phases: list[str] = Field(
|
|
25
|
+
default_factory=list,
|
|
26
|
+
description="Release phases (e.g., General Availability, Preview, Targeted Release)",
|
|
27
|
+
)
|
|
28
|
+
platforms: list[str] = Field(
|
|
29
|
+
default_factory=list,
|
|
30
|
+
description="Target platforms (e.g., Web, Desktop, iOS, Android, Mac)",
|
|
31
|
+
)
|
|
24
32
|
public_disclosure_date: str | None = Field(
|
|
25
33
|
default=None,
|
|
26
|
-
description="Estimated
|
|
34
|
+
description="Estimated rollout start date (e.g., December CY2026)",
|
|
35
|
+
)
|
|
36
|
+
public_preview_date: str | None = Field(
|
|
37
|
+
default=None,
|
|
38
|
+
description="Estimated preview availability date (e.g., July 2026)",
|
|
27
39
|
)
|
|
28
40
|
created: str | None = Field(
|
|
29
41
|
default=None,
|
|
@@ -43,7 +55,10 @@ class RoadmapFeature(BaseModel):
|
|
|
43
55
|
"status": self.status,
|
|
44
56
|
"tags": self.tags,
|
|
45
57
|
"cloud_instances": self.cloud_instances,
|
|
58
|
+
"release_phases": self.release_phases,
|
|
59
|
+
"platforms": self.platforms,
|
|
46
60
|
"public_disclosure_date": self.public_disclosure_date,
|
|
61
|
+
"public_preview_date": self.public_preview_date,
|
|
47
62
|
"created": self.created,
|
|
48
63
|
"modified": self.modified,
|
|
49
64
|
}
|
|
@@ -22,13 +22,22 @@ mcp = FastMCP(
|
|
|
22
22
|
"- status: filter by status ('In development', 'Rolling out', 'Launched')\n"
|
|
23
23
|
"- cloud_instance: filter by cloud instance ('GCC', 'GCC High', 'DoD')\n"
|
|
24
24
|
"- feature_id: retrieve a single feature by its roadmap ID\n"
|
|
25
|
-
"- added_within_days: show only features added within N days\n
|
|
25
|
+
"- added_within_days: show only features added within N days\n"
|
|
26
|
+
"- release_phase: filter by release phase ('General Availability', 'Preview')\n"
|
|
27
|
+
"- platform: filter by platform ('Web', 'Desktop', 'iOS', 'Android', 'Mac')\n"
|
|
28
|
+
"- rollout_date: filter by rollout start date (e.g. 'December 2026')\n"
|
|
29
|
+
"- preview_date: filter by preview availability date (e.g. 'July 2026')\n"
|
|
30
|
+
"- modified_within_days: show only features modified within N days\n"
|
|
31
|
+
"- include_facets: include taxonomy facets with counts (default: False)\n\n"
|
|
26
32
|
"Tips:\n"
|
|
27
33
|
"- To get feature details, use feature_id with the roadmap ID.\n"
|
|
28
34
|
"- To check cloud availability, use cloud_instance with a feature_id or "
|
|
29
35
|
"product filter. The cloud_instances field in each result shows all "
|
|
30
36
|
"supported instances.\n"
|
|
31
|
-
"- To list recent additions, use added_within_days (e.g. 30 for last month)
|
|
37
|
+
"- To list recent additions, use added_within_days (e.g. 30 for last month).\n"
|
|
38
|
+
"- To filter by release phase or platform, use release_phase or platform filters.\n"
|
|
39
|
+
"- To search by dates, use rollout_date or preview_date with partial date strings.\n"
|
|
40
|
+
"- To discover available filter values, use include_facets=True (optionally with limit=0)."
|
|
32
41
|
),
|
|
33
42
|
)
|
|
34
43
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Tool for checking cloud instance availability of M365 Roadmap features."""
|
|
2
|
+
|
|
3
|
+
from ..feeds.m365_api import fetch_features
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def check_cloud_availability(feature_id: str, instance: str) -> dict:
|
|
7
|
+
"""Check whether a Microsoft 365 Roadmap feature is available for a specific cloud instance.
|
|
8
|
+
|
|
9
|
+
Critical for government and defense clients who need to verify feature availability
|
|
10
|
+
on GCC, GCC High, or DoD cloud instances before planning deployments.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
feature_id: The unique Roadmap ID (e.g., "534606").
|
|
14
|
+
instance: The cloud instance to check (e.g., "GCC", "GCC High", "DoD",
|
|
15
|
+
"Worldwide"). Case-insensitive partial match is used, so "gcc" matches
|
|
16
|
+
"GCC" and "GCC High".
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Dictionary with:
|
|
20
|
+
- feature_id: The queried feature ID
|
|
21
|
+
- instance_queried: The cloud instance that was checked
|
|
22
|
+
- found: Whether the feature was found in the roadmap
|
|
23
|
+
- available: Whether the feature is available for the queried instance
|
|
24
|
+
- matched_instances: List of cloud instances that matched the query
|
|
25
|
+
- all_instances: All cloud instances the feature supports
|
|
26
|
+
- status: Feature status (if found)
|
|
27
|
+
- public_disclosure_date: Estimated release date (if found)
|
|
28
|
+
- title: Feature title (if found)
|
|
29
|
+
- error: Error message (if feature not found)
|
|
30
|
+
"""
|
|
31
|
+
features = await fetch_features()
|
|
32
|
+
|
|
33
|
+
# Find the feature by ID
|
|
34
|
+
target = None
|
|
35
|
+
for feature in features:
|
|
36
|
+
if feature.id == feature_id:
|
|
37
|
+
target = feature
|
|
38
|
+
break
|
|
39
|
+
|
|
40
|
+
if target is None:
|
|
41
|
+
return {
|
|
42
|
+
"feature_id": feature_id,
|
|
43
|
+
"instance_queried": instance,
|
|
44
|
+
"found": False,
|
|
45
|
+
"available": False,
|
|
46
|
+
"matched_instances": [],
|
|
47
|
+
"all_instances": [],
|
|
48
|
+
"status": None,
|
|
49
|
+
"public_disclosure_date": None,
|
|
50
|
+
"title": None,
|
|
51
|
+
"error": f"No feature found with ID '{feature_id}'",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Case-insensitive partial match on cloud instances
|
|
55
|
+
instance_lower = instance.lower()
|
|
56
|
+
matched = [
|
|
57
|
+
ci for ci in target.cloud_instances if instance_lower in ci.lower()
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
"feature_id": feature_id,
|
|
62
|
+
"instance_queried": instance,
|
|
63
|
+
"found": True,
|
|
64
|
+
"available": len(matched) > 0,
|
|
65
|
+
"matched_instances": matched,
|
|
66
|
+
"all_instances": target.cloud_instances,
|
|
67
|
+
"status": target.status,
|
|
68
|
+
"public_disclosure_date": target.public_disclosure_date,
|
|
69
|
+
"title": target.title,
|
|
70
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Tool for retrieving full details of a single M365 Roadmap feature."""
|
|
2
|
+
|
|
3
|
+
from ..feeds.m365_api import fetch_features
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def get_feature_details(feature_id: str) -> dict:
|
|
7
|
+
"""Retrieve full metadata for a specific Microsoft 365 Roadmap feature by its ID.
|
|
8
|
+
|
|
9
|
+
Use this tool when you need complete details about a known roadmap feature,
|
|
10
|
+
including its description, status, product tags, cloud instance availability,
|
|
11
|
+
and release date.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
feature_id: The unique Roadmap ID (e.g., "534606").
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Dictionary with:
|
|
18
|
+
- found: Whether the feature was found
|
|
19
|
+
- feature: Full feature object if found, None otherwise
|
|
20
|
+
- error: Error message if not found
|
|
21
|
+
"""
|
|
22
|
+
features = await fetch_features()
|
|
23
|
+
|
|
24
|
+
for feature in features:
|
|
25
|
+
if feature.id == feature_id:
|
|
26
|
+
return {
|
|
27
|
+
"found": True,
|
|
28
|
+
"feature": feature.to_dict(),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
"found": False,
|
|
33
|
+
"feature": None,
|
|
34
|
+
"error": f"No feature found with ID '{feature_id}'",
|
|
35
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Tool for listing recently added M365 Roadmap features."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
|
|
5
|
+
from ..feeds.m365_api import fetch_features
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def list_recent_additions(days: int = 7) -> dict:
|
|
9
|
+
"""List features recently added to the Microsoft 365 Roadmap.
|
|
10
|
+
|
|
11
|
+
Use this tool to monitor what new features have appeared on the roadmap
|
|
12
|
+
within a given time window. Useful for staying current on Microsoft's
|
|
13
|
+
latest plans and announcements.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
days: Number of days to look back (default: 7, clamped to 1–365).
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Dictionary with:
|
|
20
|
+
- total_found: Number of features added within the time window
|
|
21
|
+
- features: List of recently added feature objects
|
|
22
|
+
- days_queried: The actual number of days used (after clamping)
|
|
23
|
+
- cutoff_date: The earliest date included in the results (ISO format)
|
|
24
|
+
"""
|
|
25
|
+
# Clamp days to reasonable bounds
|
|
26
|
+
days = max(1, min(days, 365))
|
|
27
|
+
|
|
28
|
+
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
|
29
|
+
features = await fetch_features()
|
|
30
|
+
|
|
31
|
+
recent = []
|
|
32
|
+
for feature in features:
|
|
33
|
+
if not feature.created:
|
|
34
|
+
continue
|
|
35
|
+
try:
|
|
36
|
+
created_dt = datetime.fromisoformat(feature.created)
|
|
37
|
+
# Ensure timezone-aware comparison
|
|
38
|
+
if created_dt.tzinfo is None:
|
|
39
|
+
created_dt = created_dt.replace(tzinfo=timezone.utc)
|
|
40
|
+
if created_dt >= cutoff:
|
|
41
|
+
recent.append(feature)
|
|
42
|
+
except (ValueError, TypeError):
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
"total_found": len(recent),
|
|
47
|
+
"features": [f.to_dict() for f in recent],
|
|
48
|
+
"days_queried": days,
|
|
49
|
+
"cutoff_date": cutoff.isoformat(),
|
|
50
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Search tool for querying and filtering M365 Roadmap features."""
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
|
|
6
|
+
from ..feeds.m365_api import fetch_features
|
|
7
|
+
from ..models.feature import RoadmapFeature
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def compute_facets(features: list[RoadmapFeature]) -> dict:
|
|
11
|
+
"""Compute facet counts from a list of features.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
features: List of RoadmapFeature objects to analyze
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Dictionary with facet categories and counts
|
|
18
|
+
"""
|
|
19
|
+
products = Counter()
|
|
20
|
+
statuses = Counter()
|
|
21
|
+
release_phases = Counter()
|
|
22
|
+
platforms = Counter()
|
|
23
|
+
cloud_instances = Counter()
|
|
24
|
+
|
|
25
|
+
for feature in features:
|
|
26
|
+
# Products (from tags)
|
|
27
|
+
for tag in feature.tags:
|
|
28
|
+
products[tag] += 1
|
|
29
|
+
|
|
30
|
+
# Status
|
|
31
|
+
if feature.status:
|
|
32
|
+
statuses[feature.status] += 1
|
|
33
|
+
|
|
34
|
+
# Release phases
|
|
35
|
+
for rp in feature.release_phases:
|
|
36
|
+
release_phases[rp] += 1
|
|
37
|
+
|
|
38
|
+
# Platforms
|
|
39
|
+
for p in feature.platforms:
|
|
40
|
+
platforms[p] += 1
|
|
41
|
+
|
|
42
|
+
# Cloud instances
|
|
43
|
+
for ci in feature.cloud_instances:
|
|
44
|
+
cloud_instances[ci] += 1
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
"products": [{"name": k, "count": v} for k, v in products.most_common()],
|
|
48
|
+
"statuses": [{"name": k, "count": v} for k, v in statuses.most_common()],
|
|
49
|
+
"release_phases": [{"name": k, "count": v} for k, v in release_phases.most_common()],
|
|
50
|
+
"platforms": [{"name": k, "count": v} for k, v in platforms.most_common()],
|
|
51
|
+
"cloud_instances": [{"name": k, "count": v} for k, v in cloud_instances.most_common()],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def search_roadmap(
|
|
56
|
+
query: str | None = None,
|
|
57
|
+
product: str | None = None,
|
|
58
|
+
status: str | None = None,
|
|
59
|
+
cloud_instance: str | None = None,
|
|
60
|
+
feature_id: str | None = None,
|
|
61
|
+
added_within_days: int | None = None,
|
|
62
|
+
release_phase: str | None = None,
|
|
63
|
+
platform: str | None = None,
|
|
64
|
+
rollout_date: str | None = None,
|
|
65
|
+
preview_date: str | None = None,
|
|
66
|
+
modified_within_days: int | None = None,
|
|
67
|
+
include_facets: bool = False,
|
|
68
|
+
limit: int = 10,
|
|
69
|
+
) -> dict:
|
|
70
|
+
"""Search the Microsoft 365 Roadmap for features matching keywords and filters.
|
|
71
|
+
|
|
72
|
+
Combines keyword search, product filtering, status filtering, cloud instance
|
|
73
|
+
filtering, and recency filtering into a single flexible tool. All filter
|
|
74
|
+
parameters are optional and can be combined. When no filters are provided,
|
|
75
|
+
returns the most recent features.
|
|
76
|
+
|
|
77
|
+
Use this tool to:
|
|
78
|
+
- Browse recent roadmap features (no filters)
|
|
79
|
+
- Search for features by keyword (query="Copilot")
|
|
80
|
+
- Filter by product (product="Microsoft Teams")
|
|
81
|
+
- Find features by status (status="In development", "Rolling out", "Launched")
|
|
82
|
+
- Filter by cloud instance (cloud_instance="GCC High", "DoD", "GCC")
|
|
83
|
+
- Retrieve a specific feature by ID (feature_id="534606")
|
|
84
|
+
- List recently added features (added_within_days=30)
|
|
85
|
+
- Filter by release phase (release_phase="General Availability", "Preview")
|
|
86
|
+
- Filter by platform (platform="Web", "iOS", "Android")
|
|
87
|
+
- Filter by rollout date (rollout_date="December 2026")
|
|
88
|
+
- Filter by preview date (preview_date="July 2026")
|
|
89
|
+
- List recently modified features (modified_within_days=7)
|
|
90
|
+
- Combine any of the above (query="Copilot" + product="Teams" + cloud_instance="GCC")
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
query: Optional keyword to match against title and description (case-insensitive).
|
|
94
|
+
product: Optional product tag to filter by (case-insensitive partial match,
|
|
95
|
+
e.g. "Teams" matches "Microsoft Teams").
|
|
96
|
+
status: Optional status filter. Valid values: In development, Rolling out, Launched.
|
|
97
|
+
cloud_instance: Optional cloud instance filter (case-insensitive partial match,
|
|
98
|
+
e.g. "GCC" matches "GCC", "GCC High" matches "GCC High").
|
|
99
|
+
feature_id: Optional roadmap ID to retrieve a single specific feature.
|
|
100
|
+
When provided, all other filters are ignored.
|
|
101
|
+
added_within_days: Optional number of days to look back for recently added
|
|
102
|
+
features (clamped to 1–365). Only features with a created date within
|
|
103
|
+
this window are returned.
|
|
104
|
+
release_phase: Optional release phase filter (case-insensitive partial match).
|
|
105
|
+
platform: Optional platform filter (case-insensitive partial match).
|
|
106
|
+
rollout_date: Optional rollout start date filter (partial string match against
|
|
107
|
+
publicDisclosureAvailabilityDate, e.g. "December 2026").
|
|
108
|
+
preview_date: Optional preview availability date filter (partial string match
|
|
109
|
+
against publicPreviewDate, e.g. "July 2026").
|
|
110
|
+
modified_within_days: Optional number of days to look back for recently modified
|
|
111
|
+
features (clamped to 1-365).
|
|
112
|
+
include_facets: When True, includes taxonomy facets (products, statuses,
|
|
113
|
+
release_phases, platforms, cloud_instances) with occurrence counts in
|
|
114
|
+
the response. Use with limit=0 to get only facets without features.
|
|
115
|
+
Facets are computed from matched results after filters are applied.
|
|
116
|
+
limit: Maximum number of results to return (default: 10, max: 100).
|
|
117
|
+
Ignored when feature_id is provided.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Dictionary with:
|
|
121
|
+
- total_found: Number of features matching the filters (before applying limit)
|
|
122
|
+
- features: List of matching feature objects (up to limit)
|
|
123
|
+
- filters_applied: Summary of which filters were used
|
|
124
|
+
- facets: (Optional) Dictionary with facet categories and counts when include_facets=True
|
|
125
|
+
"""
|
|
126
|
+
features = await fetch_features()
|
|
127
|
+
|
|
128
|
+
# Feature ID lookup is a fast path that ignores all other filters
|
|
129
|
+
if feature_id:
|
|
130
|
+
for feature in features:
|
|
131
|
+
if feature.id == feature_id:
|
|
132
|
+
return {
|
|
133
|
+
"total_found": 1,
|
|
134
|
+
"features": [feature.to_dict()],
|
|
135
|
+
"filters_applied": {"feature_id": feature_id},
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
"total_found": 0,
|
|
139
|
+
"features": [],
|
|
140
|
+
"filters_applied": {"feature_id": feature_id},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Clamp limit to reasonable bounds (allow 0 when only requesting facets)
|
|
144
|
+
min_limit = 0 if include_facets else 1
|
|
145
|
+
limit = max(min_limit, min(limit, 100))
|
|
146
|
+
|
|
147
|
+
# Compute recency cutoff if requested
|
|
148
|
+
cutoff = None
|
|
149
|
+
if added_within_days is not None:
|
|
150
|
+
added_within_days = max(1, min(added_within_days, 365))
|
|
151
|
+
cutoff = datetime.now(timezone.utc) - timedelta(days=added_within_days)
|
|
152
|
+
|
|
153
|
+
# Compute modified cutoff if requested
|
|
154
|
+
modified_cutoff = None
|
|
155
|
+
if modified_within_days is not None:
|
|
156
|
+
modified_within_days = max(1, min(modified_within_days, 365))
|
|
157
|
+
modified_cutoff = datetime.now(timezone.utc) - timedelta(days=modified_within_days)
|
|
158
|
+
|
|
159
|
+
# Prepare lowercase values for case-insensitive matching
|
|
160
|
+
query_lower = query.lower() if query else None
|
|
161
|
+
product_lower = product.lower() if product else None
|
|
162
|
+
status_lower = status.lower() if status else None
|
|
163
|
+
cloud_lower = cloud_instance.lower() if cloud_instance else None
|
|
164
|
+
release_phase_lower = release_phase.lower() if release_phase else None
|
|
165
|
+
platform_lower = platform.lower() if platform else None
|
|
166
|
+
rollout_date_lower = rollout_date.lower() if rollout_date else None
|
|
167
|
+
preview_date_lower = preview_date.lower() if preview_date else None
|
|
168
|
+
|
|
169
|
+
# Apply all filters
|
|
170
|
+
matched = []
|
|
171
|
+
for feature in features:
|
|
172
|
+
# Status filter
|
|
173
|
+
if status_lower:
|
|
174
|
+
if not feature.status or feature.status.lower() != status_lower:
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
# Product filter (partial match)
|
|
178
|
+
if product_lower:
|
|
179
|
+
if not any(product_lower in tag.lower() for tag in feature.tags):
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
# Cloud instance filter (partial match)
|
|
183
|
+
if cloud_lower:
|
|
184
|
+
if not any(cloud_lower in ci.lower() for ci in feature.cloud_instances):
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
# Release phase filter (partial match)
|
|
188
|
+
if release_phase_lower:
|
|
189
|
+
if not any(release_phase_lower in rp.lower() for rp in feature.release_phases):
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
# Platform filter (partial match)
|
|
193
|
+
if platform_lower:
|
|
194
|
+
if not any(platform_lower in p.lower() for p in feature.platforms):
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
# Rollout date filter (partial match against publicDisclosureAvailabilityDate)
|
|
198
|
+
# API uses "CY" prefix (e.g., "December CY2026") but users typically omit it
|
|
199
|
+
if rollout_date_lower:
|
|
200
|
+
if not feature.public_disclosure_date:
|
|
201
|
+
continue
|
|
202
|
+
# Normalize by removing "cy" prefix for comparison
|
|
203
|
+
normalized_date = feature.public_disclosure_date.lower().replace(" cy", " ")
|
|
204
|
+
normalized_query = rollout_date_lower.replace(" cy", " ")
|
|
205
|
+
if normalized_query not in normalized_date:
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
# Preview date filter (partial match against publicPreviewDate)
|
|
209
|
+
# API uses "CY" prefix (e.g., "July CY2026") but users typically omit it
|
|
210
|
+
if preview_date_lower:
|
|
211
|
+
if not feature.public_preview_date:
|
|
212
|
+
continue
|
|
213
|
+
# Normalize by removing "cy" prefix for comparison
|
|
214
|
+
normalized_date = feature.public_preview_date.lower().replace(" cy", " ")
|
|
215
|
+
normalized_query = preview_date_lower.replace(" cy", " ")
|
|
216
|
+
if normalized_query not in normalized_date:
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
# Keyword search (title + description)
|
|
220
|
+
if query_lower:
|
|
221
|
+
if (
|
|
222
|
+
query_lower not in feature.title.lower()
|
|
223
|
+
and query_lower not in feature.description.lower()
|
|
224
|
+
):
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
# Recency filter (added_within_days)
|
|
228
|
+
if cutoff is not None:
|
|
229
|
+
if not feature.created:
|
|
230
|
+
continue
|
|
231
|
+
try:
|
|
232
|
+
created_dt = datetime.fromisoformat(feature.created)
|
|
233
|
+
if created_dt.tzinfo is None:
|
|
234
|
+
created_dt = created_dt.replace(tzinfo=timezone.utc)
|
|
235
|
+
if created_dt < cutoff:
|
|
236
|
+
continue
|
|
237
|
+
except (ValueError, TypeError):
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
# Recency filter (modified_within_days)
|
|
241
|
+
if modified_cutoff is not None:
|
|
242
|
+
if not feature.modified:
|
|
243
|
+
continue
|
|
244
|
+
try:
|
|
245
|
+
modified_dt = datetime.fromisoformat(feature.modified)
|
|
246
|
+
if modified_dt.tzinfo is None:
|
|
247
|
+
modified_dt = modified_dt.replace(tzinfo=timezone.utc)
|
|
248
|
+
if modified_dt < modified_cutoff:
|
|
249
|
+
continue
|
|
250
|
+
except (ValueError, TypeError):
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
matched.append(feature)
|
|
254
|
+
|
|
255
|
+
# Build filters summary
|
|
256
|
+
filters_applied: dict = {}
|
|
257
|
+
if query:
|
|
258
|
+
filters_applied["query"] = query
|
|
259
|
+
if product:
|
|
260
|
+
filters_applied["product"] = product
|
|
261
|
+
if status:
|
|
262
|
+
filters_applied["status"] = status
|
|
263
|
+
if cloud_instance:
|
|
264
|
+
filters_applied["cloud_instance"] = cloud_instance
|
|
265
|
+
if added_within_days is not None:
|
|
266
|
+
filters_applied["added_within_days"] = added_within_days
|
|
267
|
+
filters_applied["cutoff_date"] = cutoff.isoformat()
|
|
268
|
+
if modified_within_days is not None:
|
|
269
|
+
filters_applied["modified_within_days"] = modified_within_days
|
|
270
|
+
filters_applied["modified_cutoff_date"] = modified_cutoff.isoformat()
|
|
271
|
+
if release_phase:
|
|
272
|
+
filters_applied["release_phase"] = release_phase
|
|
273
|
+
if platform:
|
|
274
|
+
filters_applied["platform"] = platform
|
|
275
|
+
if rollout_date:
|
|
276
|
+
filters_applied["rollout_date"] = rollout_date
|
|
277
|
+
if preview_date:
|
|
278
|
+
filters_applied["preview_date"] = preview_date
|
|
279
|
+
if not filters_applied:
|
|
280
|
+
filters_applied["note"] = "No filters applied, returning most recent features"
|
|
281
|
+
|
|
282
|
+
# Build response
|
|
283
|
+
result = {
|
|
284
|
+
"total_found": len(matched),
|
|
285
|
+
"features": [f.to_dict() for f in matched[:limit]],
|
|
286
|
+
"filters_applied": filters_applied,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
# Add facets if requested (computed from matched results)
|
|
290
|
+
if include_facets:
|
|
291
|
+
result["facets"] = compute_facets(matched)
|
|
292
|
+
|
|
293
|
+
return result
|
|
@@ -36,3 +36,29 @@ async def test_features_sorted_by_created_date():
|
|
|
36
36
|
# Both should have created dates; newest first
|
|
37
37
|
if features[i].created and features[i + 1].created:
|
|
38
38
|
assert features[i].created >= features[i + 1].created
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.asyncio
|
|
42
|
+
async def test_feature_has_new_fields():
|
|
43
|
+
"""Test that features have the newly added fields."""
|
|
44
|
+
features = await fetch_features()
|
|
45
|
+
|
|
46
|
+
if features:
|
|
47
|
+
feature = features[0]
|
|
48
|
+
assert hasattr(feature, "release_phases")
|
|
49
|
+
assert hasattr(feature, "platforms")
|
|
50
|
+
assert hasattr(feature, "public_preview_date")
|
|
51
|
+
assert isinstance(feature.release_phases, list)
|
|
52
|
+
assert isinstance(feature.platforms, list)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.mark.asyncio
|
|
56
|
+
async def test_feature_with_populated_fields():
|
|
57
|
+
"""Test that at least some features have populated release_phases and platforms."""
|
|
58
|
+
features = await fetch_features()
|
|
59
|
+
|
|
60
|
+
has_release_phase = any(len(f.release_phases) > 0 for f in features)
|
|
61
|
+
has_platform = any(len(f.platforms) > 0 for f in features)
|
|
62
|
+
|
|
63
|
+
assert has_release_phase, "No features have release_phases populated"
|
|
64
|
+
assert has_platform, "No features have platforms populated"
|