m365-roadmap-mcp 0.3.0__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.3.0 → m365_roadmap_mcp-0.3.1}/PKG-INFO +6 -1
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/README.md +5 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/pyproject.toml +1 -1
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/server.py +4 -2
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/tools/search.py +78 -5
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/tests/test_tools.py +72 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/.github/workflows/publish.yml +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/.gitignore +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/LICENSE +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/icon.png +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/server.json +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/__init__.py +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/__main__.py +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/feeds/__init__.py +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/feeds/m365_api.py +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/models/__init__.py +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/models/feature.py +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/tools/__init__.py +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/tools/cloud.py +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/tools/details.py +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/tools/recent.py +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/tests/__init__.py +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/tests/api_snapshot.json +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/tests/rss_feed_snapshot.xml +0 -0
- {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/tests/test_feeds.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: m365-roadmap-mcp
|
|
3
|
-
Version: 0.3.
|
|
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
|
|
@@ -189,6 +189,11 @@ Provides a single **`search_roadmap`** tool that handles all M365 roadmap querie
|
|
|
189
189
|
- **Cloud instance filter** -- Filter by cloud instance (GCC, GCC High, DoD)
|
|
190
190
|
- **Feature lookup** -- Retrieve full metadata for a specific roadmap ID
|
|
191
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
|
|
192
197
|
|
|
193
198
|
## Data Source
|
|
194
199
|
|
|
@@ -163,6 +163,11 @@ Provides a single **`search_roadmap`** tool that handles all M365 roadmap querie
|
|
|
163
163
|
- **Cloud instance filter** -- Filter by cloud instance (GCC, GCC High, DoD)
|
|
164
164
|
- **Feature lookup** -- Retrieve full metadata for a specific roadmap ID
|
|
165
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
|
|
166
171
|
|
|
167
172
|
## Data Source
|
|
168
173
|
|
|
@@ -27,7 +27,8 @@ mcp = FastMCP(
|
|
|
27
27
|
"- platform: filter by platform ('Web', 'Desktop', 'iOS', 'Android', 'Mac')\n"
|
|
28
28
|
"- rollout_date: filter by rollout start date (e.g. 'December 2026')\n"
|
|
29
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
|
|
30
|
+
"- modified_within_days: show only features modified within N days\n"
|
|
31
|
+
"- include_facets: include taxonomy facets with counts (default: False)\n\n"
|
|
31
32
|
"Tips:\n"
|
|
32
33
|
"- To get feature details, use feature_id with the roadmap ID.\n"
|
|
33
34
|
"- To check cloud availability, use cloud_instance with a feature_id or "
|
|
@@ -35,7 +36,8 @@ mcp = FastMCP(
|
|
|
35
36
|
"supported instances.\n"
|
|
36
37
|
"- To list recent additions, use added_within_days (e.g. 30 for last month).\n"
|
|
37
38
|
"- To filter by release phase or platform, use release_phase or platform filters.\n"
|
|
38
|
-
"- To search by dates, use rollout_date or preview_date with partial date strings
|
|
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)."
|
|
39
41
|
),
|
|
40
42
|
)
|
|
41
43
|
|
|
@@ -1,8 +1,55 @@
|
|
|
1
1
|
"""Search tool for querying and filtering M365 Roadmap features."""
|
|
2
2
|
|
|
3
|
+
from collections import Counter
|
|
3
4
|
from datetime import datetime, timedelta, timezone
|
|
4
5
|
|
|
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
|
+
}
|
|
6
53
|
|
|
7
54
|
|
|
8
55
|
async def search_roadmap(
|
|
@@ -17,6 +64,7 @@ async def search_roadmap(
|
|
|
17
64
|
rollout_date: str | None = None,
|
|
18
65
|
preview_date: str | None = None,
|
|
19
66
|
modified_within_days: int | None = None,
|
|
67
|
+
include_facets: bool = False,
|
|
20
68
|
limit: int = 10,
|
|
21
69
|
) -> dict:
|
|
22
70
|
"""Search the Microsoft 365 Roadmap for features matching keywords and filters.
|
|
@@ -61,6 +109,10 @@ async def search_roadmap(
|
|
|
61
109
|
against publicPreviewDate, e.g. "July 2026").
|
|
62
110
|
modified_within_days: Optional number of days to look back for recently modified
|
|
63
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.
|
|
64
116
|
limit: Maximum number of results to return (default: 10, max: 100).
|
|
65
117
|
Ignored when feature_id is provided.
|
|
66
118
|
|
|
@@ -69,6 +121,7 @@ async def search_roadmap(
|
|
|
69
121
|
- total_found: Number of features matching the filters (before applying limit)
|
|
70
122
|
- features: List of matching feature objects (up to limit)
|
|
71
123
|
- filters_applied: Summary of which filters were used
|
|
124
|
+
- facets: (Optional) Dictionary with facet categories and counts when include_facets=True
|
|
72
125
|
"""
|
|
73
126
|
features = await fetch_features()
|
|
74
127
|
|
|
@@ -87,8 +140,9 @@ async def search_roadmap(
|
|
|
87
140
|
"filters_applied": {"feature_id": feature_id},
|
|
88
141
|
}
|
|
89
142
|
|
|
90
|
-
# Clamp limit to reasonable bounds
|
|
91
|
-
|
|
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))
|
|
92
146
|
|
|
93
147
|
# Compute recency cutoff if requested
|
|
94
148
|
cutoff = None
|
|
@@ -141,13 +195,25 @@ async def search_roadmap(
|
|
|
141
195
|
continue
|
|
142
196
|
|
|
143
197
|
# Rollout date filter (partial match against publicDisclosureAvailabilityDate)
|
|
198
|
+
# API uses "CY" prefix (e.g., "December CY2026") but users typically omit it
|
|
144
199
|
if rollout_date_lower:
|
|
145
|
-
if not feature.public_disclosure_date
|
|
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:
|
|
146
206
|
continue
|
|
147
207
|
|
|
148
208
|
# Preview date filter (partial match against publicPreviewDate)
|
|
209
|
+
# API uses "CY" prefix (e.g., "July CY2026") but users typically omit it
|
|
149
210
|
if preview_date_lower:
|
|
150
|
-
if not feature.public_preview_date
|
|
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:
|
|
151
217
|
continue
|
|
152
218
|
|
|
153
219
|
# Keyword search (title + description)
|
|
@@ -213,8 +279,15 @@ async def search_roadmap(
|
|
|
213
279
|
if not filters_applied:
|
|
214
280
|
filters_applied["note"] = "No filters applied, returning most recent features"
|
|
215
281
|
|
|
216
|
-
|
|
282
|
+
# Build response
|
|
283
|
+
result = {
|
|
217
284
|
"total_found": len(matched),
|
|
218
285
|
"features": [f.to_dict() for f in matched[:limit]],
|
|
219
286
|
"filters_applied": filters_applied,
|
|
220
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
|
|
@@ -587,3 +587,75 @@ async def test_december_2026_includes_universal_print():
|
|
|
587
587
|
# This should be true if the original issue is fixed
|
|
588
588
|
assert result["total_found"] > 0, "Should return features for December CY2026"
|
|
589
589
|
assert has_universal_print, "Universal Print feature should be in December CY2026 results"
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
@pytest.mark.asyncio
|
|
593
|
+
async def test_include_facets_basic():
|
|
594
|
+
"""include_facets=True returns facets with counts."""
|
|
595
|
+
from m365_roadmap_mcp.tools.search import search_roadmap
|
|
596
|
+
|
|
597
|
+
result = await search_roadmap(include_facets=True, limit=10)
|
|
598
|
+
|
|
599
|
+
# Basic response structure
|
|
600
|
+
assert "total_found" in result
|
|
601
|
+
assert "features" in result
|
|
602
|
+
assert "filters_applied" in result
|
|
603
|
+
assert "facets" in result
|
|
604
|
+
|
|
605
|
+
# Verify facets structure
|
|
606
|
+
facets = result["facets"]
|
|
607
|
+
assert "products" in facets
|
|
608
|
+
assert "statuses" in facets
|
|
609
|
+
assert "release_phases" in facets
|
|
610
|
+
assert "platforms" in facets
|
|
611
|
+
assert "cloud_instances" in facets
|
|
612
|
+
|
|
613
|
+
# Each facet should be a list of dicts with name and count
|
|
614
|
+
for facet_category in facets.values():
|
|
615
|
+
assert isinstance(facet_category, list)
|
|
616
|
+
if len(facet_category) > 0:
|
|
617
|
+
assert "name" in facet_category[0]
|
|
618
|
+
assert "count" in facet_category[0]
|
|
619
|
+
assert isinstance(facet_category[0]["count"], int)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
@pytest.mark.asyncio
|
|
623
|
+
async def test_include_facets_with_limit_zero():
|
|
624
|
+
"""include_facets=True with limit=0 returns only facets, no features."""
|
|
625
|
+
from m365_roadmap_mcp.tools.search import search_roadmap
|
|
626
|
+
|
|
627
|
+
result = await search_roadmap(include_facets=True, limit=0)
|
|
628
|
+
|
|
629
|
+
assert result["total_found"] > 0 # Should have matched features
|
|
630
|
+
assert len(result["features"]) == 0 # But no features returned
|
|
631
|
+
assert "facets" in result
|
|
632
|
+
assert len(result["facets"]["products"]) > 0 # Should have product facets
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
@pytest.mark.asyncio
|
|
636
|
+
async def test_include_facets_with_filter():
|
|
637
|
+
"""Facets are computed from filtered results, not all features."""
|
|
638
|
+
from m365_roadmap_mcp.tools.search import search_roadmap
|
|
639
|
+
|
|
640
|
+
# Get facets for Teams features only
|
|
641
|
+
result = await search_roadmap(product="Teams", include_facets=True, limit=10)
|
|
642
|
+
|
|
643
|
+
assert "facets" in result
|
|
644
|
+
# Verify facets reflect the filtered results
|
|
645
|
+
# The total counts in facets should be <= total_found
|
|
646
|
+
products = result["facets"]["products"]
|
|
647
|
+
total_product_mentions = sum(p["count"] for p in products)
|
|
648
|
+
# Each feature can have multiple product tags, so this is >= total_found
|
|
649
|
+
assert total_product_mentions >= result["total_found"]
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
@pytest.mark.asyncio
|
|
653
|
+
async def test_include_facets_false():
|
|
654
|
+
"""include_facets=False (default) does not include facets."""
|
|
655
|
+
from m365_roadmap_mcp.tools.search import search_roadmap
|
|
656
|
+
|
|
657
|
+
result = await search_roadmap(include_facets=False, limit=10)
|
|
658
|
+
|
|
659
|
+
assert "total_found" in result
|
|
660
|
+
assert "features" in result
|
|
661
|
+
assert "facets" not in result
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|