iflow-mcp_augustab-microsoft-fabric-mcp 0.1.4__py3-none-any.whl
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.
- fabric_mcp.py +2755 -0
- iflow_mcp_augustab_microsoft_fabric_mcp-0.1.4.dist-info/METADATA +285 -0
- iflow_mcp_augustab_microsoft_fabric_mcp-0.1.4.dist-info/RECORD +7 -0
- iflow_mcp_augustab_microsoft_fabric_mcp-0.1.4.dist-info/WHEEL +5 -0
- iflow_mcp_augustab_microsoft_fabric_mcp-0.1.4.dist-info/entry_points.txt +2 -0
- iflow_mcp_augustab_microsoft_fabric_mcp-0.1.4.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_augustab_microsoft_fabric_mcp-0.1.4.dist-info/top_level.txt +1 -0
fabric_mcp.py
ADDED
|
@@ -0,0 +1,2755 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import requests
|
|
4
|
+
import uuid
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Dict, List, Optional, Tuple, Any
|
|
8
|
+
from urllib.parse import quote
|
|
9
|
+
|
|
10
|
+
from azure.identity import DefaultAzureCredential
|
|
11
|
+
from cachetools import TTLCache
|
|
12
|
+
from deltalake import DeltaTable
|
|
13
|
+
from mcp.server.fastmcp import FastMCP
|
|
14
|
+
|
|
15
|
+
# Global caches for name resolution (these never need clearing as name->ID mappings are permanent)
|
|
16
|
+
_global_workspace_cache = {}
|
|
17
|
+
_global_lakehouse_cache = {}
|
|
18
|
+
|
|
19
|
+
# Create MCP instance
|
|
20
|
+
mcp = FastMCP("fabric_schemas")
|
|
21
|
+
|
|
22
|
+
# Set up logging with more robust duplicate prevention
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
logger.setLevel(logging.INFO)
|
|
25
|
+
|
|
26
|
+
# Clear any existing handlers first
|
|
27
|
+
logger.handlers.clear()
|
|
28
|
+
|
|
29
|
+
# Add single handler
|
|
30
|
+
handler = logging.StreamHandler()
|
|
31
|
+
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
32
|
+
handler.setFormatter(formatter)
|
|
33
|
+
logger.addHandler(handler)
|
|
34
|
+
|
|
35
|
+
# Prevent propagation to parent loggers to avoid duplicates
|
|
36
|
+
logger.propagate = False
|
|
37
|
+
|
|
38
|
+
# Global shared caches for all FabricApiClient instances
|
|
39
|
+
_WORKSPACE_CACHE = TTLCache(maxsize=1, ttl=120) # 2 min - single workspace list
|
|
40
|
+
_CONNECTIONS_CACHE = TTLCache(maxsize=1, ttl=600) # 10 min - single connections list
|
|
41
|
+
_CAPACITIES_CACHE = TTLCache(maxsize=1, ttl=900) # 15 min - single capacities list
|
|
42
|
+
_ITEMS_CACHE = TTLCache(maxsize=50, ttl=300) # 5 min - items per workspace
|
|
43
|
+
_SHORTCUTS_CACHE = TTLCache(maxsize=100, ttl=300) # 5 min - shortcuts per item
|
|
44
|
+
_JOB_INSTANCES_CACHE = TTLCache(maxsize=30, ttl=600) # 10 min - jobs per workspace/item
|
|
45
|
+
_SCHEDULES_CACHE = TTLCache(maxsize=30, ttl=300) # 5 min - schedules per item/workspace
|
|
46
|
+
_ENVIRONMENTS_CACHE = TTLCache(
|
|
47
|
+
maxsize=20, ttl=600
|
|
48
|
+
) # 10 min - environments per workspace
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class FabricApiConfig:
|
|
53
|
+
"""Configuration for Fabric API"""
|
|
54
|
+
|
|
55
|
+
base_url: str = "https://api.fabric.microsoft.com/v1"
|
|
56
|
+
max_results: int = 100
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class FabricApiClient:
|
|
60
|
+
"""Client for communicating with the Fabric API"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, credential=None, config: FabricApiConfig = None):
|
|
63
|
+
self.credential = credential or DefaultAzureCredential()
|
|
64
|
+
self.config = config or FabricApiConfig()
|
|
65
|
+
|
|
66
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
67
|
+
"""Get headers for Fabric API calls"""
|
|
68
|
+
return {
|
|
69
|
+
"Authorization": f"Bearer {self.credential.get_token('https://api.fabric.microsoft.com/.default').token}"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async def _make_request(
|
|
73
|
+
self, endpoint: str, params: Optional[Dict] = None, method: str = "GET"
|
|
74
|
+
) -> Dict[str, Any]:
|
|
75
|
+
"""Make an asynchronous call to the Fabric API"""
|
|
76
|
+
# If endpoint is a full URL, use it directly, otherwise add base_url
|
|
77
|
+
url = (
|
|
78
|
+
endpoint
|
|
79
|
+
if endpoint.startswith("http")
|
|
80
|
+
else f"{self.config.base_url}/{endpoint.lstrip('/')}"
|
|
81
|
+
)
|
|
82
|
+
params = params or {}
|
|
83
|
+
|
|
84
|
+
if "maxResults" not in params:
|
|
85
|
+
params["maxResults"] = self.config.max_results
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
response = requests.request(
|
|
89
|
+
method=method, url=url, headers=self._get_headers(), params=params
|
|
90
|
+
)
|
|
91
|
+
response.raise_for_status()
|
|
92
|
+
return response.json()
|
|
93
|
+
except requests.RequestException as e:
|
|
94
|
+
logger.error(f"API call failed: {str(e)}")
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
async def paginated_request(
|
|
98
|
+
self, endpoint: str, params: Optional[Dict] = None, data_key: str = "value"
|
|
99
|
+
) -> List[Dict]:
|
|
100
|
+
"""Make a paginated call to the Fabric API"""
|
|
101
|
+
results = []
|
|
102
|
+
params = params or {}
|
|
103
|
+
continuation_token = None
|
|
104
|
+
|
|
105
|
+
while True:
|
|
106
|
+
# Construct full URL with continuation token if available
|
|
107
|
+
url = f"{self.config.base_url}/{endpoint.lstrip('/')}"
|
|
108
|
+
if continuation_token:
|
|
109
|
+
separator = "&" if "?" in url else "?"
|
|
110
|
+
# URL-encode continuation token
|
|
111
|
+
encoded_token = quote(continuation_token)
|
|
112
|
+
url += f"{separator}continuationToken={encoded_token}"
|
|
113
|
+
|
|
114
|
+
# Use params without continuation token
|
|
115
|
+
request_params = params.copy()
|
|
116
|
+
if "continuationToken" in request_params:
|
|
117
|
+
del request_params["continuationToken"]
|
|
118
|
+
|
|
119
|
+
data = await self._make_request(url, request_params)
|
|
120
|
+
if not data:
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
results.extend(data[data_key])
|
|
124
|
+
|
|
125
|
+
continuation_token = data.get("continuationToken")
|
|
126
|
+
if not continuation_token:
|
|
127
|
+
break
|
|
128
|
+
|
|
129
|
+
return results
|
|
130
|
+
|
|
131
|
+
async def get_workspaces(self) -> List[Dict]:
|
|
132
|
+
"""Get all available workspaces with caching"""
|
|
133
|
+
cache_key = "workspaces"
|
|
134
|
+
if cache_key in _WORKSPACE_CACHE:
|
|
135
|
+
logger.info(
|
|
136
|
+
f"🎯 CACHE HIT: Returning {len(_WORKSPACE_CACHE[cache_key])} workspaces from cache"
|
|
137
|
+
)
|
|
138
|
+
return _WORKSPACE_CACHE[cache_key]
|
|
139
|
+
|
|
140
|
+
logger.info("🔄 CACHE MISS: Fetching workspaces from Fabric API")
|
|
141
|
+
workspaces = await self.paginated_request("workspaces")
|
|
142
|
+
_WORKSPACE_CACHE[cache_key] = workspaces
|
|
143
|
+
logger.info(f"💾 CACHE STORE: Cached {len(workspaces)} workspaces")
|
|
144
|
+
return workspaces
|
|
145
|
+
|
|
146
|
+
async def get_lakehouses(self, workspace_id: str) -> List[Dict]:
|
|
147
|
+
"""Get all lakehouses in a workspace"""
|
|
148
|
+
return await self.paginated_request(
|
|
149
|
+
f"workspaces/{workspace_id}/items", params={"type": "Lakehouse"}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
async def get_tables(self, workspace_id: str, lakehouse_id: str) -> List[Dict]:
|
|
153
|
+
"""Get all tables in a lakehouse"""
|
|
154
|
+
return await self.paginated_request(
|
|
155
|
+
f"workspaces/{workspace_id}/lakehouses/{lakehouse_id}/tables",
|
|
156
|
+
data_key="data",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
async def resolve_workspace(self, workspace: str) -> str:
|
|
160
|
+
"""Convert workspace name or ID to workspace ID with caching"""
|
|
161
|
+
# Check global cache first
|
|
162
|
+
if workspace in _global_workspace_cache:
|
|
163
|
+
return _global_workspace_cache[workspace]
|
|
164
|
+
|
|
165
|
+
# Resolve and cache the result
|
|
166
|
+
result = await self._resolve_workspace(workspace)
|
|
167
|
+
_global_workspace_cache[workspace] = result
|
|
168
|
+
return result
|
|
169
|
+
|
|
170
|
+
async def _resolve_workspace(self, workspace: str) -> str:
|
|
171
|
+
"""Internal method to convert workspace name or ID to workspace ID"""
|
|
172
|
+
# If it's already a valid UUID, return it directly
|
|
173
|
+
if is_valid_uuid(workspace):
|
|
174
|
+
return workspace
|
|
175
|
+
|
|
176
|
+
# Otherwise, look up by name
|
|
177
|
+
workspaces = await self.get_workspaces()
|
|
178
|
+
matching_workspaces = [
|
|
179
|
+
w for w in workspaces if w["displayName"].lower() == workspace.lower()
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
if not matching_workspaces:
|
|
183
|
+
raise ValueError(f"No workspaces found with name: {workspace}")
|
|
184
|
+
if len(matching_workspaces) > 1:
|
|
185
|
+
raise ValueError(f"Multiple workspaces found with name: {workspace}")
|
|
186
|
+
|
|
187
|
+
return matching_workspaces[0]["id"]
|
|
188
|
+
|
|
189
|
+
async def resolve_lakehouse(self, workspace_id: str, lakehouse: str) -> str:
|
|
190
|
+
"""Convert lakehouse name or ID to lakehouse ID with caching"""
|
|
191
|
+
# Create cache key combining workspace_id and lakehouse name
|
|
192
|
+
cache_key = f"{workspace_id}:{lakehouse}"
|
|
193
|
+
|
|
194
|
+
# Check global cache first
|
|
195
|
+
if cache_key in _global_lakehouse_cache:
|
|
196
|
+
return _global_lakehouse_cache[cache_key]
|
|
197
|
+
|
|
198
|
+
# Resolve and cache the result
|
|
199
|
+
result = await self._resolve_lakehouse(workspace_id, lakehouse)
|
|
200
|
+
_global_lakehouse_cache[cache_key] = result
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
async def _resolve_lakehouse(self, workspace_id: str, lakehouse: str) -> str:
|
|
204
|
+
"""Internal method to convert lakehouse name or ID to lakehouse ID"""
|
|
205
|
+
if is_valid_uuid(lakehouse):
|
|
206
|
+
# Cache UUID mappings too (workspace_id:UUID -> UUID)
|
|
207
|
+
cache_key = f"{workspace_id}:{lakehouse}"
|
|
208
|
+
_global_lakehouse_cache[cache_key] = lakehouse
|
|
209
|
+
return lakehouse
|
|
210
|
+
|
|
211
|
+
lakehouses = await self.get_lakehouses(workspace_id)
|
|
212
|
+
matching_lakehouses = [
|
|
213
|
+
lh for lh in lakehouses if lh["displayName"].lower() == lakehouse.lower()
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
if not matching_lakehouses:
|
|
217
|
+
raise ValueError(f"No lakehouse found with name: {lakehouse}")
|
|
218
|
+
if len(matching_lakehouses) > 1:
|
|
219
|
+
raise ValueError(f"Multiple lakehouses found with name: {lakehouse}")
|
|
220
|
+
|
|
221
|
+
return matching_lakehouses[0]["id"]
|
|
222
|
+
|
|
223
|
+
async def get_connections(self) -> List[Dict]:
|
|
224
|
+
"""Get all connections user has access to with caching"""
|
|
225
|
+
cache_key = "connections"
|
|
226
|
+
if cache_key in _CONNECTIONS_CACHE:
|
|
227
|
+
logger.info(
|
|
228
|
+
f"🎯 CACHE HIT: Returning {len(_CONNECTIONS_CACHE[cache_key])} connections from cache"
|
|
229
|
+
)
|
|
230
|
+
return _CONNECTIONS_CACHE[cache_key]
|
|
231
|
+
|
|
232
|
+
logger.info("🔄 CACHE MISS: Fetching connections from Fabric API")
|
|
233
|
+
connections = await self.paginated_request("connections")
|
|
234
|
+
_CONNECTIONS_CACHE[cache_key] = connections
|
|
235
|
+
logger.info(f"💾 CACHE STORE: Cached {len(connections)} connections")
|
|
236
|
+
return connections
|
|
237
|
+
|
|
238
|
+
async def get_items(self, workspace_id: str, item_type: str = None) -> List[Dict]:
|
|
239
|
+
"""Get all items in workspace, optionally filtered by type with caching"""
|
|
240
|
+
cache_key = f"{workspace_id}:{item_type or 'all'}"
|
|
241
|
+
if cache_key in _ITEMS_CACHE:
|
|
242
|
+
logger.info(
|
|
243
|
+
f"🎯 CACHE HIT: Returning {len(_ITEMS_CACHE[cache_key])} items from cache (key: {cache_key})"
|
|
244
|
+
)
|
|
245
|
+
return _ITEMS_CACHE[cache_key]
|
|
246
|
+
|
|
247
|
+
logger.info(f"🔄 CACHE MISS: Fetching items from Fabric API (key: {cache_key})")
|
|
248
|
+
params = {"type": item_type} if item_type else {}
|
|
249
|
+
items = await self.paginated_request(
|
|
250
|
+
f"workspaces/{workspace_id}/items", params=params
|
|
251
|
+
)
|
|
252
|
+
_ITEMS_CACHE[cache_key] = items
|
|
253
|
+
logger.info(f"💾 CACHE STORE: Cached {len(items)} items (key: {cache_key})")
|
|
254
|
+
return items
|
|
255
|
+
|
|
256
|
+
async def get_item(self, workspace_id: str, item_id: str) -> Dict:
|
|
257
|
+
"""Get specific item details"""
|
|
258
|
+
return await self._make_request(f"workspaces/{workspace_id}/items/{item_id}")
|
|
259
|
+
|
|
260
|
+
async def get_workspace_details(self, workspace_id: str) -> Dict:
|
|
261
|
+
"""Get detailed workspace information"""
|
|
262
|
+
return await self._make_request(f"workspaces/{workspace_id}")
|
|
263
|
+
|
|
264
|
+
async def get_capacities(self) -> List[Dict]:
|
|
265
|
+
"""Get all capacities user has access to with caching"""
|
|
266
|
+
cache_key = "capacities"
|
|
267
|
+
if cache_key in _CAPACITIES_CACHE:
|
|
268
|
+
return _CAPACITIES_CACHE[cache_key]
|
|
269
|
+
|
|
270
|
+
capacities = await self.paginated_request("capacities")
|
|
271
|
+
_CAPACITIES_CACHE[cache_key] = capacities
|
|
272
|
+
return capacities
|
|
273
|
+
|
|
274
|
+
async def get_job_instances(
|
|
275
|
+
self, workspace_id: str, item_id: str = None, status: str = None
|
|
276
|
+
) -> List[Dict]:
|
|
277
|
+
"""Get job instances, optionally filtered by item and status"""
|
|
278
|
+
# Create cache key based on parameters
|
|
279
|
+
cache_key = f"{workspace_id}:{item_id or 'all'}:{status or 'all'}"
|
|
280
|
+
if cache_key in _JOB_INSTANCES_CACHE:
|
|
281
|
+
return _JOB_INSTANCES_CACHE[cache_key]
|
|
282
|
+
|
|
283
|
+
if not item_id:
|
|
284
|
+
# Get all items and collect their job instances
|
|
285
|
+
items = await self.get_items(workspace_id)
|
|
286
|
+
all_jobs = []
|
|
287
|
+
for item in items:
|
|
288
|
+
try:
|
|
289
|
+
jobs = await self.paginated_request(
|
|
290
|
+
f"workspaces/{workspace_id}/items/{item['id']}/jobs/instances"
|
|
291
|
+
)
|
|
292
|
+
for job in jobs:
|
|
293
|
+
job["itemName"] = item["displayName"]
|
|
294
|
+
job["itemType"] = item["type"]
|
|
295
|
+
if (
|
|
296
|
+
not status
|
|
297
|
+
or job.get("status", "").lower() == status.lower()
|
|
298
|
+
):
|
|
299
|
+
all_jobs.append(job)
|
|
300
|
+
except Exception:
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
# Cache the results
|
|
304
|
+
_JOB_INSTANCES_CACHE[cache_key] = all_jobs
|
|
305
|
+
return all_jobs
|
|
306
|
+
else:
|
|
307
|
+
jobs = await self.paginated_request(
|
|
308
|
+
f"workspaces/{workspace_id}/items/{item_id}/jobs/instances"
|
|
309
|
+
)
|
|
310
|
+
if status:
|
|
311
|
+
jobs = [
|
|
312
|
+
job
|
|
313
|
+
for job in jobs
|
|
314
|
+
if job.get("status", "").lower() == status.lower()
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
# Cache the results
|
|
318
|
+
_JOB_INSTANCES_CACHE[cache_key] = jobs
|
|
319
|
+
return jobs
|
|
320
|
+
|
|
321
|
+
async def get_job_instance(
|
|
322
|
+
self, workspace_id: str, item_id: str, job_instance_id: str
|
|
323
|
+
) -> Dict:
|
|
324
|
+
"""Get specific job instance details"""
|
|
325
|
+
return await self._make_request(
|
|
326
|
+
f"workspaces/{workspace_id}/items/{item_id}/jobs/instances/{job_instance_id}"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
async def get_item_schedules(self, workspace_id: str, item_id: str) -> List[Dict]:
|
|
330
|
+
"""Get all schedules for a specific item"""
|
|
331
|
+
# Create cache key for item schedules
|
|
332
|
+
cache_key = f"item:{workspace_id}:{item_id}"
|
|
333
|
+
if cache_key in _SCHEDULES_CACHE:
|
|
334
|
+
return _SCHEDULES_CACHE[cache_key]
|
|
335
|
+
|
|
336
|
+
schedules = await self.paginated_request(
|
|
337
|
+
f"workspaces/{workspace_id}/items/{item_id}/schedules"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Cache the results
|
|
341
|
+
_SCHEDULES_CACHE[cache_key] = schedules
|
|
342
|
+
return schedules
|
|
343
|
+
|
|
344
|
+
async def get_workspace_schedules(self, workspace_id: str) -> List[Dict]:
|
|
345
|
+
"""Get all schedules across all items in workspace"""
|
|
346
|
+
# Create cache key for workspace schedules
|
|
347
|
+
cache_key = f"workspace:{workspace_id}"
|
|
348
|
+
if cache_key in _SCHEDULES_CACHE:
|
|
349
|
+
return _SCHEDULES_CACHE[cache_key]
|
|
350
|
+
|
|
351
|
+
items = await self.get_items(workspace_id)
|
|
352
|
+
all_schedules = []
|
|
353
|
+
|
|
354
|
+
for item in items:
|
|
355
|
+
try:
|
|
356
|
+
schedules = await self.paginated_request(
|
|
357
|
+
f"workspaces/{workspace_id}/items/{item['id']}/schedules"
|
|
358
|
+
)
|
|
359
|
+
for schedule in schedules:
|
|
360
|
+
schedule["itemName"] = item["displayName"]
|
|
361
|
+
schedule["itemType"] = item["type"]
|
|
362
|
+
schedule["itemId"] = item["id"]
|
|
363
|
+
all_schedules.append(schedule)
|
|
364
|
+
except Exception:
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
# Cache the results
|
|
368
|
+
_SCHEDULES_CACHE[cache_key] = all_schedules
|
|
369
|
+
return all_schedules
|
|
370
|
+
|
|
371
|
+
async def get_environments(self, workspace_id: str = None) -> List[Dict]:
|
|
372
|
+
"""Get environments, optionally filtered by workspace"""
|
|
373
|
+
# Create cache key based on workspace filter
|
|
374
|
+
cache_key = f"environments:{workspace_id or 'all'}"
|
|
375
|
+
if cache_key in _ENVIRONMENTS_CACHE:
|
|
376
|
+
return _ENVIRONMENTS_CACHE[cache_key]
|
|
377
|
+
|
|
378
|
+
if workspace_id:
|
|
379
|
+
environments = await self.paginated_request(
|
|
380
|
+
f"workspaces/{workspace_id}/items", params={"type": "Environment"}
|
|
381
|
+
)
|
|
382
|
+
else:
|
|
383
|
+
# Get all accessible workspaces and their environments
|
|
384
|
+
workspaces = await self.get_workspaces()
|
|
385
|
+
environments = []
|
|
386
|
+
|
|
387
|
+
for ws in workspaces:
|
|
388
|
+
try:
|
|
389
|
+
ws_environments = await self.paginated_request(
|
|
390
|
+
f"workspaces/{ws['id']}/items", params={"type": "Environment"}
|
|
391
|
+
)
|
|
392
|
+
for env in ws_environments:
|
|
393
|
+
env["workspaceName"] = ws["displayName"]
|
|
394
|
+
environments.append(env)
|
|
395
|
+
except Exception:
|
|
396
|
+
continue
|
|
397
|
+
|
|
398
|
+
# Cache the results
|
|
399
|
+
_ENVIRONMENTS_CACHE[cache_key] = environments
|
|
400
|
+
return environments
|
|
401
|
+
|
|
402
|
+
async def get_environment_details(
|
|
403
|
+
self, workspace_id: str, environment_id: str
|
|
404
|
+
) -> Dict:
|
|
405
|
+
"""Get detailed environment configuration"""
|
|
406
|
+
try:
|
|
407
|
+
environment = await self._make_request(
|
|
408
|
+
f"workspaces/{workspace_id}/items/{environment_id}"
|
|
409
|
+
)
|
|
410
|
+
sparkcompute = await self._make_request(
|
|
411
|
+
f"workspaces/{workspace_id}/environments/{environment_id}/sparkcompute"
|
|
412
|
+
)
|
|
413
|
+
libraries = await self._make_request(
|
|
414
|
+
f"workspaces/{workspace_id}/environments/{environment_id}/libraries"
|
|
415
|
+
)
|
|
416
|
+
return {
|
|
417
|
+
"environment": environment,
|
|
418
|
+
"sparkcompute": sparkcompute,
|
|
419
|
+
"libraries": libraries,
|
|
420
|
+
}
|
|
421
|
+
except Exception:
|
|
422
|
+
# Return partial data if some calls fail
|
|
423
|
+
environment = await self._make_request(
|
|
424
|
+
f"workspaces/{workspace_id}/items/{environment_id}"
|
|
425
|
+
)
|
|
426
|
+
return {"environment": environment, "sparkcompute": None, "libraries": None}
|
|
427
|
+
|
|
428
|
+
async def get_shortcuts(
|
|
429
|
+
self, workspace_id: str, item_id: str, parent_path: str = None
|
|
430
|
+
) -> List[Dict]:
|
|
431
|
+
"""Get OneLake shortcuts for a specific item"""
|
|
432
|
+
# Create cache key based on parameters
|
|
433
|
+
cache_key = f"{workspace_id}:{item_id}:{parent_path or 'root'}"
|
|
434
|
+
if cache_key in _SHORTCUTS_CACHE:
|
|
435
|
+
return _SHORTCUTS_CACHE[cache_key]
|
|
436
|
+
|
|
437
|
+
endpoint = f"workspaces/{workspace_id}/items/{item_id}/shortcuts"
|
|
438
|
+
params = {"path": parent_path} if parent_path else {}
|
|
439
|
+
shortcuts_response = await self._make_request(endpoint, params)
|
|
440
|
+
shortcuts = (
|
|
441
|
+
shortcuts_response.get("shortcuts", []) if shortcuts_response else []
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Cache the results
|
|
445
|
+
_SHORTCUTS_CACHE[cache_key] = shortcuts
|
|
446
|
+
return shortcuts
|
|
447
|
+
|
|
448
|
+
async def get_shortcut(
|
|
449
|
+
self,
|
|
450
|
+
workspace_id: str,
|
|
451
|
+
item_id: str,
|
|
452
|
+
shortcut_name: str,
|
|
453
|
+
parent_path: str = None,
|
|
454
|
+
) -> Dict:
|
|
455
|
+
"""Get specific shortcut details"""
|
|
456
|
+
path_segment = f"/{parent_path.strip('/')}" if parent_path else ""
|
|
457
|
+
endpoint = f"workspaces/{workspace_id}/items/{item_id}/shortcuts/{shortcut_name}{path_segment}"
|
|
458
|
+
return await self._make_request(endpoint)
|
|
459
|
+
|
|
460
|
+
async def get_workspace_shortcuts(self, workspace_id: str) -> List[Dict]:
|
|
461
|
+
"""Get all shortcuts across all items in workspace"""
|
|
462
|
+
# Create cache key for workspace shortcuts
|
|
463
|
+
cache_key = f"workspace:{workspace_id}"
|
|
464
|
+
if cache_key in _SHORTCUTS_CACHE:
|
|
465
|
+
logger.info(
|
|
466
|
+
f"🎯 CACHE HIT: Returning {len(_SHORTCUTS_CACHE[cache_key])} workspace shortcuts from cache (key: {cache_key})"
|
|
467
|
+
)
|
|
468
|
+
return _SHORTCUTS_CACHE[cache_key]
|
|
469
|
+
|
|
470
|
+
logger.info(
|
|
471
|
+
f"🔄 CACHE MISS: Fetching workspace shortcuts from Fabric API (key: {cache_key})"
|
|
472
|
+
)
|
|
473
|
+
items = await self.get_items(workspace_id)
|
|
474
|
+
all_shortcuts = []
|
|
475
|
+
|
|
476
|
+
for item in items:
|
|
477
|
+
if item["type"] in ["Lakehouse", "KqlDatabase"]:
|
|
478
|
+
try:
|
|
479
|
+
shortcuts_response = await self._make_request(
|
|
480
|
+
f"workspaces/{workspace_id}/items/{item['id']}/shortcuts"
|
|
481
|
+
)
|
|
482
|
+
shortcuts = (
|
|
483
|
+
shortcuts_response.get("shortcuts", [])
|
|
484
|
+
if shortcuts_response
|
|
485
|
+
else []
|
|
486
|
+
)
|
|
487
|
+
for shortcut in shortcuts:
|
|
488
|
+
shortcut["itemName"] = item["displayName"]
|
|
489
|
+
shortcut["itemType"] = item["type"]
|
|
490
|
+
all_shortcuts.append(shortcut)
|
|
491
|
+
except Exception:
|
|
492
|
+
continue
|
|
493
|
+
|
|
494
|
+
# Cache the results
|
|
495
|
+
_SHORTCUTS_CACHE[cache_key] = all_shortcuts
|
|
496
|
+
logger.info(
|
|
497
|
+
f"💾 CACHE STORE: Cached {len(all_shortcuts)} workspace shortcuts (key: {cache_key})"
|
|
498
|
+
)
|
|
499
|
+
return all_shortcuts
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def is_valid_uuid(value: str) -> bool:
|
|
503
|
+
"""Check if a string is a valid UUID"""
|
|
504
|
+
try:
|
|
505
|
+
uuid.UUID(value)
|
|
506
|
+
return True
|
|
507
|
+
except ValueError:
|
|
508
|
+
return False
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
async def get_delta_schemas(
|
|
512
|
+
tables: List[Dict], credential: DefaultAzureCredential
|
|
513
|
+
) -> List[Tuple[Dict, object, object]]:
|
|
514
|
+
"""Get schema and metadata for each Delta table"""
|
|
515
|
+
delta_tables = []
|
|
516
|
+
logger.info(f"Starting schema extraction for {len(tables)} tables")
|
|
517
|
+
|
|
518
|
+
# Get token for Azure Storage (not Fabric API)
|
|
519
|
+
token = credential.get_token("https://storage.azure.com/.default").token
|
|
520
|
+
storage_options = {"bearer_token": token, "use_fabric_endpoint": "true"}
|
|
521
|
+
|
|
522
|
+
for table in tables:
|
|
523
|
+
if table["format"].lower() == "delta":
|
|
524
|
+
try:
|
|
525
|
+
table_path = table["location"]
|
|
526
|
+
logger.debug(f"Processing Delta table: {table['name']} at {table_path}")
|
|
527
|
+
|
|
528
|
+
# Create DeltaTable instance with storage options
|
|
529
|
+
delta_table = DeltaTable(table_path, storage_options=storage_options)
|
|
530
|
+
|
|
531
|
+
# Get both schema and metadata
|
|
532
|
+
delta_tables.append(
|
|
533
|
+
(table, delta_table.schema(), delta_table.metadata())
|
|
534
|
+
)
|
|
535
|
+
logger.info(f"Processed table: {table['name']}")
|
|
536
|
+
|
|
537
|
+
except Exception as e:
|
|
538
|
+
logger.error(f"Could not process table {table['name']}: {str(e)}")
|
|
539
|
+
|
|
540
|
+
return delta_tables
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def format_metadata_to_markdown(metadata: object) -> str:
|
|
544
|
+
"""Convert Delta table metadata to markdown format"""
|
|
545
|
+
markdown = "### Metadata\n\n"
|
|
546
|
+
|
|
547
|
+
markdown += f"**ID:** {metadata.id}\n\n"
|
|
548
|
+
if metadata.name:
|
|
549
|
+
markdown += f"**Name:** {metadata.name}\n\n"
|
|
550
|
+
if metadata.description:
|
|
551
|
+
markdown += f"**Description:** {metadata.description}\n\n"
|
|
552
|
+
if metadata.partition_columns:
|
|
553
|
+
markdown += (
|
|
554
|
+
f"**Partition Columns:** {', '.join(metadata.partition_columns)}\n\n"
|
|
555
|
+
)
|
|
556
|
+
if metadata.created_time:
|
|
557
|
+
created_time = datetime.fromtimestamp(metadata.created_time / 1000)
|
|
558
|
+
markdown += f"**Created:** {created_time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
559
|
+
|
|
560
|
+
if metadata.configuration:
|
|
561
|
+
markdown += "**Configuration:**\n\n"
|
|
562
|
+
markdown += "```json\n"
|
|
563
|
+
markdown += json.dumps(metadata.configuration, indent=2)
|
|
564
|
+
markdown += "\n```\n"
|
|
565
|
+
|
|
566
|
+
return markdown
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def format_schema_to_markdown(
|
|
570
|
+
table_info: Dict, schema: object, metadata: object
|
|
571
|
+
) -> str:
|
|
572
|
+
"""Convert a Delta table schema and metadata to markdown format"""
|
|
573
|
+
markdown = f"## Delta Table: `{table_info['name']}`\n\n"
|
|
574
|
+
markdown += f"**Type:** {table_info['type']}\n\n"
|
|
575
|
+
markdown += f"**Location:** `{table_info['location']}`\n\n"
|
|
576
|
+
|
|
577
|
+
# Add schema information
|
|
578
|
+
markdown += "### Schema\n\n"
|
|
579
|
+
markdown += "| Column Name | Data Type | Nullable |\n"
|
|
580
|
+
markdown += "|------------|-----------|----------|\n"
|
|
581
|
+
|
|
582
|
+
for field in schema.fields:
|
|
583
|
+
name = field.name
|
|
584
|
+
dtype = field.type
|
|
585
|
+
nullable = field.nullable
|
|
586
|
+
markdown += f"| {name} | {dtype} | {nullable} |\n"
|
|
587
|
+
|
|
588
|
+
markdown += "\n"
|
|
589
|
+
|
|
590
|
+
# Add metadata information
|
|
591
|
+
markdown += format_metadata_to_markdown(metadata)
|
|
592
|
+
|
|
593
|
+
return markdown + "\n"
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
@mcp.tool()
|
|
597
|
+
async def get_table_schema(workspace: str, lakehouse: str, table_name: str) -> str:
|
|
598
|
+
"""Get schema for a specific table in a Fabric lakehouse.
|
|
599
|
+
|
|
600
|
+
Args:
|
|
601
|
+
workspace: Name or ID of the workspace
|
|
602
|
+
lakehouse: Name or ID of the lakehouse
|
|
603
|
+
table_name: Name of the table to retrieve
|
|
604
|
+
"""
|
|
605
|
+
try:
|
|
606
|
+
credential = DefaultAzureCredential()
|
|
607
|
+
client = FabricApiClient(credential)
|
|
608
|
+
|
|
609
|
+
# Convert names to IDs
|
|
610
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
611
|
+
lakehouse_id = await client.resolve_lakehouse(workspace_id, lakehouse)
|
|
612
|
+
|
|
613
|
+
# Get all tables
|
|
614
|
+
tables = await client.get_tables(workspace_id, lakehouse_id)
|
|
615
|
+
|
|
616
|
+
# Find the specific table
|
|
617
|
+
matching_tables = [t for t in tables if t["name"].lower() == table_name.lower()]
|
|
618
|
+
|
|
619
|
+
if not matching_tables:
|
|
620
|
+
return (
|
|
621
|
+
f"No table found with name '{table_name}' in lakehouse '{lakehouse}'."
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
table = matching_tables[0]
|
|
625
|
+
|
|
626
|
+
# Check that it is a Delta table
|
|
627
|
+
if table["format"].lower() != "delta":
|
|
628
|
+
return f"The table '{table_name}' is not a Delta table (format: {table['format']})."
|
|
629
|
+
|
|
630
|
+
# Get schema
|
|
631
|
+
delta_tables = await get_delta_schemas([table], credential)
|
|
632
|
+
|
|
633
|
+
if not delta_tables:
|
|
634
|
+
return f"Could not retrieve schema for table '{table_name}'."
|
|
635
|
+
|
|
636
|
+
# Format result as markdown
|
|
637
|
+
table_info, schema, metadata = delta_tables[0]
|
|
638
|
+
markdown = format_schema_to_markdown(table_info, schema, metadata)
|
|
639
|
+
|
|
640
|
+
return markdown
|
|
641
|
+
|
|
642
|
+
except Exception as e:
|
|
643
|
+
return f"Error retrieving table schema: {str(e)}"
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
@mcp.tool()
|
|
647
|
+
async def get_all_schemas(workspace: str, lakehouse: str) -> str:
|
|
648
|
+
"""Get schemas for all Delta tables in a Fabric lakehouse.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
workspace: Name or ID of the workspace
|
|
652
|
+
lakehouse: Name or ID of the lakehouse
|
|
653
|
+
"""
|
|
654
|
+
try:
|
|
655
|
+
credential = DefaultAzureCredential()
|
|
656
|
+
client = FabricApiClient(credential)
|
|
657
|
+
|
|
658
|
+
# Convert names to IDs
|
|
659
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
660
|
+
lakehouse_id = await client.resolve_lakehouse(workspace_id, lakehouse)
|
|
661
|
+
|
|
662
|
+
# Get all tables
|
|
663
|
+
tables = await client.get_tables(workspace_id, lakehouse_id)
|
|
664
|
+
|
|
665
|
+
if not tables:
|
|
666
|
+
return f"No tables found in lakehouse '{lakehouse}'."
|
|
667
|
+
|
|
668
|
+
# Filter to only Delta tables
|
|
669
|
+
delta_format_tables = [t for t in tables if t["format"].lower() == "delta"]
|
|
670
|
+
|
|
671
|
+
if not delta_format_tables:
|
|
672
|
+
return f"No Delta tables found in lakehouse '{lakehouse}'."
|
|
673
|
+
|
|
674
|
+
# Get schema for all tables
|
|
675
|
+
delta_tables = await get_delta_schemas(delta_format_tables, credential)
|
|
676
|
+
|
|
677
|
+
if not delta_tables:
|
|
678
|
+
return "Could not retrieve schemas for any tables."
|
|
679
|
+
|
|
680
|
+
# Format the result as markdown
|
|
681
|
+
markdown = f"# Delta Table Schemas\n\n"
|
|
682
|
+
markdown += f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
683
|
+
markdown += f"Workspace: {workspace}\n"
|
|
684
|
+
markdown += f"Lakehouse: {lakehouse}\n\n"
|
|
685
|
+
|
|
686
|
+
for table_info, schema, metadata in delta_tables:
|
|
687
|
+
markdown += format_schema_to_markdown(table_info, schema, metadata)
|
|
688
|
+
|
|
689
|
+
return markdown
|
|
690
|
+
|
|
691
|
+
except Exception as e:
|
|
692
|
+
return f"Error retrieving table schemas: {str(e)}"
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
@mcp.tool()
|
|
696
|
+
async def list_workspaces() -> str:
|
|
697
|
+
"""List all available Fabric workspaces."""
|
|
698
|
+
try:
|
|
699
|
+
credential = DefaultAzureCredential()
|
|
700
|
+
client = FabricApiClient(credential)
|
|
701
|
+
|
|
702
|
+
workspaces = await client.get_workspaces()
|
|
703
|
+
|
|
704
|
+
if not workspaces:
|
|
705
|
+
return "No workspaces found."
|
|
706
|
+
|
|
707
|
+
markdown = "# Fabric Workspaces\n\n"
|
|
708
|
+
markdown += "| ID | Name | Capacity |\n"
|
|
709
|
+
markdown += "|-----|------|----------|\n"
|
|
710
|
+
|
|
711
|
+
for ws in workspaces:
|
|
712
|
+
markdown += f"| {ws['id']} | {ws['displayName']} | {ws.get('capacityId', 'N/A')} |\n"
|
|
713
|
+
|
|
714
|
+
return markdown
|
|
715
|
+
|
|
716
|
+
except Exception as e:
|
|
717
|
+
return f"Error listing workspaces: {str(e)}"
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@mcp.tool()
|
|
721
|
+
async def list_lakehouses(workspace: str) -> str:
|
|
722
|
+
"""List all lakehouses in a Fabric workspace.
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
workspace: Name or ID of the workspace
|
|
726
|
+
"""
|
|
727
|
+
try:
|
|
728
|
+
credential = DefaultAzureCredential()
|
|
729
|
+
client = FabricApiClient(credential)
|
|
730
|
+
|
|
731
|
+
# Convert name to ID
|
|
732
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
733
|
+
|
|
734
|
+
lakehouses = await client.get_lakehouses(workspace_id)
|
|
735
|
+
|
|
736
|
+
if not lakehouses:
|
|
737
|
+
return f"No lakehouses found in workspace '{workspace}'."
|
|
738
|
+
|
|
739
|
+
markdown = f"# Lakehouses in workspace '{workspace}'\n\n"
|
|
740
|
+
markdown += "| ID | Name |\n"
|
|
741
|
+
markdown += "|-----|------|\n"
|
|
742
|
+
|
|
743
|
+
for lh in lakehouses:
|
|
744
|
+
markdown += f"| {lh['id']} | {lh['displayName']} |\n"
|
|
745
|
+
|
|
746
|
+
return markdown
|
|
747
|
+
|
|
748
|
+
except Exception as e:
|
|
749
|
+
return f"Error listing lakehouses: {str(e)}"
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
@mcp.tool()
|
|
753
|
+
async def list_tables(workspace: str, lakehouse: str) -> str:
|
|
754
|
+
"""List all tables in a Fabric lakehouse.
|
|
755
|
+
|
|
756
|
+
Args:
|
|
757
|
+
workspace: Name or ID of the workspace
|
|
758
|
+
lakehouse: Name or ID of the lakehouse
|
|
759
|
+
"""
|
|
760
|
+
try:
|
|
761
|
+
credential = DefaultAzureCredential()
|
|
762
|
+
client = FabricApiClient(credential)
|
|
763
|
+
|
|
764
|
+
# Convert names to IDs
|
|
765
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
766
|
+
lakehouse_id = await client.resolve_lakehouse(workspace_id, lakehouse)
|
|
767
|
+
|
|
768
|
+
tables = await client.get_tables(workspace_id, lakehouse_id)
|
|
769
|
+
|
|
770
|
+
if not tables:
|
|
771
|
+
return f"No tables found in lakehouse '{lakehouse}'."
|
|
772
|
+
|
|
773
|
+
markdown = f"# Tables in lakehouse '{lakehouse}'\n\n"
|
|
774
|
+
markdown += "| Name | Format | Type |\n"
|
|
775
|
+
markdown += "|------|--------|------|\n"
|
|
776
|
+
|
|
777
|
+
for table in tables:
|
|
778
|
+
markdown += f"| {table['name']} | {table['format']} | {table['type']} |\n"
|
|
779
|
+
|
|
780
|
+
return markdown
|
|
781
|
+
|
|
782
|
+
except Exception as e:
|
|
783
|
+
return f"Error listing tables: {str(e)}"
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
@mcp.tool()
|
|
787
|
+
async def list_connections() -> str:
|
|
788
|
+
"""List all connections the user has permission for across the entire Fabric tenant (READ-ONLY).
|
|
789
|
+
|
|
790
|
+
⚠️ SECURITY LIMITATION: This only returns connections the authenticated user/service principal
|
|
791
|
+
has permission for. To get ALL tenant connections, you need:
|
|
792
|
+
1. Service Principal with broader workspace access, OR
|
|
793
|
+
2. Admin-level API access (if available), OR
|
|
794
|
+
3. Aggregate results from multiple users
|
|
795
|
+
|
|
796
|
+
This returns ALL connections the user can access, not limited to any specific workspace.
|
|
797
|
+
"""
|
|
798
|
+
try:
|
|
799
|
+
credential = DefaultAzureCredential()
|
|
800
|
+
client = FabricApiClient(credential)
|
|
801
|
+
|
|
802
|
+
# Get connections from the connections endpoint (limited to user's permissions)
|
|
803
|
+
connections = await client.get_connections()
|
|
804
|
+
|
|
805
|
+
if not connections:
|
|
806
|
+
return "No connections found for the current user/service principal."
|
|
807
|
+
|
|
808
|
+
markdown = "# Fabric Connections (User-Scoped)\n\n"
|
|
809
|
+
markdown += f"**Total Connections Accessible:** {len(connections)}\n\n"
|
|
810
|
+
|
|
811
|
+
# Add security warning
|
|
812
|
+
markdown += "⚠️ **Security Note**: This list only includes connections you have permission to access.\n"
|
|
813
|
+
markdown += "To get ALL tenant connections, consider using a Service Principal with broader permissions.\n\n"
|
|
814
|
+
|
|
815
|
+
markdown += "| ID | Display Name | Type | Connectivity Type | Privacy Level |\n"
|
|
816
|
+
markdown += "|-----|--------------|------|------------------|---------------|\n"
|
|
817
|
+
|
|
818
|
+
for conn in connections:
|
|
819
|
+
conn_type = conn.get("connectionDetails", {}).get("type", "N/A")
|
|
820
|
+
connectivity_type = conn.get("connectivityType", "N/A")
|
|
821
|
+
privacy_level = conn.get("privacyLevel", "N/A")
|
|
822
|
+
markdown += f"| {conn['id']} | {conn['displayName']} | {conn_type} | {connectivity_type} | {privacy_level} |\n"
|
|
823
|
+
|
|
824
|
+
markdown += "\n## Getting More Connections\n\n"
|
|
825
|
+
markdown += "To access more connections across the tenant:\n\n"
|
|
826
|
+
markdown += "1. **Service Principal Approach**: Use a Service Principal with access to more workspaces\n"
|
|
827
|
+
markdown += "2. **Multi-User Aggregation**: Run this tool with different user credentials and combine results\n"
|
|
828
|
+
markdown += "3. **Admin Access**: Check if your organization has Admin APIs enabled for broader access\n"
|
|
829
|
+
|
|
830
|
+
return markdown
|
|
831
|
+
|
|
832
|
+
except Exception as e:
|
|
833
|
+
return f"Error listing connections: {str(e)}"
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
@mcp.tool()
|
|
837
|
+
async def list_items(workspace: str, item_type: str = None) -> str:
|
|
838
|
+
"""List all items in a Fabric workspace (READ-ONLY).
|
|
839
|
+
|
|
840
|
+
Args:
|
|
841
|
+
workspace: Name or ID of the workspace
|
|
842
|
+
item_type: Optional filter by item type (e.g., 'Lakehouse', 'Notebook', 'DataPipeline')
|
|
843
|
+
"""
|
|
844
|
+
try:
|
|
845
|
+
credential = DefaultAzureCredential()
|
|
846
|
+
client = FabricApiClient(credential)
|
|
847
|
+
|
|
848
|
+
# Convert name to ID
|
|
849
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
850
|
+
|
|
851
|
+
# Get items with optional type filter
|
|
852
|
+
items = await client.get_items(workspace_id, item_type)
|
|
853
|
+
|
|
854
|
+
if not items:
|
|
855
|
+
return f"No items found in workspace '{workspace}'."
|
|
856
|
+
|
|
857
|
+
markdown = f"# Items in workspace '{workspace}'\n\n"
|
|
858
|
+
if item_type:
|
|
859
|
+
markdown += f"Filtered by type: **{item_type}**\n\n"
|
|
860
|
+
|
|
861
|
+
markdown += "| ID | Display Name | Type | Description |\n"
|
|
862
|
+
markdown += "|-----|--------------|------|-------------|\n"
|
|
863
|
+
|
|
864
|
+
for item in items:
|
|
865
|
+
description = item.get("description", "N/A")
|
|
866
|
+
markdown += f"| {item['id']} | {item['displayName']} | {item['type']} | {description} |\n"
|
|
867
|
+
|
|
868
|
+
return markdown
|
|
869
|
+
|
|
870
|
+
except Exception as e:
|
|
871
|
+
return f"Error listing items: {str(e)}"
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
@mcp.tool()
|
|
875
|
+
async def get_item(workspace: str, item_id: str) -> str:
|
|
876
|
+
"""Get details of a specific Fabric item (READ-ONLY).
|
|
877
|
+
|
|
878
|
+
Args:
|
|
879
|
+
workspace: Name or ID of the workspace
|
|
880
|
+
item_id: ID of the item to retrieve
|
|
881
|
+
"""
|
|
882
|
+
try:
|
|
883
|
+
credential = DefaultAzureCredential()
|
|
884
|
+
client = FabricApiClient(credential)
|
|
885
|
+
|
|
886
|
+
# Convert name to ID
|
|
887
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
888
|
+
|
|
889
|
+
# Get item details
|
|
890
|
+
item = await client.get_item(workspace_id, item_id)
|
|
891
|
+
|
|
892
|
+
if not item:
|
|
893
|
+
return f"Item '{item_id}' not found in workspace '{workspace}'."
|
|
894
|
+
|
|
895
|
+
markdown = f"# Item Details\n\n"
|
|
896
|
+
markdown += f"**ID:** {item['id']}\n\n"
|
|
897
|
+
markdown += f"**Display Name:** {item['displayName']}\n\n"
|
|
898
|
+
markdown += f"**Type:** {item['type']}\n\n"
|
|
899
|
+
markdown += f"**Workspace ID:** {item['workspaceId']}\n\n"
|
|
900
|
+
|
|
901
|
+
if item.get("description"):
|
|
902
|
+
markdown += f"**Description:** {item['description']}\n\n"
|
|
903
|
+
|
|
904
|
+
return markdown
|
|
905
|
+
|
|
906
|
+
except Exception as e:
|
|
907
|
+
return f"Error getting item details: {str(e)}"
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
@mcp.tool()
|
|
911
|
+
async def get_workspace(workspace: str) -> str:
|
|
912
|
+
"""Get details of a specific Fabric workspace (READ-ONLY).
|
|
913
|
+
|
|
914
|
+
Args:
|
|
915
|
+
workspace: Name or ID of the workspace
|
|
916
|
+
"""
|
|
917
|
+
try:
|
|
918
|
+
credential = DefaultAzureCredential()
|
|
919
|
+
client = FabricApiClient(credential)
|
|
920
|
+
|
|
921
|
+
# Convert name to ID
|
|
922
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
923
|
+
|
|
924
|
+
# Get workspace details
|
|
925
|
+
workspace_details = await client._make_request(f"workspaces/{workspace_id}")
|
|
926
|
+
|
|
927
|
+
if not workspace_details:
|
|
928
|
+
return f"Workspace '{workspace}' not found."
|
|
929
|
+
|
|
930
|
+
markdown = f"# Workspace Details\n\n"
|
|
931
|
+
markdown += f"**ID:** {workspace_details['id']}\n\n"
|
|
932
|
+
markdown += f"**Display Name:** {workspace_details['displayName']}\n\n"
|
|
933
|
+
markdown += f"**Type:** {workspace_details.get('type', 'N/A')}\n\n"
|
|
934
|
+
|
|
935
|
+
if workspace_details.get("description"):
|
|
936
|
+
markdown += f"**Description:** {workspace_details['description']}\n\n"
|
|
937
|
+
|
|
938
|
+
if workspace_details.get("capacityId"):
|
|
939
|
+
markdown += f"**Capacity ID:** {workspace_details['capacityId']}\n\n"
|
|
940
|
+
|
|
941
|
+
return markdown
|
|
942
|
+
|
|
943
|
+
except Exception as e:
|
|
944
|
+
return f"Error getting workspace details: {str(e)}"
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
@mcp.tool()
|
|
948
|
+
async def list_capacities() -> str:
|
|
949
|
+
"""List all Fabric capacities the user has access to (READ-ONLY)."""
|
|
950
|
+
try:
|
|
951
|
+
credential = DefaultAzureCredential()
|
|
952
|
+
client = FabricApiClient(credential)
|
|
953
|
+
|
|
954
|
+
# Get capacities
|
|
955
|
+
capacities = await client.get_capacities()
|
|
956
|
+
|
|
957
|
+
if not capacities:
|
|
958
|
+
return "No capacities found."
|
|
959
|
+
|
|
960
|
+
markdown = "# Fabric Capacities\n\n"
|
|
961
|
+
markdown += "| ID | Display Name | SKU | Region | State |\n"
|
|
962
|
+
markdown += "|-----|--------------|-----|--------|-------|\n"
|
|
963
|
+
|
|
964
|
+
for capacity in capacities:
|
|
965
|
+
sku = capacity.get("sku", "N/A")
|
|
966
|
+
region = capacity.get("region", "N/A")
|
|
967
|
+
state = capacity.get("state", "N/A")
|
|
968
|
+
markdown += f"| {capacity['id']} | {capacity['displayName']} | {sku} | {region} | {state} |\n"
|
|
969
|
+
|
|
970
|
+
return markdown
|
|
971
|
+
|
|
972
|
+
except Exception as e:
|
|
973
|
+
return f"Error listing capacities: {str(e)}"
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
@mcp.tool()
|
|
977
|
+
async def list_workspaces_with_identity() -> str:
|
|
978
|
+
"""List workspaces that have workspace identities configured (READ-ONLY).
|
|
979
|
+
|
|
980
|
+
This identifies which workspaces have workspace identities for secure authentication.
|
|
981
|
+
"""
|
|
982
|
+
try:
|
|
983
|
+
credential = DefaultAzureCredential()
|
|
984
|
+
client = FabricApiClient(credential)
|
|
985
|
+
|
|
986
|
+
# Get all workspaces
|
|
987
|
+
workspaces = await client.get_workspaces()
|
|
988
|
+
|
|
989
|
+
if not workspaces:
|
|
990
|
+
return "No workspaces found."
|
|
991
|
+
|
|
992
|
+
workspaces_with_identity = []
|
|
993
|
+
|
|
994
|
+
# Check each workspace for workspace identity
|
|
995
|
+
for workspace in workspaces:
|
|
996
|
+
try:
|
|
997
|
+
# Try to get workspace details which might include identity info
|
|
998
|
+
workspace_details = await client._make_request(
|
|
999
|
+
f"workspaces/{workspace['id']}"
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
# Check if workspace has identity-related properties
|
|
1003
|
+
# Note: The exact property name may vary based on API response
|
|
1004
|
+
has_identity = False
|
|
1005
|
+
identity_info = {}
|
|
1006
|
+
|
|
1007
|
+
# Check for various possible identity indicators
|
|
1008
|
+
if workspace_details:
|
|
1009
|
+
# Look for identity-related fields
|
|
1010
|
+
if (
|
|
1011
|
+
workspace_details.get("hasWorkspaceIdentity")
|
|
1012
|
+
or workspace_details.get("workspaceIdentity")
|
|
1013
|
+
or workspace_details.get("identityId")
|
|
1014
|
+
):
|
|
1015
|
+
has_identity = True
|
|
1016
|
+
identity_info = {
|
|
1017
|
+
"hasIdentity": workspace_details.get(
|
|
1018
|
+
"hasWorkspaceIdentity", True
|
|
1019
|
+
),
|
|
1020
|
+
"identityId": workspace_details.get("identityId", "N/A"),
|
|
1021
|
+
"identityState": workspace_details.get(
|
|
1022
|
+
"identityState", "N/A"
|
|
1023
|
+
),
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if has_identity:
|
|
1027
|
+
workspaces_with_identity.append(
|
|
1028
|
+
{"workspace": workspace, "identity": identity_info}
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
except Exception as e:
|
|
1032
|
+
# Skip workspaces we can't access
|
|
1033
|
+
logger.debug(
|
|
1034
|
+
f"Could not check identity for workspace {workspace['displayName']}: {str(e)}"
|
|
1035
|
+
)
|
|
1036
|
+
continue
|
|
1037
|
+
|
|
1038
|
+
if not workspaces_with_identity:
|
|
1039
|
+
return "No workspaces with workspace identities found (or user lacks permission to view identity details)."
|
|
1040
|
+
|
|
1041
|
+
markdown = "# Workspaces with Workspace Identities\n\n"
|
|
1042
|
+
markdown += "| Workspace ID | Workspace Name | Capacity | Identity State |\n"
|
|
1043
|
+
markdown += "|--------------|----------------|----------|----------------|\n"
|
|
1044
|
+
|
|
1045
|
+
for item in workspaces_with_identity:
|
|
1046
|
+
workspace = item["workspace"]
|
|
1047
|
+
identity = item["identity"]
|
|
1048
|
+
capacity = workspace.get("capacityId", "N/A")
|
|
1049
|
+
identity_state = identity.get("identityState", "Active")
|
|
1050
|
+
|
|
1051
|
+
markdown += f"| {workspace['id']} | {workspace['displayName']} | {capacity} | {identity_state} |\n"
|
|
1052
|
+
|
|
1053
|
+
markdown += (
|
|
1054
|
+
f"\n**Total workspaces with identities:** {len(workspaces_with_identity)}\n"
|
|
1055
|
+
)
|
|
1056
|
+
markdown += "\n*Note: This tool identifies workspaces with workspace identities based on available API data. Some identity details may require admin permissions.*\n"
|
|
1057
|
+
|
|
1058
|
+
return markdown
|
|
1059
|
+
|
|
1060
|
+
except Exception as e:
|
|
1061
|
+
return f"Error listing workspaces with identities: {str(e)}"
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
@mcp.tool()
|
|
1065
|
+
async def get_workspace_identity(workspace: str) -> str:
|
|
1066
|
+
"""Get workspace identity details for a specific workspace (READ-ONLY).
|
|
1067
|
+
|
|
1068
|
+
Args:
|
|
1069
|
+
workspace: Name or ID of the workspace
|
|
1070
|
+
"""
|
|
1071
|
+
try:
|
|
1072
|
+
credential = DefaultAzureCredential()
|
|
1073
|
+
client = FabricApiClient(credential)
|
|
1074
|
+
|
|
1075
|
+
# Convert name to ID
|
|
1076
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
1077
|
+
|
|
1078
|
+
# Get workspace details
|
|
1079
|
+
workspace_details = await client._make_request(f"workspaces/{workspace_id}")
|
|
1080
|
+
|
|
1081
|
+
if not workspace_details:
|
|
1082
|
+
return f"Workspace '{workspace}' not found."
|
|
1083
|
+
|
|
1084
|
+
markdown = f"# Workspace Identity Details for '{workspace}'\n\n"
|
|
1085
|
+
markdown += f"**Workspace ID:** {workspace_details['id']}\n\n"
|
|
1086
|
+
markdown += f"**Workspace Name:** {workspace_details['displayName']}\n\n"
|
|
1087
|
+
|
|
1088
|
+
# Check for workspace identity information
|
|
1089
|
+
has_identity = (
|
|
1090
|
+
workspace_details.get("hasWorkspaceIdentity")
|
|
1091
|
+
or workspace_details.get("workspaceIdentity")
|
|
1092
|
+
or workspace_details.get("identityId")
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
if has_identity:
|
|
1096
|
+
markdown += "## ✅ Workspace Identity Configured\n\n"
|
|
1097
|
+
|
|
1098
|
+
if workspace_details.get("identityId"):
|
|
1099
|
+
markdown += f"**Identity ID:** {workspace_details['identityId']}\n\n"
|
|
1100
|
+
|
|
1101
|
+
if workspace_details.get("identityState"):
|
|
1102
|
+
markdown += (
|
|
1103
|
+
f"**Identity State:** {workspace_details['identityState']}\n\n"
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
if workspace_details.get("workspaceIdentity"):
|
|
1107
|
+
identity = workspace_details["workspaceIdentity"]
|
|
1108
|
+
if isinstance(identity, dict):
|
|
1109
|
+
markdown += "**Identity Details:**\n\n"
|
|
1110
|
+
for key, value in identity.items():
|
|
1111
|
+
markdown += f"- **{key}:** {value}\n"
|
|
1112
|
+
markdown += "\n"
|
|
1113
|
+
else:
|
|
1114
|
+
markdown += "## ❌ No Workspace Identity Configured\n\n"
|
|
1115
|
+
markdown += (
|
|
1116
|
+
"This workspace does not have a workspace identity configured.\n\n"
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
# Add information about workspace identity benefits
|
|
1120
|
+
markdown += "## About Workspace Identity\n\n"
|
|
1121
|
+
markdown += "Workspace identity provides:\n"
|
|
1122
|
+
markdown += "- Secure authentication without managing credentials\n"
|
|
1123
|
+
markdown += "- Trusted workspace access to firewall-enabled storage accounts\n"
|
|
1124
|
+
markdown += "- Integration with Microsoft Entra ID\n"
|
|
1125
|
+
markdown += (
|
|
1126
|
+
"- Support for OneLake shortcuts, data pipelines, and semantic models\n"
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
return markdown
|
|
1130
|
+
|
|
1131
|
+
except Exception as e:
|
|
1132
|
+
return f"Error getting workspace identity details: {str(e)}"
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
@mcp.tool()
|
|
1136
|
+
async def list_shortcuts(
|
|
1137
|
+
workspace: str, item_name: str, parent_path: str = None
|
|
1138
|
+
) -> str:
|
|
1139
|
+
"""List all OneLake shortcuts in a specific Fabric item (READ-ONLY).
|
|
1140
|
+
|
|
1141
|
+
Args:
|
|
1142
|
+
workspace: Name or ID of the workspace
|
|
1143
|
+
item_name: Name or ID of the item (Lakehouse, KQL Database, etc.)
|
|
1144
|
+
parent_path: Optional parent path to filter shortcuts (e.g., 'Files', 'Tables')
|
|
1145
|
+
"""
|
|
1146
|
+
try:
|
|
1147
|
+
credential = DefaultAzureCredential()
|
|
1148
|
+
client = FabricApiClient(credential)
|
|
1149
|
+
|
|
1150
|
+
# Convert names to IDs
|
|
1151
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
1152
|
+
|
|
1153
|
+
# Get all items to find the specified item
|
|
1154
|
+
items = await client.paginated_request(f"workspaces/{workspace_id}/items")
|
|
1155
|
+
|
|
1156
|
+
# Find the target item
|
|
1157
|
+
target_item = None
|
|
1158
|
+
for item in items:
|
|
1159
|
+
if (
|
|
1160
|
+
item["displayName"].lower() == item_name.lower()
|
|
1161
|
+
or item["id"] == item_name
|
|
1162
|
+
):
|
|
1163
|
+
target_item = item
|
|
1164
|
+
break
|
|
1165
|
+
|
|
1166
|
+
if not target_item:
|
|
1167
|
+
return f"Item '{item_name}' not found in workspace '{workspace}'."
|
|
1168
|
+
|
|
1169
|
+
# Build shortcuts endpoint
|
|
1170
|
+
endpoint = f"workspaces/{workspace_id}/items/{target_item['id']}/shortcuts"
|
|
1171
|
+
params = {}
|
|
1172
|
+
if parent_path:
|
|
1173
|
+
params["parentPath"] = parent_path
|
|
1174
|
+
|
|
1175
|
+
# Get shortcuts
|
|
1176
|
+
shortcuts_response = await client._make_request(endpoint, params)
|
|
1177
|
+
|
|
1178
|
+
if not shortcuts_response or not shortcuts_response.get("value"):
|
|
1179
|
+
return f"No shortcuts found in item '{item_name}'."
|
|
1180
|
+
|
|
1181
|
+
shortcuts = shortcuts_response["value"]
|
|
1182
|
+
|
|
1183
|
+
markdown = f"# OneLake Shortcuts in '{item_name}'\n\n"
|
|
1184
|
+
markdown += f"**Workspace:** {workspace}\n"
|
|
1185
|
+
markdown += f"**Item Type:** {target_item['type']}\n"
|
|
1186
|
+
if parent_path:
|
|
1187
|
+
markdown += f"**Parent Path:** {parent_path}\n"
|
|
1188
|
+
markdown += f"**Total Shortcuts:** {len(shortcuts)}\n\n"
|
|
1189
|
+
|
|
1190
|
+
markdown += "| Name | Path | Target Type | Target Details |\n"
|
|
1191
|
+
markdown += "|------|------|-------------|----------------|\n"
|
|
1192
|
+
|
|
1193
|
+
for shortcut in shortcuts:
|
|
1194
|
+
name = shortcut["name"]
|
|
1195
|
+
path = shortcut["path"]
|
|
1196
|
+
target = shortcut["target"]
|
|
1197
|
+
target_type = target["type"]
|
|
1198
|
+
|
|
1199
|
+
# Build target details based on type
|
|
1200
|
+
target_details = "N/A"
|
|
1201
|
+
if target_type == "OneLake" and target.get("oneLake"):
|
|
1202
|
+
onelake = target["oneLake"]
|
|
1203
|
+
target_details = f"Workspace: {onelake.get('workspaceId', 'N/A')}, Item: {onelake.get('itemId', 'N/A')}"
|
|
1204
|
+
elif target_type == "AmazonS3" and target.get("amazonS3"):
|
|
1205
|
+
s3 = target["amazonS3"]
|
|
1206
|
+
target_details = f"Location: {s3.get('location', 'N/A')}"
|
|
1207
|
+
elif target_type == "AdlsGen2" and target.get("adlsGen2"):
|
|
1208
|
+
adls = target["adlsGen2"]
|
|
1209
|
+
target_details = f"Location: {adls.get('location', 'N/A')}"
|
|
1210
|
+
elif target_type == "GoogleCloudStorage" and target.get(
|
|
1211
|
+
"googleCloudStorage"
|
|
1212
|
+
):
|
|
1213
|
+
gcs = target["googleCloudStorage"]
|
|
1214
|
+
target_details = f"Location: {gcs.get('location', 'N/A')}"
|
|
1215
|
+
elif target_type == "AzureBlobStorage" and target.get("azureBlobStorage"):
|
|
1216
|
+
blob = target["azureBlobStorage"]
|
|
1217
|
+
target_details = f"Location: {blob.get('location', 'N/A')}"
|
|
1218
|
+
|
|
1219
|
+
markdown += f"| {name} | {path} | {target_type} | {target_details} |\n"
|
|
1220
|
+
|
|
1221
|
+
# Add information about shortcut types
|
|
1222
|
+
markdown += "\n## About OneLake Shortcuts\n\n"
|
|
1223
|
+
markdown += "OneLake shortcuts provide references to data stored in:\n"
|
|
1224
|
+
markdown += "- **OneLake**: Other Fabric items (Lakehouses, KQL Databases)\n"
|
|
1225
|
+
markdown += "- **External Storage**: Amazon S3, Azure Data Lake Gen2, Google Cloud Storage, etc.\n"
|
|
1226
|
+
markdown += "- **Shortcuts appear as folders** and can be accessed by Spark, SQL, and other services\n"
|
|
1227
|
+
|
|
1228
|
+
return markdown
|
|
1229
|
+
|
|
1230
|
+
except Exception as e:
|
|
1231
|
+
return f"Error listing shortcuts: {str(e)}"
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
@mcp.tool()
|
|
1235
|
+
async def get_shortcut(
|
|
1236
|
+
workspace: str, item_name: str, shortcut_path: str, shortcut_name: str
|
|
1237
|
+
) -> str:
|
|
1238
|
+
"""Get detailed information about a specific OneLake shortcut (READ-ONLY).
|
|
1239
|
+
|
|
1240
|
+
Args:
|
|
1241
|
+
workspace: Name or ID of the workspace
|
|
1242
|
+
item_name: Name or ID of the item containing the shortcut
|
|
1243
|
+
shortcut_path: Path where the shortcut is located (e.g., 'Files', 'Tables/subfolder')
|
|
1244
|
+
shortcut_name: Name of the shortcut
|
|
1245
|
+
"""
|
|
1246
|
+
try:
|
|
1247
|
+
credential = DefaultAzureCredential()
|
|
1248
|
+
client = FabricApiClient(credential)
|
|
1249
|
+
|
|
1250
|
+
# Convert names to IDs
|
|
1251
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
1252
|
+
|
|
1253
|
+
# Get all items to find the specified item
|
|
1254
|
+
items = await client.paginated_request(f"workspaces/{workspace_id}/items")
|
|
1255
|
+
|
|
1256
|
+
# Find the target item
|
|
1257
|
+
target_item = None
|
|
1258
|
+
for item in items:
|
|
1259
|
+
if (
|
|
1260
|
+
item["displayName"].lower() == item_name.lower()
|
|
1261
|
+
or item["id"] == item_name
|
|
1262
|
+
):
|
|
1263
|
+
target_item = item
|
|
1264
|
+
break
|
|
1265
|
+
|
|
1266
|
+
if not target_item:
|
|
1267
|
+
return f"Item '{item_name}' not found in workspace '{workspace}'."
|
|
1268
|
+
|
|
1269
|
+
# Get shortcut details
|
|
1270
|
+
endpoint = f"workspaces/{workspace_id}/items/{target_item['id']}/shortcuts/{shortcut_path}/{shortcut_name}"
|
|
1271
|
+
shortcut = await client._make_request(endpoint)
|
|
1272
|
+
|
|
1273
|
+
if not shortcut:
|
|
1274
|
+
return f"Shortcut '{shortcut_name}' not found at path '{shortcut_path}' in item '{item_name}'."
|
|
1275
|
+
|
|
1276
|
+
markdown = f"# OneLake Shortcut Details\n\n"
|
|
1277
|
+
markdown += f"**Shortcut Name:** {shortcut['name']}\n\n"
|
|
1278
|
+
markdown += f"**Path:** {shortcut['path']}\n\n"
|
|
1279
|
+
markdown += f"**Workspace:** {workspace}\n\n"
|
|
1280
|
+
markdown += f"**Item:** {item_name} ({target_item['type']})\n\n"
|
|
1281
|
+
|
|
1282
|
+
# Target information
|
|
1283
|
+
target = shortcut["target"]
|
|
1284
|
+
target_type = target["type"]
|
|
1285
|
+
markdown += f"**Target Type:** {target_type}\n\n"
|
|
1286
|
+
|
|
1287
|
+
markdown += "## Target Configuration\n\n"
|
|
1288
|
+
|
|
1289
|
+
if target_type == "OneLake" and target.get("oneLake"):
|
|
1290
|
+
onelake = target["oneLake"]
|
|
1291
|
+
markdown += (
|
|
1292
|
+
f"**Target Workspace ID:** {onelake.get('workspaceId', 'N/A')}\n\n"
|
|
1293
|
+
)
|
|
1294
|
+
markdown += f"**Target Item ID:** {onelake.get('itemId', 'N/A')}\n\n"
|
|
1295
|
+
markdown += f"**Target Path:** {onelake.get('path', 'N/A')}\n\n"
|
|
1296
|
+
if onelake.get("connectionId"):
|
|
1297
|
+
markdown += f"**Connection ID:** {onelake['connectionId']}\n\n"
|
|
1298
|
+
|
|
1299
|
+
elif target_type == "AmazonS3" and target.get("amazonS3"):
|
|
1300
|
+
s3 = target["amazonS3"]
|
|
1301
|
+
markdown += f"**S3 Location:** {s3.get('location', 'N/A')}\n\n"
|
|
1302
|
+
markdown += f"**Subpath:** {s3.get('subpath', 'N/A')}\n\n"
|
|
1303
|
+
markdown += f"**Connection ID:** {s3.get('connectionId', 'N/A')}\n\n"
|
|
1304
|
+
|
|
1305
|
+
elif target_type == "AdlsGen2" and target.get("adlsGen2"):
|
|
1306
|
+
adls = target["adlsGen2"]
|
|
1307
|
+
markdown += f"**ADLS Location:** {adls.get('location', 'N/A')}\n\n"
|
|
1308
|
+
markdown += f"**Subpath:** {adls.get('subpath', 'N/A')}\n\n"
|
|
1309
|
+
markdown += f"**Connection ID:** {adls.get('connectionId', 'N/A')}\n\n"
|
|
1310
|
+
|
|
1311
|
+
elif target_type == "GoogleCloudStorage" and target.get("googleCloudStorage"):
|
|
1312
|
+
gcs = target["googleCloudStorage"]
|
|
1313
|
+
markdown += f"**GCS Location:** {gcs.get('location', 'N/A')}\n\n"
|
|
1314
|
+
markdown += f"**Subpath:** {gcs.get('subpath', 'N/A')}\n\n"
|
|
1315
|
+
markdown += f"**Connection ID:** {gcs.get('connectionId', 'N/A')}\n\n"
|
|
1316
|
+
|
|
1317
|
+
elif target_type == "AzureBlobStorage" and target.get("azureBlobStorage"):
|
|
1318
|
+
blob = target["azureBlobStorage"]
|
|
1319
|
+
markdown += f"**Blob Storage Location:** {blob.get('location', 'N/A')}\n\n"
|
|
1320
|
+
markdown += f"**Subpath:** {blob.get('subpath', 'N/A')}\n\n"
|
|
1321
|
+
markdown += f"**Connection ID:** {blob.get('connectionId', 'N/A')}\n\n"
|
|
1322
|
+
|
|
1323
|
+
elif target_type == "Dataverse" and target.get("dataverse"):
|
|
1324
|
+
dv = target["dataverse"]
|
|
1325
|
+
markdown += (
|
|
1326
|
+
f"**Environment Domain:** {dv.get('environmentDomain', 'N/A')}\n\n"
|
|
1327
|
+
)
|
|
1328
|
+
markdown += f"**Table Name:** {dv.get('tableName', 'N/A')}\n\n"
|
|
1329
|
+
markdown += f"**Delta Lake Folder:** {dv.get('deltaLakeFolder', 'N/A')}\n\n"
|
|
1330
|
+
markdown += f"**Connection ID:** {dv.get('connectionId', 'N/A')}\n\n"
|
|
1331
|
+
|
|
1332
|
+
# Transform information (if any)
|
|
1333
|
+
if shortcut.get("transform"):
|
|
1334
|
+
transform = shortcut["transform"]
|
|
1335
|
+
markdown += "## Transform Configuration\n\n"
|
|
1336
|
+
markdown += f"**Transform Type:** {transform.get('type', 'N/A')}\n\n"
|
|
1337
|
+
|
|
1338
|
+
if transform.get("properties"):
|
|
1339
|
+
props = transform["properties"]
|
|
1340
|
+
markdown += "**Transform Properties:**\n\n"
|
|
1341
|
+
for key, value in props.items():
|
|
1342
|
+
markdown += f"- **{key}:** {value}\n"
|
|
1343
|
+
markdown += "\n"
|
|
1344
|
+
|
|
1345
|
+
# Access information
|
|
1346
|
+
markdown += "## Access Information\n\n"
|
|
1347
|
+
markdown += "This shortcut can be accessed through:\n"
|
|
1348
|
+
markdown += "- **Apache Spark**: Use relative paths or SQL syntax\n"
|
|
1349
|
+
markdown += (
|
|
1350
|
+
"- **SQL Analytics Endpoint**: Query through T-SQL (if in Tables folder)\n"
|
|
1351
|
+
)
|
|
1352
|
+
markdown += "- **OneLake API**: Direct file access via REST API\n"
|
|
1353
|
+
markdown += "- **External Tools**: Any tool supporting ADLS Gen2 APIs\n"
|
|
1354
|
+
|
|
1355
|
+
return markdown
|
|
1356
|
+
|
|
1357
|
+
except Exception as e:
|
|
1358
|
+
return f"Error getting shortcut details: {str(e)}"
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
@mcp.tool()
|
|
1362
|
+
async def list_workspace_shortcuts(workspace: str) -> str:
|
|
1363
|
+
"""List all OneLake shortcuts across all items in a workspace (READ-ONLY).
|
|
1364
|
+
|
|
1365
|
+
This aggregates shortcuts from all Lakehouses and KQL Databases in the workspace.
|
|
1366
|
+
|
|
1367
|
+
Args:
|
|
1368
|
+
workspace: Name or ID of the workspace
|
|
1369
|
+
"""
|
|
1370
|
+
try:
|
|
1371
|
+
credential = DefaultAzureCredential()
|
|
1372
|
+
client = FabricApiClient(credential)
|
|
1373
|
+
|
|
1374
|
+
# Convert name to ID
|
|
1375
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
1376
|
+
|
|
1377
|
+
# Get all items in workspace that can contain shortcuts
|
|
1378
|
+
items = await client.paginated_request(f"workspaces/{workspace_id}/items")
|
|
1379
|
+
|
|
1380
|
+
# Filter to items that can contain shortcuts (Lakehouse, KQLDatabase)
|
|
1381
|
+
shortcut_items = [
|
|
1382
|
+
item for item in items if item["type"] in ["Lakehouse", "KQLDatabase"]
|
|
1383
|
+
]
|
|
1384
|
+
|
|
1385
|
+
if not shortcut_items:
|
|
1386
|
+
return (
|
|
1387
|
+
f"No items that can contain shortcuts found in workspace '{workspace}'."
|
|
1388
|
+
)
|
|
1389
|
+
|
|
1390
|
+
all_shortcuts = []
|
|
1391
|
+
total_shortcuts = 0
|
|
1392
|
+
|
|
1393
|
+
# Get shortcuts from each item
|
|
1394
|
+
for item in shortcut_items:
|
|
1395
|
+
try:
|
|
1396
|
+
endpoint = f"workspaces/{workspace_id}/items/{item['id']}/shortcuts"
|
|
1397
|
+
shortcuts_response = await client._make_request(endpoint)
|
|
1398
|
+
|
|
1399
|
+
if shortcuts_response and shortcuts_response.get("value"):
|
|
1400
|
+
item_shortcuts = shortcuts_response["value"]
|
|
1401
|
+
total_shortcuts += len(item_shortcuts)
|
|
1402
|
+
|
|
1403
|
+
for shortcut in item_shortcuts:
|
|
1404
|
+
shortcut["_item_name"] = item["displayName"]
|
|
1405
|
+
shortcut["_item_type"] = item["type"]
|
|
1406
|
+
shortcut["_item_id"] = item["id"]
|
|
1407
|
+
|
|
1408
|
+
all_shortcuts.extend(item_shortcuts)
|
|
1409
|
+
|
|
1410
|
+
except Exception as e:
|
|
1411
|
+
# Skip items we can't access
|
|
1412
|
+
logger.debug(
|
|
1413
|
+
f"Could not get shortcuts for item {item['displayName']}: {str(e)}"
|
|
1414
|
+
)
|
|
1415
|
+
continue
|
|
1416
|
+
|
|
1417
|
+
if not all_shortcuts:
|
|
1418
|
+
return f"No shortcuts found in any items in workspace '{workspace}'."
|
|
1419
|
+
|
|
1420
|
+
markdown = f"# OneLake Shortcuts in Workspace '{workspace}'\n\n"
|
|
1421
|
+
markdown += f"**Total Items with Shortcuts:** {len([item for item in shortcut_items if any(s.get('_item_id') == item['id'] for s in all_shortcuts)])}\n"
|
|
1422
|
+
markdown += f"**Total Shortcuts:** {total_shortcuts}\n\n"
|
|
1423
|
+
|
|
1424
|
+
# Group shortcuts by item
|
|
1425
|
+
from collections import defaultdict
|
|
1426
|
+
|
|
1427
|
+
shortcuts_by_item = defaultdict(list)
|
|
1428
|
+
for shortcut in all_shortcuts:
|
|
1429
|
+
item_name = shortcut.get("_item_name", "Unknown")
|
|
1430
|
+
shortcuts_by_item[item_name].append(shortcut)
|
|
1431
|
+
|
|
1432
|
+
for item_name, shortcuts in shortcuts_by_item.items():
|
|
1433
|
+
item_type = shortcuts[0].get("_item_type", "Unknown")
|
|
1434
|
+
markdown += f"## {item_name} ({item_type})\n\n"
|
|
1435
|
+
markdown += f"**Shortcuts in this item:** {len(shortcuts)}\n\n"
|
|
1436
|
+
|
|
1437
|
+
markdown += "| Name | Path | Target Type | Target Details |\n"
|
|
1438
|
+
markdown += "|------|------|-------------|----------------|\n"
|
|
1439
|
+
|
|
1440
|
+
for shortcut in shortcuts:
|
|
1441
|
+
name = shortcut["name"]
|
|
1442
|
+
path = shortcut["path"]
|
|
1443
|
+
target = shortcut["target"]
|
|
1444
|
+
target_type = target["type"]
|
|
1445
|
+
|
|
1446
|
+
# Build target details based on type
|
|
1447
|
+
target_details = "N/A"
|
|
1448
|
+
if target_type == "OneLake" and target.get("oneLake"):
|
|
1449
|
+
onelake = target["oneLake"]
|
|
1450
|
+
target_details = f"Item: {onelake.get('itemId', 'N/A')[:8]}..."
|
|
1451
|
+
elif target_type in [
|
|
1452
|
+
"AmazonS3",
|
|
1453
|
+
"AdlsGen2",
|
|
1454
|
+
"GoogleCloudStorage",
|
|
1455
|
+
"AzureBlobStorage",
|
|
1456
|
+
]:
|
|
1457
|
+
# Get location from the appropriate target type
|
|
1458
|
+
target_obj = target.get(target_type.lower()) or target.get(
|
|
1459
|
+
target_type
|
|
1460
|
+
)
|
|
1461
|
+
if target_obj and target_obj.get("location"):
|
|
1462
|
+
target_details = (
|
|
1463
|
+
target_obj["location"][:50] + "..."
|
|
1464
|
+
if len(target_obj["location"]) > 50
|
|
1465
|
+
else target_obj["location"]
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1468
|
+
markdown += f"| {name} | {path} | {target_type} | {target_details} |\n"
|
|
1469
|
+
|
|
1470
|
+
markdown += "\n"
|
|
1471
|
+
|
|
1472
|
+
# Summary by target type
|
|
1473
|
+
target_types = defaultdict(int)
|
|
1474
|
+
for shortcut in all_shortcuts:
|
|
1475
|
+
target_types[shortcut["target"]["type"]] += 1
|
|
1476
|
+
|
|
1477
|
+
markdown += "## Summary by Target Type\n\n"
|
|
1478
|
+
for target_type, count in sorted(target_types.items()):
|
|
1479
|
+
markdown += f"- **{target_type}**: {count} shortcuts\n"
|
|
1480
|
+
|
|
1481
|
+
markdown += "\n*Note: This aggregates shortcuts from all Lakehouses and KQL Databases in the workspace.*\n"
|
|
1482
|
+
|
|
1483
|
+
return markdown
|
|
1484
|
+
|
|
1485
|
+
except Exception as e:
|
|
1486
|
+
return f"Error listing workspace shortcuts: {str(e)}"
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
@mcp.tool()
|
|
1490
|
+
async def list_job_instances(
|
|
1491
|
+
workspace: str, item_name: str = None, status: str = None
|
|
1492
|
+
) -> str:
|
|
1493
|
+
"""List all job instances for items in a workspace (READ-ONLY).
|
|
1494
|
+
|
|
1495
|
+
Shows what's running, queued, completed, or failed - perfect for monitoring!
|
|
1496
|
+
|
|
1497
|
+
Args:
|
|
1498
|
+
workspace: Name or ID of the workspace
|
|
1499
|
+
item_name: Optional item name to filter jobs for specific item
|
|
1500
|
+
status: Optional status filter (NotStarted, InProgress, Completed, Failed, Cancelled)
|
|
1501
|
+
"""
|
|
1502
|
+
try:
|
|
1503
|
+
credential = DefaultAzureCredential()
|
|
1504
|
+
client = FabricApiClient(credential)
|
|
1505
|
+
|
|
1506
|
+
# Convert names to IDs
|
|
1507
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
1508
|
+
|
|
1509
|
+
# Resolve item_name to item_id if provided
|
|
1510
|
+
item_id = None
|
|
1511
|
+
if item_name:
|
|
1512
|
+
items = await client.get_items(workspace_id)
|
|
1513
|
+
matching_items = [
|
|
1514
|
+
item
|
|
1515
|
+
for item in items
|
|
1516
|
+
if item_name.lower() in item["displayName"].lower()
|
|
1517
|
+
]
|
|
1518
|
+
if not matching_items:
|
|
1519
|
+
return (
|
|
1520
|
+
f"No items matching '{item_name}' found in workspace '{workspace}'."
|
|
1521
|
+
)
|
|
1522
|
+
item_id = matching_items[0]["id"]
|
|
1523
|
+
|
|
1524
|
+
# Get job instances using client method
|
|
1525
|
+
all_jobs = await client.get_job_instances(workspace_id, item_id, status)
|
|
1526
|
+
|
|
1527
|
+
if not all_jobs:
|
|
1528
|
+
return f"No job instances found in workspace '{workspace}'."
|
|
1529
|
+
|
|
1530
|
+
# Sort by start time (most recent first)
|
|
1531
|
+
all_jobs.sort(key=lambda x: x.get("startTimeUtc", ""), reverse=True)
|
|
1532
|
+
|
|
1533
|
+
markdown = f"# Job Instances in '{workspace}'\n\n"
|
|
1534
|
+
if status:
|
|
1535
|
+
markdown += f"**Status Filter**: {status}\n\n"
|
|
1536
|
+
if item_name:
|
|
1537
|
+
markdown += f"**Item Filter**: {item_name}\n\n"
|
|
1538
|
+
|
|
1539
|
+
markdown += "| Item | Type | Job Type | Status | Invoke Type | Start Time | Duration |\n"
|
|
1540
|
+
markdown += "|------|------|----------|--------|-------------|------------|----------|\n"
|
|
1541
|
+
|
|
1542
|
+
for job in all_jobs[:50]: # Limit to 50 most recent
|
|
1543
|
+
start_time = job.get("startTimeUtc", "N/A")
|
|
1544
|
+
end_time = job.get("endTimeUtc", "")
|
|
1545
|
+
duration = (
|
|
1546
|
+
"Running"
|
|
1547
|
+
if job.get("status") == "InProgress"
|
|
1548
|
+
else ("N/A" if not end_time else f"{end_time}")
|
|
1549
|
+
)
|
|
1550
|
+
|
|
1551
|
+
markdown += f"| {job['itemName']} | {job['itemType']} | {job.get('jobType', 'N/A')} | "
|
|
1552
|
+
markdown += (
|
|
1553
|
+
f"{job.get('status', 'N/A')} | {job.get('invokeType', 'N/A')} | "
|
|
1554
|
+
)
|
|
1555
|
+
markdown += f"{start_time} | {duration} |\n"
|
|
1556
|
+
|
|
1557
|
+
if len(all_jobs) > 50:
|
|
1558
|
+
markdown += (
|
|
1559
|
+
f"\n*Showing 50 most recent jobs out of {len(all_jobs)} total.*\n"
|
|
1560
|
+
)
|
|
1561
|
+
|
|
1562
|
+
return markdown
|
|
1563
|
+
|
|
1564
|
+
except Exception as e:
|
|
1565
|
+
return f"Error listing job instances: {str(e)}"
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
@mcp.tool()
|
|
1569
|
+
async def get_job_instance(workspace: str, item_name: str, job_instance_id: str) -> str:
|
|
1570
|
+
"""Get detailed information about a specific job instance (READ-ONLY).
|
|
1571
|
+
|
|
1572
|
+
Args:
|
|
1573
|
+
workspace: Name or ID of the workspace
|
|
1574
|
+
item_name: Name or ID of the item
|
|
1575
|
+
job_instance_id: ID of the job instance
|
|
1576
|
+
"""
|
|
1577
|
+
try:
|
|
1578
|
+
credential = DefaultAzureCredential()
|
|
1579
|
+
client = FabricApiClient(credential)
|
|
1580
|
+
|
|
1581
|
+
# Convert names to IDs
|
|
1582
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
1583
|
+
|
|
1584
|
+
# Get all items to find the specified item
|
|
1585
|
+
items = await client.paginated_request(f"workspaces/{workspace_id}/items")
|
|
1586
|
+
item = None
|
|
1587
|
+
for i in items:
|
|
1588
|
+
if item_name.lower() in i["displayName"].lower() or i["id"] == item_name:
|
|
1589
|
+
item = i
|
|
1590
|
+
break
|
|
1591
|
+
|
|
1592
|
+
if not item:
|
|
1593
|
+
return f"Item '{item_name}' not found in workspace '{workspace}'."
|
|
1594
|
+
|
|
1595
|
+
# Get job instance details
|
|
1596
|
+
job = await client._make_request(
|
|
1597
|
+
f"workspaces/{workspace_id}/items/{item['id']}/jobs/instances/{job_instance_id}"
|
|
1598
|
+
)
|
|
1599
|
+
|
|
1600
|
+
markdown = f"# Job Instance Details\n\n"
|
|
1601
|
+
markdown += f"**Item**: {item['displayName']} ({item['type']})\n"
|
|
1602
|
+
markdown += f"**Workspace**: {workspace}\n\n"
|
|
1603
|
+
|
|
1604
|
+
markdown += "## Job Information\n"
|
|
1605
|
+
markdown += f"- **Job ID**: {job.get('id', 'N/A')}\n"
|
|
1606
|
+
markdown += f"- **Job Type**: {job.get('jobType', 'N/A')}\n"
|
|
1607
|
+
markdown += f"- **Status**: {job.get('status', 'N/A')}\n"
|
|
1608
|
+
markdown += f"- **Invoke Type**: {job.get('invokeType', 'N/A')}\n"
|
|
1609
|
+
|
|
1610
|
+
markdown += "\n## Timing\n"
|
|
1611
|
+
markdown += f"- **Start Time**: {job.get('startTimeUtc', 'N/A')}\n"
|
|
1612
|
+
markdown += f"- **End Time**: {job.get('endTimeUtc', 'N/A')}\n"
|
|
1613
|
+
|
|
1614
|
+
if job.get("failureReason"):
|
|
1615
|
+
markdown += f"\n## Error Details\n"
|
|
1616
|
+
markdown += f"- **Failure Reason**: {job['failureReason']}\n"
|
|
1617
|
+
|
|
1618
|
+
if job.get("rootActivityId"):
|
|
1619
|
+
markdown += f"\n## Technical Details\n"
|
|
1620
|
+
markdown += f"- **Root Activity ID**: {job['rootActivityId']}\n"
|
|
1621
|
+
|
|
1622
|
+
return markdown
|
|
1623
|
+
|
|
1624
|
+
except Exception as e:
|
|
1625
|
+
return f"Error getting job instance: {str(e)}"
|
|
1626
|
+
|
|
1627
|
+
|
|
1628
|
+
@mcp.tool()
|
|
1629
|
+
async def list_item_schedules(workspace: str, item_name: str) -> str:
|
|
1630
|
+
"""List all schedules for a specific item - see what's scheduled to run! (READ-ONLY)
|
|
1631
|
+
|
|
1632
|
+
Args:
|
|
1633
|
+
workspace: Name or ID of the workspace
|
|
1634
|
+
item_name: Name or ID of the item to check schedules for
|
|
1635
|
+
"""
|
|
1636
|
+
try:
|
|
1637
|
+
credential = DefaultAzureCredential()
|
|
1638
|
+
client = FabricApiClient(credential)
|
|
1639
|
+
|
|
1640
|
+
# Convert names to IDs
|
|
1641
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
1642
|
+
|
|
1643
|
+
# Get all items to find the specified item
|
|
1644
|
+
items = await client.paginated_request(f"workspaces/{workspace_id}/items")
|
|
1645
|
+
item = None
|
|
1646
|
+
for i in items:
|
|
1647
|
+
if item_name.lower() in i["displayName"].lower() or i["id"] == item_name:
|
|
1648
|
+
item = i
|
|
1649
|
+
break
|
|
1650
|
+
|
|
1651
|
+
if not item:
|
|
1652
|
+
return f"Item '{item_name}' not found in workspace '{workspace}'."
|
|
1653
|
+
|
|
1654
|
+
# Get schedules for the item
|
|
1655
|
+
schedules = await client.paginated_request(
|
|
1656
|
+
f"workspaces/{workspace_id}/items/{item['id']}/schedules"
|
|
1657
|
+
)
|
|
1658
|
+
|
|
1659
|
+
if not schedules:
|
|
1660
|
+
return f"No schedules found for item '{item['displayName']}'."
|
|
1661
|
+
|
|
1662
|
+
markdown = f"# Schedules for '{item['displayName']}'\n\n"
|
|
1663
|
+
markdown += f"**Item Type**: {item['type']}\n"
|
|
1664
|
+
markdown += f"**Workspace**: {workspace}\n\n"
|
|
1665
|
+
|
|
1666
|
+
markdown += "| Schedule ID | Job Type | Enabled | Frequency | Start Date | End Date | Timezone |\n"
|
|
1667
|
+
markdown += "|-------------|----------|---------|-----------|------------|----------|----------|\n"
|
|
1668
|
+
|
|
1669
|
+
for schedule in schedules:
|
|
1670
|
+
enabled = "✅ Yes" if schedule.get("enabled", False) else "❌ No"
|
|
1671
|
+
frequency = schedule.get("recurrence", {}).get("frequency", "N/A")
|
|
1672
|
+
start_date = schedule.get("recurrence", {}).get("startDate", "N/A")
|
|
1673
|
+
end_date = schedule.get("recurrence", {}).get("endDate", "N/A")
|
|
1674
|
+
timezone = schedule.get("recurrence", {}).get("timeZone", "N/A")
|
|
1675
|
+
|
|
1676
|
+
markdown += (
|
|
1677
|
+
f"| {schedule.get('id', 'N/A')} | {schedule.get('jobType', 'N/A')} | "
|
|
1678
|
+
)
|
|
1679
|
+
markdown += (
|
|
1680
|
+
f"{enabled} | {frequency} | {start_date} | {end_date} | {timezone} |\n"
|
|
1681
|
+
)
|
|
1682
|
+
|
|
1683
|
+
return markdown
|
|
1684
|
+
|
|
1685
|
+
except Exception as e:
|
|
1686
|
+
return f"Error listing item schedules: {str(e)}"
|
|
1687
|
+
|
|
1688
|
+
|
|
1689
|
+
@mcp.tool()
|
|
1690
|
+
async def list_workspace_schedules(workspace: str) -> str:
|
|
1691
|
+
"""List ALL schedules across all items in a workspace - see everything that's scheduled to run! (READ-ONLY)
|
|
1692
|
+
|
|
1693
|
+
This aggregates schedules from all items in the workspace to give you a complete view
|
|
1694
|
+
of what's scheduled to run when.
|
|
1695
|
+
|
|
1696
|
+
Args:
|
|
1697
|
+
workspace: Name or ID of the workspace
|
|
1698
|
+
"""
|
|
1699
|
+
try:
|
|
1700
|
+
credential = DefaultAzureCredential()
|
|
1701
|
+
client = FabricApiClient(credential)
|
|
1702
|
+
|
|
1703
|
+
# Convert names to IDs
|
|
1704
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
1705
|
+
|
|
1706
|
+
# Get all items in workspace
|
|
1707
|
+
items = await client.paginated_request(f"workspaces/{workspace_id}/items")
|
|
1708
|
+
|
|
1709
|
+
if not items:
|
|
1710
|
+
return f"No items found in workspace '{workspace}'."
|
|
1711
|
+
|
|
1712
|
+
all_schedules = []
|
|
1713
|
+
|
|
1714
|
+
# Get schedules for each item
|
|
1715
|
+
for item in items:
|
|
1716
|
+
try:
|
|
1717
|
+
schedules = await client.paginated_request(
|
|
1718
|
+
f"workspaces/{workspace_id}/items/{item['id']}/schedules"
|
|
1719
|
+
)
|
|
1720
|
+
if schedules:
|
|
1721
|
+
for schedule in schedules:
|
|
1722
|
+
schedule["itemName"] = item["displayName"]
|
|
1723
|
+
schedule["itemType"] = item["type"]
|
|
1724
|
+
schedule["itemId"] = item["id"]
|
|
1725
|
+
all_schedules.append(schedule)
|
|
1726
|
+
except Exception:
|
|
1727
|
+
# Skip items that don't support schedules
|
|
1728
|
+
continue
|
|
1729
|
+
|
|
1730
|
+
if not all_schedules:
|
|
1731
|
+
return f"No schedules found in workspace '{workspace}'."
|
|
1732
|
+
|
|
1733
|
+
# Sort by enabled status first, then by item name
|
|
1734
|
+
all_schedules.sort(key=lambda x: (not x.get("enabled", False), x["itemName"]))
|
|
1735
|
+
|
|
1736
|
+
markdown = f"# All Schedules in Workspace '{workspace}'\n\n"
|
|
1737
|
+
markdown += f"**Total Schedules**: {len(all_schedules)}\n\n"
|
|
1738
|
+
|
|
1739
|
+
# Count enabled vs disabled
|
|
1740
|
+
enabled_count = sum(1 for s in all_schedules if s.get("enabled", False))
|
|
1741
|
+
disabled_count = len(all_schedules) - enabled_count
|
|
1742
|
+
markdown += f"- ✅ **Enabled**: {enabled_count}\n"
|
|
1743
|
+
markdown += f"- ❌ **Disabled**: {disabled_count}\n\n"
|
|
1744
|
+
|
|
1745
|
+
markdown += (
|
|
1746
|
+
"| Item | Type | Job Type | Status | Frequency | Next Run | Timezone |\n"
|
|
1747
|
+
)
|
|
1748
|
+
markdown += (
|
|
1749
|
+
"|------|------|----------|--------|-----------|----------|----------|\n"
|
|
1750
|
+
)
|
|
1751
|
+
|
|
1752
|
+
for schedule in all_schedules:
|
|
1753
|
+
status = "✅ Enabled" if schedule.get("enabled", False) else "❌ Disabled"
|
|
1754
|
+
frequency = schedule.get("recurrence", {}).get("frequency", "N/A")
|
|
1755
|
+
next_run = schedule.get("recurrence", {}).get("startDate", "N/A")
|
|
1756
|
+
timezone = schedule.get("recurrence", {}).get("timeZone", "N/A")
|
|
1757
|
+
|
|
1758
|
+
markdown += f"| {schedule['itemName']} | {schedule['itemType']} | "
|
|
1759
|
+
markdown += f"{schedule.get('jobType', 'N/A')} | {status} | "
|
|
1760
|
+
markdown += f"{frequency} | {next_run} | {timezone} |\n"
|
|
1761
|
+
|
|
1762
|
+
return markdown
|
|
1763
|
+
|
|
1764
|
+
except Exception as e:
|
|
1765
|
+
return f"Error listing workspace schedules: {str(e)}"
|
|
1766
|
+
|
|
1767
|
+
|
|
1768
|
+
@mcp.tool()
|
|
1769
|
+
async def list_environments(workspace: str = None) -> str:
|
|
1770
|
+
"""List all Fabric environments for compute and library management (READ-ONLY).
|
|
1771
|
+
|
|
1772
|
+
Args:
|
|
1773
|
+
workspace: Optional workspace name or ID to filter environments
|
|
1774
|
+
"""
|
|
1775
|
+
try:
|
|
1776
|
+
credential = DefaultAzureCredential()
|
|
1777
|
+
client = FabricApiClient(credential)
|
|
1778
|
+
|
|
1779
|
+
if workspace:
|
|
1780
|
+
# Get environments for specific workspace
|
|
1781
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
1782
|
+
environments = await client.paginated_request(
|
|
1783
|
+
f"workspaces/{workspace_id}/items", params={"type": "Environment"}
|
|
1784
|
+
)
|
|
1785
|
+
workspace_filter = f" in workspace '{workspace}'"
|
|
1786
|
+
else:
|
|
1787
|
+
# Get all accessible workspaces and their environments
|
|
1788
|
+
workspaces = await client.paginated_request("workspaces")
|
|
1789
|
+
environments = []
|
|
1790
|
+
|
|
1791
|
+
for ws in workspaces:
|
|
1792
|
+
try:
|
|
1793
|
+
ws_environments = await client.paginated_request(
|
|
1794
|
+
f"workspaces/{ws['id']}/items", params={"type": "Environment"}
|
|
1795
|
+
)
|
|
1796
|
+
for env in ws_environments:
|
|
1797
|
+
env["workspaceName"] = ws["displayName"]
|
|
1798
|
+
environments.append(env)
|
|
1799
|
+
except Exception:
|
|
1800
|
+
# Skip workspaces we can't access
|
|
1801
|
+
continue
|
|
1802
|
+
|
|
1803
|
+
workspace_filter = " across all accessible workspaces"
|
|
1804
|
+
|
|
1805
|
+
if not environments:
|
|
1806
|
+
return f"No environments found{workspace_filter}."
|
|
1807
|
+
|
|
1808
|
+
markdown = f"# Fabric Environments{workspace_filter}\n\n"
|
|
1809
|
+
markdown += f"**Total Environments**: {len(environments)}\n\n"
|
|
1810
|
+
|
|
1811
|
+
markdown += "| Name | Workspace | Description | Created | Modified |\n"
|
|
1812
|
+
markdown += "|------|-----------|-------------|---------|----------|\n"
|
|
1813
|
+
|
|
1814
|
+
for env in environments:
|
|
1815
|
+
workspace_name = env.get("workspaceName", "Current")
|
|
1816
|
+
description = env.get("description", "No description")[:50] + (
|
|
1817
|
+
"..." if len(env.get("description", "")) > 50 else ""
|
|
1818
|
+
)
|
|
1819
|
+
created = env.get("createdDate", "N/A")
|
|
1820
|
+
modified = env.get("lastModifiedDate", "N/A")
|
|
1821
|
+
|
|
1822
|
+
markdown += f"| {env['displayName']} | {workspace_name} | {description} | {created} | {modified} |\n"
|
|
1823
|
+
|
|
1824
|
+
return markdown
|
|
1825
|
+
|
|
1826
|
+
except Exception as e:
|
|
1827
|
+
return f"Error listing environments: {str(e)}"
|
|
1828
|
+
|
|
1829
|
+
|
|
1830
|
+
@mcp.tool()
|
|
1831
|
+
async def get_environment_details(workspace: str, environment_name: str) -> str:
|
|
1832
|
+
"""Get detailed configuration of a Fabric environment (READ-ONLY).
|
|
1833
|
+
|
|
1834
|
+
Args:
|
|
1835
|
+
workspace: Name or ID of the workspace
|
|
1836
|
+
environment_name: Name or ID of the environment
|
|
1837
|
+
"""
|
|
1838
|
+
try:
|
|
1839
|
+
credential = DefaultAzureCredential()
|
|
1840
|
+
client = FabricApiClient(credential)
|
|
1841
|
+
|
|
1842
|
+
# Convert names to IDs
|
|
1843
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
1844
|
+
|
|
1845
|
+
# Find the environment
|
|
1846
|
+
environments = await client.paginated_request(
|
|
1847
|
+
f"workspaces/{workspace_id}/items", params={"type": "Environment"}
|
|
1848
|
+
)
|
|
1849
|
+
environment = None
|
|
1850
|
+
for env in environments:
|
|
1851
|
+
if (
|
|
1852
|
+
environment_name.lower() in env["displayName"].lower()
|
|
1853
|
+
or env["id"] == environment_name
|
|
1854
|
+
):
|
|
1855
|
+
environment = env
|
|
1856
|
+
break
|
|
1857
|
+
|
|
1858
|
+
if not environment:
|
|
1859
|
+
return f"Environment '{environment_name}' not found in workspace '{workspace}'."
|
|
1860
|
+
|
|
1861
|
+
markdown = f"# Environment Details: {environment['displayName']}\n\n"
|
|
1862
|
+
markdown += f"**Workspace**: {workspace}\n"
|
|
1863
|
+
markdown += f"**Environment ID**: {environment['id']}\n"
|
|
1864
|
+
markdown += (
|
|
1865
|
+
f"**Description**: {environment.get('description', 'No description')}\n\n"
|
|
1866
|
+
)
|
|
1867
|
+
|
|
1868
|
+
# Get Spark compute configuration
|
|
1869
|
+
try:
|
|
1870
|
+
spark_config = await client._make_request(
|
|
1871
|
+
f"workspaces/{workspace_id}/environments/{environment['id']}/sparkcompute"
|
|
1872
|
+
)
|
|
1873
|
+
|
|
1874
|
+
markdown += "## Spark Compute Configuration\n"
|
|
1875
|
+
markdown += (
|
|
1876
|
+
f"- **Runtime Version**: {spark_config.get('runtimeVersion', 'N/A')}\n"
|
|
1877
|
+
)
|
|
1878
|
+
markdown += f"- **Pool Name**: {spark_config.get('poolName', 'N/A')}\n"
|
|
1879
|
+
markdown += (
|
|
1880
|
+
f"- **Driver Cores**: {spark_config.get('driverCores', 'N/A')}\n"
|
|
1881
|
+
)
|
|
1882
|
+
markdown += (
|
|
1883
|
+
f"- **Driver Memory**: {spark_config.get('driverMemory', 'N/A')}\n"
|
|
1884
|
+
)
|
|
1885
|
+
markdown += (
|
|
1886
|
+
f"- **Executor Cores**: {spark_config.get('executorCores', 'N/A')}\n"
|
|
1887
|
+
)
|
|
1888
|
+
markdown += (
|
|
1889
|
+
f"- **Executor Memory**: {spark_config.get('executorMemory', 'N/A')}\n"
|
|
1890
|
+
)
|
|
1891
|
+
markdown += f"- **Dynamic Executor Allocation**: {spark_config.get('dynamicExecutorAllocation', {}).get('enabled', 'N/A')}\n\n"
|
|
1892
|
+
|
|
1893
|
+
# Spark properties
|
|
1894
|
+
if spark_config.get("sparkProperties"):
|
|
1895
|
+
markdown += "### Spark Properties\n"
|
|
1896
|
+
for key, value in spark_config["sparkProperties"].items():
|
|
1897
|
+
markdown += f"- **{key}**: {value}\n"
|
|
1898
|
+
markdown += "\n"
|
|
1899
|
+
|
|
1900
|
+
except Exception:
|
|
1901
|
+
markdown += (
|
|
1902
|
+
"## Spark Compute Configuration\n*Not available or access denied*\n\n"
|
|
1903
|
+
)
|
|
1904
|
+
|
|
1905
|
+
# Get libraries
|
|
1906
|
+
try:
|
|
1907
|
+
libraries = await client._make_request(
|
|
1908
|
+
f"workspaces/{workspace_id}/environments/{environment['id']}/libraries"
|
|
1909
|
+
)
|
|
1910
|
+
|
|
1911
|
+
if libraries and libraries.get("libraries"):
|
|
1912
|
+
markdown += "## Installed Libraries\n"
|
|
1913
|
+
|
|
1914
|
+
public_libs = [
|
|
1915
|
+
lib for lib in libraries["libraries"] if lib.get("type") == "Public"
|
|
1916
|
+
]
|
|
1917
|
+
custom_libs = [
|
|
1918
|
+
lib for lib in libraries["libraries"] if lib.get("type") == "Custom"
|
|
1919
|
+
]
|
|
1920
|
+
|
|
1921
|
+
if public_libs:
|
|
1922
|
+
markdown += "### Public Libraries\n"
|
|
1923
|
+
for lib in public_libs[:20]: # Limit to first 20
|
|
1924
|
+
markdown += f"- **{lib.get('name', 'N/A')}**: {lib.get('version', 'N/A')}\n"
|
|
1925
|
+
if len(public_libs) > 20:
|
|
1926
|
+
markdown += f"- *... and {len(public_libs) - 20} more public libraries*\n"
|
|
1927
|
+
markdown += "\n"
|
|
1928
|
+
|
|
1929
|
+
if custom_libs:
|
|
1930
|
+
markdown += "### Custom Libraries\n"
|
|
1931
|
+
for lib in custom_libs:
|
|
1932
|
+
markdown += f"- **{lib.get('name', 'N/A')}** ({lib.get('size', 'N/A')} bytes)\n"
|
|
1933
|
+
markdown += "\n"
|
|
1934
|
+
else:
|
|
1935
|
+
markdown += (
|
|
1936
|
+
"## Installed Libraries\n*No additional libraries installed*\n\n"
|
|
1937
|
+
)
|
|
1938
|
+
|
|
1939
|
+
except Exception:
|
|
1940
|
+
markdown += "## Installed Libraries\n*Not available or access denied*\n\n"
|
|
1941
|
+
|
|
1942
|
+
return markdown
|
|
1943
|
+
|
|
1944
|
+
except Exception as e:
|
|
1945
|
+
return f"Error getting environment details: {str(e)}"
|
|
1946
|
+
|
|
1947
|
+
|
|
1948
|
+
@mcp.tool()
|
|
1949
|
+
async def list_compute_usage(workspace: str = None, time_range_hours: int = 24) -> str:
|
|
1950
|
+
"""Monitor current compute resource consumption across Fabric workloads (READ-ONLY).
|
|
1951
|
+
|
|
1952
|
+
Shows active Spark jobs, resource allocation, and capacity utilization.
|
|
1953
|
+
|
|
1954
|
+
Args:
|
|
1955
|
+
workspace: Optional workspace name or ID to filter usage
|
|
1956
|
+
time_range_hours: Hours to look back for usage data (default: 24)
|
|
1957
|
+
"""
|
|
1958
|
+
try:
|
|
1959
|
+
credential = DefaultAzureCredential()
|
|
1960
|
+
client = FabricApiClient(credential)
|
|
1961
|
+
|
|
1962
|
+
markdown = f"# Compute Usage Report\n\n"
|
|
1963
|
+
markdown += f"**Time Range**: Last {time_range_hours} hours\n"
|
|
1964
|
+
|
|
1965
|
+
if workspace:
|
|
1966
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
1967
|
+
markdown += f"**Workspace**: {workspace}\n\n"
|
|
1968
|
+
else:
|
|
1969
|
+
workspace_id = None
|
|
1970
|
+
markdown += f"**Scope**: All accessible workspaces\n\n"
|
|
1971
|
+
|
|
1972
|
+
# Get active job instances to show current compute usage
|
|
1973
|
+
active_jobs = []
|
|
1974
|
+
total_active_jobs = 0
|
|
1975
|
+
|
|
1976
|
+
if workspace_id:
|
|
1977
|
+
# Single workspace
|
|
1978
|
+
workspaces_to_check = [{"id": workspace_id, "displayName": workspace}]
|
|
1979
|
+
else:
|
|
1980
|
+
# All workspaces
|
|
1981
|
+
workspaces_to_check = await client.paginated_request("workspaces")
|
|
1982
|
+
|
|
1983
|
+
for ws in workspaces_to_check:
|
|
1984
|
+
try:
|
|
1985
|
+
# Get items in workspace
|
|
1986
|
+
items = await client.paginated_request(f"workspaces/{ws['id']}/items")
|
|
1987
|
+
|
|
1988
|
+
for item in items:
|
|
1989
|
+
try:
|
|
1990
|
+
# Get active job instances
|
|
1991
|
+
jobs = await client.paginated_request(
|
|
1992
|
+
f"workspaces/{ws['id']}/items/{item['id']}/jobs/instances",
|
|
1993
|
+
params={"status": "InProgress"}, # Only active jobs
|
|
1994
|
+
)
|
|
1995
|
+
|
|
1996
|
+
for job in jobs:
|
|
1997
|
+
if job.get("status") == "InProgress":
|
|
1998
|
+
job["workspaceName"] = ws["displayName"]
|
|
1999
|
+
job["itemName"] = item["displayName"]
|
|
2000
|
+
job["itemType"] = item["type"]
|
|
2001
|
+
active_jobs.append(job)
|
|
2002
|
+
total_active_jobs += 1
|
|
2003
|
+
|
|
2004
|
+
except Exception:
|
|
2005
|
+
continue
|
|
2006
|
+
|
|
2007
|
+
except Exception:
|
|
2008
|
+
continue
|
|
2009
|
+
|
|
2010
|
+
# Summary section
|
|
2011
|
+
markdown += "## Current Usage Summary\n"
|
|
2012
|
+
markdown += f"- **Active Jobs**: {total_active_jobs}\n"
|
|
2013
|
+
|
|
2014
|
+
# Estimate resource usage (this is approximate since we don't have direct CU API)
|
|
2015
|
+
if total_active_jobs > 0:
|
|
2016
|
+
# Rough estimation: assume average job uses 4-8 cores
|
|
2017
|
+
estimated_cores = total_active_jobs * 6 # Average estimate
|
|
2018
|
+
estimated_cus = estimated_cores / 2 # 1 CU = 2 Spark cores
|
|
2019
|
+
markdown += (
|
|
2020
|
+
f"- **Estimated Active Cores**: ~{estimated_cores} Spark vCores\n"
|
|
2021
|
+
)
|
|
2022
|
+
markdown += f"- **Estimated Capacity Usage**: ~{estimated_cus:.1f} CUs\n"
|
|
2023
|
+
else:
|
|
2024
|
+
markdown += f"- **Estimated Active Cores**: 0 Spark vCores\n"
|
|
2025
|
+
markdown += f"- **Estimated Capacity Usage**: 0 CUs\n"
|
|
2026
|
+
|
|
2027
|
+
markdown += "\n"
|
|
2028
|
+
|
|
2029
|
+
if active_jobs:
|
|
2030
|
+
markdown += "## Active Jobs Details\n"
|
|
2031
|
+
markdown += "| Workspace | Item | Type | Job Type | Status | Start Time | Duration |\n"
|
|
2032
|
+
markdown += "|-----------|------|------|----------|--------|------------|----------|\n"
|
|
2033
|
+
|
|
2034
|
+
for job in active_jobs[:30]: # Limit to 30 most recent
|
|
2035
|
+
start_time = job.get("startTimeUtc", "N/A")
|
|
2036
|
+
# Calculate rough duration if we have start time
|
|
2037
|
+
duration = "Running"
|
|
2038
|
+
|
|
2039
|
+
markdown += f"| {job['workspaceName']} | {job['itemName']} | {job['itemType']} | "
|
|
2040
|
+
markdown += (
|
|
2041
|
+
f"{job.get('jobType', 'N/A')} | {job.get('status', 'N/A')} | "
|
|
2042
|
+
)
|
|
2043
|
+
markdown += f"{start_time} | {duration} |\n"
|
|
2044
|
+
|
|
2045
|
+
if len(active_jobs) > 30:
|
|
2046
|
+
markdown += f"\n*Showing 30 most recent active jobs out of {len(active_jobs)} total.*\n"
|
|
2047
|
+
else:
|
|
2048
|
+
markdown += "## Active Jobs Details\n*No active jobs found.*\n"
|
|
2049
|
+
|
|
2050
|
+
# Resource recommendations
|
|
2051
|
+
markdown += "\n## Usage Insights\n"
|
|
2052
|
+
if total_active_jobs == 0:
|
|
2053
|
+
markdown += "✅ **Low Usage**: No active compute jobs detected.\n"
|
|
2054
|
+
elif total_active_jobs <= 5:
|
|
2055
|
+
markdown += "🟡 **Moderate Usage**: Few active jobs running.\n"
|
|
2056
|
+
else:
|
|
2057
|
+
markdown += (
|
|
2058
|
+
"🔴 **High Usage**: Many active jobs - consider capacity planning.\n"
|
|
2059
|
+
)
|
|
2060
|
+
|
|
2061
|
+
markdown += "\n*Note: This shows current job activity. For detailed capacity metrics and historical usage, use the Microsoft Fabric Capacity Metrics app.*\n"
|
|
2062
|
+
|
|
2063
|
+
return markdown
|
|
2064
|
+
|
|
2065
|
+
except Exception as e:
|
|
2066
|
+
return f"Error getting compute usage: {str(e)}"
|
|
2067
|
+
|
|
2068
|
+
|
|
2069
|
+
@mcp.tool()
|
|
2070
|
+
async def get_item_lineage(workspace: str, item_name: str) -> str:
|
|
2071
|
+
"""Get data lineage information for a Fabric item (READ-ONLY).
|
|
2072
|
+
|
|
2073
|
+
Shows upstream and downstream dependencies, data flow relationships.
|
|
2074
|
+
Note: This provides item-level lineage based on workspace relationships.
|
|
2075
|
+
|
|
2076
|
+
Args:
|
|
2077
|
+
workspace: Name or ID of the workspace
|
|
2078
|
+
item_name: Name or ID of the item to analyze
|
|
2079
|
+
"""
|
|
2080
|
+
try:
|
|
2081
|
+
credential = DefaultAzureCredential()
|
|
2082
|
+
client = FabricApiClient(credential)
|
|
2083
|
+
|
|
2084
|
+
# Convert names to IDs
|
|
2085
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
2086
|
+
|
|
2087
|
+
# Find the target item
|
|
2088
|
+
items = await client.paginated_request(f"workspaces/{workspace_id}/items")
|
|
2089
|
+
target_item = None
|
|
2090
|
+
for item in items:
|
|
2091
|
+
if (
|
|
2092
|
+
item_name.lower() in item["displayName"].lower()
|
|
2093
|
+
or item["id"] == item_name
|
|
2094
|
+
):
|
|
2095
|
+
target_item = item
|
|
2096
|
+
break
|
|
2097
|
+
|
|
2098
|
+
if not target_item:
|
|
2099
|
+
return f"Item '{item_name}' not found in workspace '{workspace}'."
|
|
2100
|
+
|
|
2101
|
+
markdown = f"# Data Lineage: {target_item['displayName']}\n\n"
|
|
2102
|
+
markdown += f"**Item Type**: {target_item['type']}\n"
|
|
2103
|
+
markdown += f"**Workspace**: {workspace}\n"
|
|
2104
|
+
markdown += f"**Item ID**: {target_item['id']}\n\n"
|
|
2105
|
+
|
|
2106
|
+
# Analyze relationships based on item type and connections
|
|
2107
|
+
upstream_items = []
|
|
2108
|
+
downstream_items = []
|
|
2109
|
+
|
|
2110
|
+
# Get all shortcuts if this is a lakehouse
|
|
2111
|
+
if target_item["type"] == "Lakehouse":
|
|
2112
|
+
try:
|
|
2113
|
+
shortcuts = await client.paginated_request(
|
|
2114
|
+
f"workspaces/{workspace_id}/items/{target_item['id']}/shortcuts"
|
|
2115
|
+
)
|
|
2116
|
+
if shortcuts:
|
|
2117
|
+
markdown += "## Data Sources (via OneLake Shortcuts)\n"
|
|
2118
|
+
for shortcut in shortcuts:
|
|
2119
|
+
target_path = shortcut.get("target", {})
|
|
2120
|
+
if target_path.get("lakehouse"):
|
|
2121
|
+
upstream_items.append(
|
|
2122
|
+
{
|
|
2123
|
+
"name": f"Lakehouse: {target_path['lakehouse'].get('workspaceName', 'Unknown')}/{target_path['lakehouse'].get('itemName', 'Unknown')}",
|
|
2124
|
+
"type": "Lakehouse",
|
|
2125
|
+
"relationship": "Data Source",
|
|
2126
|
+
}
|
|
2127
|
+
)
|
|
2128
|
+
elif target_path.get("adlsGen2"):
|
|
2129
|
+
upstream_items.append(
|
|
2130
|
+
{
|
|
2131
|
+
"name": f"ADLS Gen2: {target_path['adlsGen2'].get('url', 'Unknown')}",
|
|
2132
|
+
"type": "External Storage",
|
|
2133
|
+
"relationship": "Data Source",
|
|
2134
|
+
}
|
|
2135
|
+
)
|
|
2136
|
+
except Exception:
|
|
2137
|
+
pass
|
|
2138
|
+
|
|
2139
|
+
# Find items that might depend on this item (basic analysis)
|
|
2140
|
+
for item in items:
|
|
2141
|
+
if item["id"] == target_item["id"]:
|
|
2142
|
+
continue
|
|
2143
|
+
|
|
2144
|
+
# Notebooks often depend on lakehouses
|
|
2145
|
+
if item["type"] == "Notebook" and target_item["type"] == "Lakehouse":
|
|
2146
|
+
downstream_items.append(
|
|
2147
|
+
{
|
|
2148
|
+
"name": item["displayName"],
|
|
2149
|
+
"type": item["type"],
|
|
2150
|
+
"relationship": "Likely Consumer",
|
|
2151
|
+
}
|
|
2152
|
+
)
|
|
2153
|
+
|
|
2154
|
+
# Reports might depend on semantic models/datasets
|
|
2155
|
+
elif item["type"] == "Report" and target_item["type"] in [
|
|
2156
|
+
"SemanticModel",
|
|
2157
|
+
"Dataset",
|
|
2158
|
+
]:
|
|
2159
|
+
downstream_items.append(
|
|
2160
|
+
{
|
|
2161
|
+
"name": item["displayName"],
|
|
2162
|
+
"type": item["type"],
|
|
2163
|
+
"relationship": "Likely Consumer",
|
|
2164
|
+
}
|
|
2165
|
+
)
|
|
2166
|
+
|
|
2167
|
+
# Pipelines might depend on various items
|
|
2168
|
+
elif item["type"] == "DataPipeline":
|
|
2169
|
+
if target_item["type"] in ["Lakehouse", "Warehouse", "KqlDatabase"]:
|
|
2170
|
+
downstream_items.append(
|
|
2171
|
+
{
|
|
2172
|
+
"name": item["displayName"],
|
|
2173
|
+
"type": item["type"],
|
|
2174
|
+
"relationship": "Possible Consumer",
|
|
2175
|
+
}
|
|
2176
|
+
)
|
|
2177
|
+
|
|
2178
|
+
# Display upstream dependencies
|
|
2179
|
+
if upstream_items:
|
|
2180
|
+
markdown += "## Upstream Dependencies\n"
|
|
2181
|
+
markdown += "*Items that this item depends on for data*\n\n"
|
|
2182
|
+
markdown += "| Name | Type | Relationship |\n"
|
|
2183
|
+
markdown += "|------|------|-------------|\n"
|
|
2184
|
+
for item in upstream_items:
|
|
2185
|
+
markdown += (
|
|
2186
|
+
f"| {item['name']} | {item['type']} | {item['relationship']} |\n"
|
|
2187
|
+
)
|
|
2188
|
+
markdown += "\n"
|
|
2189
|
+
else:
|
|
2190
|
+
markdown += (
|
|
2191
|
+
"## Upstream Dependencies\n*No upstream dependencies detected*\n\n"
|
|
2192
|
+
)
|
|
2193
|
+
|
|
2194
|
+
# Display downstream dependencies
|
|
2195
|
+
if downstream_items:
|
|
2196
|
+
markdown += "## Downstream Dependencies\n"
|
|
2197
|
+
markdown += "*Items that likely depend on this item*\n\n"
|
|
2198
|
+
markdown += "| Name | Type | Relationship |\n"
|
|
2199
|
+
markdown += "|------|------|-------------|\n"
|
|
2200
|
+
for item in downstream_items[:20]: # Limit to 20
|
|
2201
|
+
markdown += (
|
|
2202
|
+
f"| {item['name']} | {item['type']} | {item['relationship']} |\n"
|
|
2203
|
+
)
|
|
2204
|
+
if len(downstream_items) > 20:
|
|
2205
|
+
markdown += f"\n*... and {len(downstream_items) - 20} more potential dependencies*\n"
|
|
2206
|
+
markdown += "\n"
|
|
2207
|
+
else:
|
|
2208
|
+
markdown += (
|
|
2209
|
+
"## Downstream Dependencies\n*No downstream dependencies detected*\n\n"
|
|
2210
|
+
)
|
|
2211
|
+
|
|
2212
|
+
# Impact analysis summary
|
|
2213
|
+
markdown += "## Impact Analysis Summary\n"
|
|
2214
|
+
total_dependencies = len(upstream_items) + len(downstream_items)
|
|
2215
|
+
if total_dependencies == 0:
|
|
2216
|
+
markdown += (
|
|
2217
|
+
"✅ **Low Impact**: This item appears to have minimal dependencies.\n"
|
|
2218
|
+
)
|
|
2219
|
+
elif total_dependencies <= 5:
|
|
2220
|
+
markdown += (
|
|
2221
|
+
"🟡 **Moderate Impact**: This item has some dependencies to consider.\n"
|
|
2222
|
+
)
|
|
2223
|
+
else:
|
|
2224
|
+
markdown += "🔴 **High Impact**: This item has many dependencies - changes should be carefully planned.\n"
|
|
2225
|
+
|
|
2226
|
+
markdown += "\n*Note: This provides basic lineage analysis based on item types and relationships. For detailed column-level lineage, use the Fabric Lineage view in the UI or Microsoft Purview integration.*\n"
|
|
2227
|
+
|
|
2228
|
+
return markdown
|
|
2229
|
+
|
|
2230
|
+
except Exception as e:
|
|
2231
|
+
return f"Error getting item lineage: {str(e)}"
|
|
2232
|
+
|
|
2233
|
+
|
|
2234
|
+
@mcp.tool()
|
|
2235
|
+
async def list_item_dependencies(workspace: str, item_type: str = None) -> str:
|
|
2236
|
+
"""List dependencies between items in a workspace (READ-ONLY).
|
|
2237
|
+
|
|
2238
|
+
Shows which items depend on which other items for comprehensive dependency mapping.
|
|
2239
|
+
|
|
2240
|
+
Args:
|
|
2241
|
+
workspace: Name or ID of the workspace
|
|
2242
|
+
item_type: Optional item type filter (Lakehouse, Notebook, Report, etc.)
|
|
2243
|
+
"""
|
|
2244
|
+
try:
|
|
2245
|
+
credential = DefaultAzureCredential()
|
|
2246
|
+
client = FabricApiClient(credential)
|
|
2247
|
+
|
|
2248
|
+
# Convert names to IDs
|
|
2249
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
2250
|
+
|
|
2251
|
+
# Get all items in workspace
|
|
2252
|
+
items = await client.paginated_request(f"workspaces/{workspace_id}/items")
|
|
2253
|
+
|
|
2254
|
+
if not items:
|
|
2255
|
+
return f"No items found in workspace '{workspace}'."
|
|
2256
|
+
|
|
2257
|
+
# Filter by item type if specified
|
|
2258
|
+
if item_type:
|
|
2259
|
+
items = [
|
|
2260
|
+
item for item in items if item["type"].lower() == item_type.lower()
|
|
2261
|
+
]
|
|
2262
|
+
if not items:
|
|
2263
|
+
return (
|
|
2264
|
+
f"No items of type '{item_type}' found in workspace '{workspace}'."
|
|
2265
|
+
)
|
|
2266
|
+
|
|
2267
|
+
markdown = f"# Item Dependencies in '{workspace}'\n\n"
|
|
2268
|
+
if item_type:
|
|
2269
|
+
markdown += f"**Item Type Filter**: {item_type}\n"
|
|
2270
|
+
markdown += f"**Total Items**: {len(items)}\n\n"
|
|
2271
|
+
|
|
2272
|
+
# Analyze dependencies
|
|
2273
|
+
dependencies = []
|
|
2274
|
+
|
|
2275
|
+
for item in items:
|
|
2276
|
+
item_deps = {"item": item, "depends_on": [], "used_by": []}
|
|
2277
|
+
|
|
2278
|
+
# Check for shortcuts (data dependencies)
|
|
2279
|
+
if item["type"] == "Lakehouse":
|
|
2280
|
+
try:
|
|
2281
|
+
shortcuts = await client.paginated_request(
|
|
2282
|
+
f"workspaces/{workspace_id}/items/{item['id']}/shortcuts"
|
|
2283
|
+
)
|
|
2284
|
+
for shortcut in shortcuts:
|
|
2285
|
+
target = shortcut.get("target", {})
|
|
2286
|
+
if target.get("lakehouse"):
|
|
2287
|
+
item_deps["depends_on"].append(
|
|
2288
|
+
{
|
|
2289
|
+
"name": f"{target['lakehouse'].get('itemName', 'Unknown')}",
|
|
2290
|
+
"type": "Lakehouse",
|
|
2291
|
+
"relationship": "Data Source (Shortcut)",
|
|
2292
|
+
}
|
|
2293
|
+
)
|
|
2294
|
+
except Exception:
|
|
2295
|
+
pass
|
|
2296
|
+
|
|
2297
|
+
# Analyze potential relationships based on item types
|
|
2298
|
+
for other_item in items:
|
|
2299
|
+
if other_item["id"] == item["id"]:
|
|
2300
|
+
continue
|
|
2301
|
+
|
|
2302
|
+
# Common dependency patterns
|
|
2303
|
+
if item["type"] == "Notebook":
|
|
2304
|
+
if other_item["type"] in ["Lakehouse", "Warehouse", "KqlDatabase"]:
|
|
2305
|
+
item_deps["depends_on"].append(
|
|
2306
|
+
{
|
|
2307
|
+
"name": other_item["displayName"],
|
|
2308
|
+
"type": other_item["type"],
|
|
2309
|
+
"relationship": "Likely Data Source",
|
|
2310
|
+
}
|
|
2311
|
+
)
|
|
2312
|
+
|
|
2313
|
+
elif item["type"] == "Report":
|
|
2314
|
+
if other_item["type"] in [
|
|
2315
|
+
"SemanticModel",
|
|
2316
|
+
"Dataset",
|
|
2317
|
+
"Lakehouse",
|
|
2318
|
+
"Warehouse",
|
|
2319
|
+
]:
|
|
2320
|
+
item_deps["depends_on"].append(
|
|
2321
|
+
{
|
|
2322
|
+
"name": other_item["displayName"],
|
|
2323
|
+
"type": other_item["type"],
|
|
2324
|
+
"relationship": "Likely Data Source",
|
|
2325
|
+
}
|
|
2326
|
+
)
|
|
2327
|
+
|
|
2328
|
+
elif item["type"] == "DataPipeline":
|
|
2329
|
+
if other_item["type"] in [
|
|
2330
|
+
"Lakehouse",
|
|
2331
|
+
"Warehouse",
|
|
2332
|
+
"Notebook",
|
|
2333
|
+
"DataFlow",
|
|
2334
|
+
]:
|
|
2335
|
+
item_deps["depends_on"].append(
|
|
2336
|
+
{
|
|
2337
|
+
"name": other_item["displayName"],
|
|
2338
|
+
"type": other_item["type"],
|
|
2339
|
+
"relationship": "Pipeline Component",
|
|
2340
|
+
}
|
|
2341
|
+
)
|
|
2342
|
+
|
|
2343
|
+
dependencies.append(item_deps)
|
|
2344
|
+
|
|
2345
|
+
# Display dependency matrix
|
|
2346
|
+
markdown += "## Dependency Overview\n"
|
|
2347
|
+
markdown += "| Item | Type | Dependencies | Used By |\n"
|
|
2348
|
+
markdown += "|------|------|--------------|--------|\n"
|
|
2349
|
+
|
|
2350
|
+
for dep in dependencies:
|
|
2351
|
+
item = dep["item"]
|
|
2352
|
+
dep_count = len(dep["depends_on"])
|
|
2353
|
+
used_count = sum(
|
|
2354
|
+
1
|
|
2355
|
+
for other_dep in dependencies
|
|
2356
|
+
for depends in other_dep["depends_on"]
|
|
2357
|
+
if depends["name"] == item["displayName"]
|
|
2358
|
+
)
|
|
2359
|
+
|
|
2360
|
+
markdown += f"| {item['displayName']} | {item['type']} | {dep_count} | {used_count} |\n"
|
|
2361
|
+
|
|
2362
|
+
# Detailed dependencies
|
|
2363
|
+
markdown += "\n## Detailed Dependencies\n"
|
|
2364
|
+
|
|
2365
|
+
for dep in dependencies:
|
|
2366
|
+
if dep["depends_on"]:
|
|
2367
|
+
item = dep["item"]
|
|
2368
|
+
markdown += f"\n### {item['displayName']} ({item['type']})\n"
|
|
2369
|
+
markdown += "**Depends on:**\n"
|
|
2370
|
+
for depends in dep["depends_on"]:
|
|
2371
|
+
markdown += f"- **{depends['name']}** ({depends['type']}) - {depends['relationship']}\n"
|
|
2372
|
+
|
|
2373
|
+
# Dependency insights
|
|
2374
|
+
high_dependency_items = [
|
|
2375
|
+
dep for dep in dependencies if len(dep["depends_on"]) > 3
|
|
2376
|
+
]
|
|
2377
|
+
highly_used_items = []
|
|
2378
|
+
|
|
2379
|
+
for dep in dependencies:
|
|
2380
|
+
item = dep["item"]
|
|
2381
|
+
usage_count = sum(
|
|
2382
|
+
1
|
|
2383
|
+
for other_dep in dependencies
|
|
2384
|
+
for depends in other_dep["depends_on"]
|
|
2385
|
+
if depends["name"] == item["displayName"]
|
|
2386
|
+
)
|
|
2387
|
+
if usage_count > 2:
|
|
2388
|
+
highly_used_items.append((item, usage_count))
|
|
2389
|
+
|
|
2390
|
+
markdown += "\n## Dependency Insights\n"
|
|
2391
|
+
|
|
2392
|
+
if high_dependency_items:
|
|
2393
|
+
markdown += "### High Dependency Items (>3 dependencies)\n"
|
|
2394
|
+
for dep in high_dependency_items:
|
|
2395
|
+
markdown += f"- **{dep['item']['displayName']}** ({dep['item']['type']}) - {len(dep['depends_on'])} dependencies\n"
|
|
2396
|
+
markdown += "\n"
|
|
2397
|
+
|
|
2398
|
+
if highly_used_items:
|
|
2399
|
+
markdown += "### Highly Used Items (>2 dependents)\n"
|
|
2400
|
+
for item, count in highly_used_items:
|
|
2401
|
+
markdown += f"- **{item['displayName']}** ({item['type']}) - Used by {count} items\n"
|
|
2402
|
+
markdown += "\n"
|
|
2403
|
+
|
|
2404
|
+
markdown += "*Note: Dependencies are inferred based on common patterns and item types. For exact lineage, use the Fabric Lineage view or detailed analysis.*\n"
|
|
2405
|
+
|
|
2406
|
+
return markdown
|
|
2407
|
+
|
|
2408
|
+
except Exception as e:
|
|
2409
|
+
return f"Error listing item dependencies: {str(e)}"
|
|
2410
|
+
|
|
2411
|
+
|
|
2412
|
+
@mcp.tool()
|
|
2413
|
+
async def get_data_source_usage(
|
|
2414
|
+
workspace: str = None, connection_name: str = None
|
|
2415
|
+
) -> str:
|
|
2416
|
+
"""Analyze where data sources and connections are used across Fabric items (READ-ONLY).
|
|
2417
|
+
|
|
2418
|
+
Args:
|
|
2419
|
+
workspace: Optional workspace name or ID to scope analysis
|
|
2420
|
+
connection_name: Optional connection name to focus on specific data source
|
|
2421
|
+
"""
|
|
2422
|
+
try:
|
|
2423
|
+
credential = DefaultAzureCredential()
|
|
2424
|
+
client = FabricApiClient(credential)
|
|
2425
|
+
|
|
2426
|
+
markdown = f"# Data Source Usage Analysis\n\n"
|
|
2427
|
+
|
|
2428
|
+
# Get connections
|
|
2429
|
+
connections = await client.paginated_request("connections")
|
|
2430
|
+
|
|
2431
|
+
if connection_name:
|
|
2432
|
+
# Filter to specific connection
|
|
2433
|
+
connections = [
|
|
2434
|
+
conn
|
|
2435
|
+
for conn in connections
|
|
2436
|
+
if connection_name.lower() in conn["displayName"].lower()
|
|
2437
|
+
]
|
|
2438
|
+
if not connections:
|
|
2439
|
+
return f"Connection '{connection_name}' not found."
|
|
2440
|
+
markdown += f"**Connection Filter**: {connection_name}\n"
|
|
2441
|
+
|
|
2442
|
+
if workspace:
|
|
2443
|
+
workspace_id = await client.resolve_workspace(workspace)
|
|
2444
|
+
workspaces_to_check = [{"id": workspace_id, "displayName": workspace}]
|
|
2445
|
+
markdown += f"**Workspace Filter**: {workspace}\n"
|
|
2446
|
+
else:
|
|
2447
|
+
workspaces_to_check = await client.paginated_request("workspaces")
|
|
2448
|
+
markdown += f"**Scope**: All accessible workspaces\n"
|
|
2449
|
+
|
|
2450
|
+
markdown += f"**Connections Found**: {len(connections)}\n\n"
|
|
2451
|
+
|
|
2452
|
+
usage_analysis = {}
|
|
2453
|
+
|
|
2454
|
+
# Analyze each connection
|
|
2455
|
+
for connection in connections:
|
|
2456
|
+
conn_usage = {
|
|
2457
|
+
"connection": connection,
|
|
2458
|
+
"used_in_items": [],
|
|
2459
|
+
"total_usage": 0,
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
# Check usage across workspaces
|
|
2463
|
+
for ws in workspaces_to_check:
|
|
2464
|
+
try:
|
|
2465
|
+
items = await client.paginated_request(
|
|
2466
|
+
f"workspaces/{ws['id']}/items"
|
|
2467
|
+
)
|
|
2468
|
+
|
|
2469
|
+
for item in items:
|
|
2470
|
+
# Items that commonly use connections
|
|
2471
|
+
if item["type"] in [
|
|
2472
|
+
"DataPipeline",
|
|
2473
|
+
"Dataflow",
|
|
2474
|
+
"Notebook",
|
|
2475
|
+
"Report",
|
|
2476
|
+
"SemanticModel",
|
|
2477
|
+
]:
|
|
2478
|
+
# This is a heuristic - in practice, we'd need to inspect item definitions
|
|
2479
|
+
# For now, we'll identify potential usage based on item types
|
|
2480
|
+
conn_usage["used_in_items"].append(
|
|
2481
|
+
{
|
|
2482
|
+
"workspace": ws["displayName"],
|
|
2483
|
+
"item": item["displayName"],
|
|
2484
|
+
"type": item["type"],
|
|
2485
|
+
"usage_type": "Potential Usage",
|
|
2486
|
+
}
|
|
2487
|
+
)
|
|
2488
|
+
conn_usage["total_usage"] += 1
|
|
2489
|
+
|
|
2490
|
+
except Exception:
|
|
2491
|
+
continue
|
|
2492
|
+
|
|
2493
|
+
if conn_usage["total_usage"] > 0 or not connection_name:
|
|
2494
|
+
usage_analysis[connection["id"]] = conn_usage
|
|
2495
|
+
|
|
2496
|
+
# Display results
|
|
2497
|
+
if not usage_analysis:
|
|
2498
|
+
return "No data source usage found with the specified filters."
|
|
2499
|
+
|
|
2500
|
+
# Summary
|
|
2501
|
+
markdown += "## Usage Summary\n"
|
|
2502
|
+
total_connections = len(usage_analysis)
|
|
2503
|
+
total_usages = sum(
|
|
2504
|
+
analysis["total_usage"] for analysis in usage_analysis.values()
|
|
2505
|
+
)
|
|
2506
|
+
|
|
2507
|
+
markdown += f"- **Connections Analyzed**: {total_connections}\n"
|
|
2508
|
+
markdown += f"- **Total Potential Usages**: {total_usages}\n\n"
|
|
2509
|
+
|
|
2510
|
+
# Top used connections
|
|
2511
|
+
sorted_connections = sorted(
|
|
2512
|
+
usage_analysis.values(), key=lambda x: x["total_usage"], reverse=True
|
|
2513
|
+
)
|
|
2514
|
+
|
|
2515
|
+
markdown += "## Connection Usage Details\n"
|
|
2516
|
+
|
|
2517
|
+
for analysis in sorted_connections[:20]: # Top 20
|
|
2518
|
+
connection = analysis["connection"]
|
|
2519
|
+
conn_type = connection.get("connectionDetails", {}).get("type", "Unknown")
|
|
2520
|
+
|
|
2521
|
+
markdown += f"\n### {connection['displayName']} ({conn_type})\n"
|
|
2522
|
+
markdown += f"**Connection ID**: {connection['id']}\n"
|
|
2523
|
+
markdown += f"**Privacy Level**: {connection.get('privacyLevel', 'N/A')}\n"
|
|
2524
|
+
markdown += f"**Total Potential Usages**: {analysis['total_usage']}\n"
|
|
2525
|
+
|
|
2526
|
+
if analysis["used_in_items"]:
|
|
2527
|
+
markdown += "\n**Used in Items:**\n"
|
|
2528
|
+
markdown += "| Workspace | Item | Type | Usage Type |\n"
|
|
2529
|
+
markdown += "|-----------|------|------|------------|\n"
|
|
2530
|
+
|
|
2531
|
+
for usage in analysis["used_in_items"][
|
|
2532
|
+
:10
|
|
2533
|
+
]: # Limit to 10 per connection
|
|
2534
|
+
markdown += f"| {usage['workspace']} | {usage['item']} | {usage['type']} | {usage['usage_type']} |\n"
|
|
2535
|
+
|
|
2536
|
+
if len(analysis["used_in_items"]) > 10:
|
|
2537
|
+
markdown += f"\n*... and {len(analysis['used_in_items']) - 10} more usages*\n"
|
|
2538
|
+
|
|
2539
|
+
markdown += "\n"
|
|
2540
|
+
|
|
2541
|
+
# Usage insights
|
|
2542
|
+
markdown += "## Usage Insights\n"
|
|
2543
|
+
|
|
2544
|
+
unused_connections = [
|
|
2545
|
+
analysis
|
|
2546
|
+
for analysis in usage_analysis.values()
|
|
2547
|
+
if analysis["total_usage"] == 0
|
|
2548
|
+
]
|
|
2549
|
+
highly_used = [
|
|
2550
|
+
analysis
|
|
2551
|
+
for analysis in usage_analysis.values()
|
|
2552
|
+
if analysis["total_usage"] > 5
|
|
2553
|
+
]
|
|
2554
|
+
|
|
2555
|
+
if unused_connections:
|
|
2556
|
+
markdown += (
|
|
2557
|
+
f"### Potentially Unused Connections ({len(unused_connections)})\n"
|
|
2558
|
+
)
|
|
2559
|
+
for analysis in unused_connections[:5]:
|
|
2560
|
+
conn = analysis["connection"]
|
|
2561
|
+
markdown += f"- **{conn['displayName']}** ({conn.get('connectionDetails', {}).get('type', 'Unknown')})\n"
|
|
2562
|
+
if len(unused_connections) > 5:
|
|
2563
|
+
markdown += f"- *... and {len(unused_connections) - 5} more*\n"
|
|
2564
|
+
markdown += "\n"
|
|
2565
|
+
|
|
2566
|
+
if highly_used:
|
|
2567
|
+
markdown += f"### Highly Used Connections (>5 usages)\n"
|
|
2568
|
+
for analysis in highly_used:
|
|
2569
|
+
conn = analysis["connection"]
|
|
2570
|
+
markdown += f"- **{conn['displayName']}**: {analysis['total_usage']} potential usages\n"
|
|
2571
|
+
markdown += "\n"
|
|
2572
|
+
|
|
2573
|
+
markdown += "*Note: Usage analysis is based on item types and potential patterns. For exact connection usage, detailed item definition analysis would be required.*\n"
|
|
2574
|
+
|
|
2575
|
+
return markdown
|
|
2576
|
+
|
|
2577
|
+
except Exception as e:
|
|
2578
|
+
return f"Error analyzing data source usage: {str(e)}"
|
|
2579
|
+
|
|
2580
|
+
|
|
2581
|
+
@mcp.tool()
|
|
2582
|
+
async def clear_fabric_data_cache(show_stats: bool = True) -> str:
|
|
2583
|
+
"""Clear Fabric data list caches to see newly created resources immediately (ADMIN).
|
|
2584
|
+
|
|
2585
|
+
Clears caches for: workspaces, connections, items, capacities, environments, jobs, shortcuts, schedules.
|
|
2586
|
+
Does NOT clear name→ID resolution caches (those never become invalid).
|
|
2587
|
+
|
|
2588
|
+
Use this after creating new workspaces, connections, or items to see them immediately.
|
|
2589
|
+
|
|
2590
|
+
Args:
|
|
2591
|
+
show_stats: Show cache statistics before clearing
|
|
2592
|
+
"""
|
|
2593
|
+
try:
|
|
2594
|
+
markdown = "# 🗑️ Fabric Data Cache Management\n\n"
|
|
2595
|
+
|
|
2596
|
+
cleared_caches = []
|
|
2597
|
+
cache_stats = []
|
|
2598
|
+
|
|
2599
|
+
# Collect cache stats and clear TTL-based data caches
|
|
2600
|
+
cache_info = [
|
|
2601
|
+
("Workspace list", _WORKSPACE_CACHE, "workspaces"),
|
|
2602
|
+
("Connections list", _CONNECTIONS_CACHE, "connections"),
|
|
2603
|
+
("Capacities list", _CAPACITIES_CACHE, "capacities"),
|
|
2604
|
+
("Items lists", _ITEMS_CACHE, "workspace items"),
|
|
2605
|
+
("Shortcuts lists", _SHORTCUTS_CACHE, "item shortcuts"),
|
|
2606
|
+
("Job instances", _JOB_INSTANCES_CACHE, "job instances"),
|
|
2607
|
+
("Schedules", _SCHEDULES_CACHE, "schedules"),
|
|
2608
|
+
("Environments", _ENVIRONMENTS_CACHE, "environments"),
|
|
2609
|
+
]
|
|
2610
|
+
|
|
2611
|
+
if show_stats:
|
|
2612
|
+
markdown += "## 📊 Cache Statistics (Before Clearing)\n\n"
|
|
2613
|
+
markdown += "| Cache Type | Entries | TTL (seconds) | Description |\n"
|
|
2614
|
+
markdown += "|------------|---------|---------------|-------------|\n"
|
|
2615
|
+
|
|
2616
|
+
for name, cache, desc in cache_info:
|
|
2617
|
+
entries = len(cache)
|
|
2618
|
+
ttl = cache.ttl if hasattr(cache, "ttl") else "N/A"
|
|
2619
|
+
markdown += f"| {name} | {entries} | {ttl} | {desc} |\n"
|
|
2620
|
+
cache_stats.append((name, entries))
|
|
2621
|
+
|
|
2622
|
+
markdown += "\n"
|
|
2623
|
+
|
|
2624
|
+
# Clear all TTL caches
|
|
2625
|
+
for name, cache, desc in cache_info:
|
|
2626
|
+
size = len(cache)
|
|
2627
|
+
cache.clear()
|
|
2628
|
+
if size > 0:
|
|
2629
|
+
cleared_caches.append(f"{name} ({size} entries)")
|
|
2630
|
+
|
|
2631
|
+
markdown += "## ✅ Caches Cleared\n\n"
|
|
2632
|
+
|
|
2633
|
+
if cleared_caches:
|
|
2634
|
+
for cache in cleared_caches:
|
|
2635
|
+
markdown += f"- 🗑️ {cache}\n"
|
|
2636
|
+
else:
|
|
2637
|
+
markdown += "- ℹ️ No cached data found to clear\n"
|
|
2638
|
+
|
|
2639
|
+
markdown += "\n## 🔒 Preserved Caches\n\n"
|
|
2640
|
+
markdown += "- ✅ Workspace name → ID mappings (never become invalid)\n"
|
|
2641
|
+
markdown += "- ✅ Lakehouse name → ID mappings (never become invalid)\n"
|
|
2642
|
+
|
|
2643
|
+
markdown += "\n## 🎯 Result\n\n"
|
|
2644
|
+
markdown += (
|
|
2645
|
+
"**Next API calls will fetch fresh data from Microsoft Fabric!**\n\n"
|
|
2646
|
+
)
|
|
2647
|
+
markdown += "⚠️ **Performance Impact:** Initial calls will be slower until caches rebuild.\n"
|
|
2648
|
+
markdown += "⚡ **Cache Rebuild:** Caches will automatically repopulate on next use with fresh data."
|
|
2649
|
+
|
|
2650
|
+
return markdown
|
|
2651
|
+
|
|
2652
|
+
except Exception as e:
|
|
2653
|
+
return f"Error clearing data cache: {str(e)}"
|
|
2654
|
+
|
|
2655
|
+
|
|
2656
|
+
@mcp.tool()
|
|
2657
|
+
async def clear_name_resolution_cache(show_stats: bool = True) -> str:
|
|
2658
|
+
"""Clear global name→ID resolution caches for workspaces and lakehouses (ADMIN).
|
|
2659
|
+
|
|
2660
|
+
This clears the permanent name→ID mapping caches. Use this if:
|
|
2661
|
+
- A workspace was renamed or deleted/recreated with the same name
|
|
2662
|
+
- A lakehouse was renamed or deleted/recreated with the same name
|
|
2663
|
+
- You suspect stale name→ID mappings are causing issues
|
|
2664
|
+
|
|
2665
|
+
Note: These caches normally never need clearing as name→ID mappings are permanent.
|
|
2666
|
+
|
|
2667
|
+
Args:
|
|
2668
|
+
show_stats: Show cache statistics before clearing
|
|
2669
|
+
"""
|
|
2670
|
+
try:
|
|
2671
|
+
markdown = "# 🔄 Name Resolution Cache Management\n\n"
|
|
2672
|
+
|
|
2673
|
+
if show_stats:
|
|
2674
|
+
workspace_count = len(_global_workspace_cache)
|
|
2675
|
+
lakehouse_count = len(_global_lakehouse_cache)
|
|
2676
|
+
|
|
2677
|
+
markdown += "## 📊 Current Cache Statistics\n\n"
|
|
2678
|
+
markdown += (
|
|
2679
|
+
f"- 🏢 Workspace name→ID mappings: **{workspace_count}** entries\n"
|
|
2680
|
+
)
|
|
2681
|
+
markdown += (
|
|
2682
|
+
f"- 🏠 Lakehouse name→ID mappings: **{lakehouse_count}** entries\n\n"
|
|
2683
|
+
)
|
|
2684
|
+
|
|
2685
|
+
if workspace_count > 0:
|
|
2686
|
+
markdown += "### Workspace Mappings\n"
|
|
2687
|
+
for name, workspace_id in list(_global_workspace_cache.items())[
|
|
2688
|
+
:10
|
|
2689
|
+
]: # Show first 10
|
|
2690
|
+
display_name = name if len(name) <= 30 else f"{name[:27]}..."
|
|
2691
|
+
display_id = (
|
|
2692
|
+
workspace_id
|
|
2693
|
+
if len(workspace_id) <= 20
|
|
2694
|
+
else f"{workspace_id[:17]}..."
|
|
2695
|
+
)
|
|
2696
|
+
markdown += f"- `{display_name}` → `{display_id}`\n"
|
|
2697
|
+
if workspace_count > 10:
|
|
2698
|
+
markdown += f"- ... and {workspace_count - 10} more\n"
|
|
2699
|
+
markdown += "\n"
|
|
2700
|
+
|
|
2701
|
+
if lakehouse_count > 0:
|
|
2702
|
+
markdown += "### Lakehouse Mappings\n"
|
|
2703
|
+
for cache_key, lakehouse_id in list(_global_lakehouse_cache.items())[
|
|
2704
|
+
:10
|
|
2705
|
+
]: # Show first 10
|
|
2706
|
+
display_key = (
|
|
2707
|
+
cache_key if len(cache_key) <= 40 else f"{cache_key[:37]}..."
|
|
2708
|
+
)
|
|
2709
|
+
display_id = (
|
|
2710
|
+
lakehouse_id
|
|
2711
|
+
if len(lakehouse_id) <= 20
|
|
2712
|
+
else f"{lakehouse_id[:17]}..."
|
|
2713
|
+
)
|
|
2714
|
+
markdown += f"- `{display_key}` → `{display_id}`\n"
|
|
2715
|
+
if lakehouse_count > 10:
|
|
2716
|
+
markdown += f"- ... and {lakehouse_count - 10} more\n"
|
|
2717
|
+
markdown += "\n"
|
|
2718
|
+
|
|
2719
|
+
# Clear the caches
|
|
2720
|
+
workspace_cleared = len(_global_workspace_cache)
|
|
2721
|
+
lakehouse_cleared = len(_global_lakehouse_cache)
|
|
2722
|
+
|
|
2723
|
+
_global_workspace_cache.clear()
|
|
2724
|
+
_global_lakehouse_cache.clear()
|
|
2725
|
+
|
|
2726
|
+
markdown += "## ✅ Caches Cleared\n\n"
|
|
2727
|
+
markdown += (
|
|
2728
|
+
f"- 🗑️ Workspace name→ID cache: **{workspace_cleared}** entries cleared\n"
|
|
2729
|
+
)
|
|
2730
|
+
markdown += (
|
|
2731
|
+
f"- 🗑️ Lakehouse name→ID cache: **{lakehouse_cleared}** entries cleared\n\n"
|
|
2732
|
+
)
|
|
2733
|
+
|
|
2734
|
+
markdown += "## 🎯 Result\n\n"
|
|
2735
|
+
if workspace_cleared > 0 or lakehouse_cleared > 0:
|
|
2736
|
+
markdown += "✅ **Name resolution caches cleared successfully!**\n\n"
|
|
2737
|
+
markdown += "Next workspace/lakehouse name lookups will make fresh API calls to resolve names to IDs.\n"
|
|
2738
|
+
else:
|
|
2739
|
+
markdown += "ℹ️ **No cached name mappings found to clear.**\n\n"
|
|
2740
|
+
markdown += "The name resolution caches were already empty.\n"
|
|
2741
|
+
|
|
2742
|
+
return markdown
|
|
2743
|
+
|
|
2744
|
+
except Exception as e:
|
|
2745
|
+
return f"Error clearing name resolution cache: {str(e)}"
|
|
2746
|
+
|
|
2747
|
+
|
|
2748
|
+
def main():
|
|
2749
|
+
"""Main entry point for the MCP server."""
|
|
2750
|
+
# Initialize and run the server
|
|
2751
|
+
mcp.run(transport="stdio")
|
|
2752
|
+
|
|
2753
|
+
|
|
2754
|
+
if __name__ == "__main__":
|
|
2755
|
+
main()
|