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.
Files changed (25) hide show
  1. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/PKG-INFO +6 -1
  2. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/README.md +5 -0
  3. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/pyproject.toml +1 -1
  4. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/server.py +4 -2
  5. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/tools/search.py +78 -5
  6. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/tests/test_tools.py +72 -0
  7. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/.github/workflows/publish.yml +0 -0
  8. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/.gitignore +0 -0
  9. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/LICENSE +0 -0
  10. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/icon.png +0 -0
  11. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/server.json +0 -0
  12. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/__init__.py +0 -0
  13. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/__main__.py +0 -0
  14. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/feeds/__init__.py +0 -0
  15. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/feeds/m365_api.py +0 -0
  16. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/models/__init__.py +0 -0
  17. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/models/feature.py +0 -0
  18. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/tools/__init__.py +0 -0
  19. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/tools/cloud.py +0 -0
  20. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/tools/details.py +0 -0
  21. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/src/m365_roadmap_mcp/tools/recent.py +0 -0
  22. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/tests/__init__.py +0 -0
  23. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/tests/api_snapshot.json +0 -0
  24. {m365_roadmap_mcp-0.3.0 → m365_roadmap_mcp-0.3.1}/tests/rss_feed_snapshot.xml +0 -0
  25. {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.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
@@ -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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "m365-roadmap-mcp"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  description = "MCP server for querying the Microsoft 365 Roadmap"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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\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
- limit = max(1, min(limit, 100))
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 or rollout_date_lower not in feature.public_disclosure_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:
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 or preview_date_lower not in feature.public_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:
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
- return {
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