From a832d5b0d201249d76c313ec43a227b850600338 Mon Sep 17 00:00:00 2001 From: Srinivas Thatipamula Date: Mon, 11 May 2026 13:11:41 -0700 Subject: [PATCH 01/15] Add 12 new OneLake tools: Security, Shortcuts, Settings - Data Access Security: list, get, create-or-update, delete roles - Shortcuts: list, get, create-or-update, delete, reset-cache - Settings: get, modify-diagnostics, modify-immutability-policy - Refactored DFS ListPath methods to follow ADLS Gen2 Path List API spec - Consolidated path resolution logic via ResolveDirectoryPath helper - Added 233 unit tests (309 total OneLake tests passing) - Updated OneLake and Fabric Server READMEs with new tool documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- servers/Fabric.Mcp.Server/README.md | 30 ++ .../changelog-entries/1778530260238.yaml | 3 + .../changelog-entries/1778530268679.yaml | 3 + tools/Fabric.Mcp.Tools.OneLake/README.md | 254 ++++++++++++-- .../DataAccessRoleCreateOrUpdateCommand.cs | 108 ++++++ .../Security/DataAccessRoleDeleteCommand.cs | 123 +++++++ .../Security/DataAccessRoleGetCommand.cs | 108 ++++++ .../Security/DataAccessRoleListCommand.cs | 104 ++++++ .../Settings/DiagnosticsModifyCommand.cs | 89 +++++ .../ImmutabilityPolicyModifyCommand.cs | 88 +++++ .../Commands/Settings/SettingsGetCommand.cs | 86 +++++ .../Shortcut/ShortcutCreateOrUpdateCommand.cs | 108 ++++++ .../Shortcut/ShortcutDeleteCommand.cs | 125 +++++++ .../Commands/Shortcut/ShortcutGetCommand.cs | 105 ++++++ .../Commands/Shortcut/ShortcutListCommand.cs | 103 ++++++ .../Shortcut/ShortcutResetCacheCommand.cs | 117 +++++++ .../src/FabricOneLakeSetup.cs | 39 +++ .../src/Models/DataAccessRoleModels.cs | 99 ++++++ .../src/Models/OneLakeJsonContext.cs | 34 ++ .../src/Models/SettingsModels.cs | 75 +++++ .../src/Models/ShortcutModels.cs | 186 +++++++++++ .../DataAccessRoleCreateOrUpdateOptions.cs | 15 + .../Options/DataAccessRoleDeleteOptions.cs | 15 + .../src/Options/DataAccessRoleGetOptions.cs | 15 + .../src/Options/DataAccessRoleListOptions.cs | 14 + .../src/Options/DiagnosticsModifyOptions.cs | 13 + .../src/Options/FabricOptionDefinitions.cs | 66 ++++ .../ImmutabilityPolicyModifyOptions.cs | 13 + .../src/Options/SettingsGetOptions.cs | 12 + .../Options/ShortcutCreateOrUpdateOptions.cs | 16 + .../src/Options/ShortcutDeleteOptions.cs | 16 + .../src/Options/ShortcutGetOptions.cs | 16 + .../src/Options/ShortcutListOptions.cs | 15 + .../src/Options/ShortcutResetCacheOptions.cs | 14 + .../src/Services/IOneLakeService.cs | 18 + .../src/Services/OneLakeService.cs | 312 +++++++++++------- ...ataAccessRoleCreateOrUpdateCommandTests.cs | 55 +++ .../DataAccessRoleDeleteCommandTests.cs | 55 +++ .../Security/DataAccessRoleGetCommandTests.cs | 55 +++ .../DataAccessRoleListCommandTests.cs | 55 +++ .../Settings/DiagnosticsModifyCommandTests.cs | 55 +++ .../ImmutabilityPolicyModifyCommandTests.cs | 55 +++ .../Settings/SettingsGetCommandTests.cs | 55 +++ .../ShortcutCreateOrUpdateCommandTests.cs | 55 +++ .../Shortcut/ShortcutDeleteCommandTests.cs | 55 +++ .../Shortcut/ShortcutGetCommandTests.cs | 55 +++ .../Shortcut/ShortcutListCommandTests.cs | 55 +++ .../ShortcutResetCacheCommandTests.cs | 55 +++ 48 files changed, 3074 insertions(+), 143 deletions(-) create mode 100644 servers/Fabric.Mcp.Server/changelog-entries/1778530260238.yaml create mode 100644 servers/Fabric.Mcp.Server/changelog-entries/1778530268679.yaml create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleDeleteCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOrUpdateCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutDeleteCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutGetCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutResetCacheCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Models/DataAccessRoleModels.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Models/SettingsModels.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Models/ShortcutModels.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleCreateOrUpdateOptions.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleDeleteOptions.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleGetOptions.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleListOptions.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Options/DiagnosticsModifyOptions.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Options/ImmutabilityPolicyModifyOptions.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Options/SettingsGetOptions.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOrUpdateOptions.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutDeleteOptions.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutGetOptions.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutListOptions.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutResetCacheOptions.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleCreateOrUpdateCommandTests.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleDeleteCommandTests.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleGetCommandTests.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleListCommandTests.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/DiagnosticsModifyCommandTests.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/SettingsGetCommandTests.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateOrUpdateCommandTests.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutDeleteCommandTests.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutGetCommandTests.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutListCommandTests.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutResetCacheCommandTests.cs diff --git a/servers/Fabric.Mcp.Server/README.md b/servers/Fabric.Mcp.Server/README.md index a414ea4274..ee52908925 100644 --- a/servers/Fabric.Mcp.Server/README.md +++ b/servers/Fabric.Mcp.Server/README.md @@ -30,6 +30,9 @@ A local-first Model Context Protocol (MCP) server that provides AI agents with c - [Available Tools](#available-tools) - [API Documentation & Best Practices](#api-documentation--best-practices) - [OneLake Data Operations](#onelake-data-operations) + - [OneLake Security — Data Access Roles](#onelake-security--data-access-roles) + - [OneLake Shortcuts](#onelake-shortcuts) + - [OneLake Settings](#onelake-settings) - [Core Fabric Operations](#core-fabric-operations) - [Data Factory Operations](#data-factory-operations) - [Support and Reference](#support-and-reference) @@ -252,6 +255,33 @@ The Fabric MCP Server exposes tools organized into three categories: | `onelake_list_tables` | Lists tables published within a namespace. | | `onelake_get_table` | Retrieves the definition for a specific table. | +### OneLake Security — Data Access Roles + +| Tool Name | Description | +|-----------|-------------| +| `onelake_list_data_access_roles` | Lists all data access roles defined on a single item. | +| `onelake_get_data_access_role` | Gets the full definition of a single data access role (members, permissions, decision rules). | +| `onelake_create_or_update_data_access_role` | Upserts a single data access role on a single item. | +| `onelake_delete_data_access_role` | Deletes a single data access role from an item. | + +### OneLake Shortcuts + +| Tool Name | Description | +|-----------|-------------| +| `onelake_list_shortcuts` | Lists shortcuts defined within an item, recursing through subfolders. | +| `onelake_get_shortcut` | Gets the properties of a single shortcut. | +| `onelake_create_or_update_shortcuts` | Creates or updates one or more shortcuts in a single call. | +| `onelake_delete_shortcut` | Deletes a single shortcut from an item (preserves destination data). | +| `onelake_reset_shortcut_cache` | Drops cached shortcut reads, forcing re-resolution from destination. | + +### OneLake Settings + +| Tool Name | Description | +|-----------|-------------| +| `onelake_get_settings` | Gets OneLake settings for a workspace (diagnostics + immutability policy). | +| `onelake_modify_diagnostics` | Modifies diagnostic logging configuration at workspace scope. | +| `onelake_modify_immutability_policy` | Modifies the workspace-level OneLake immutability policy. | + ### Core Fabric Operations | Tool Name | Description | diff --git a/servers/Fabric.Mcp.Server/changelog-entries/1778530260238.yaml b/servers/Fabric.Mcp.Server/changelog-entries/1778530260238.yaml new file mode 100644 index 0000000000..81dcf6a9e1 --- /dev/null +++ b/servers/Fabric.Mcp.Server/changelog-entries/1778530260238.yaml @@ -0,0 +1,3 @@ +changes: + - section: "Features Added" + description: "Added 12 new OneLake tools: Data Access Security (list, get, create-or-update, delete roles), Shortcuts (list, get, create-or-update, delete, reset-cache), and Settings (get, modify-diagnostics, modify-immutability-policy)" diff --git a/servers/Fabric.Mcp.Server/changelog-entries/1778530268679.yaml b/servers/Fabric.Mcp.Server/changelog-entries/1778530268679.yaml new file mode 100644 index 0000000000..89f4e16b88 --- /dev/null +++ b/servers/Fabric.Mcp.Server/changelog-entries/1778530268679.yaml @@ -0,0 +1,3 @@ +changes: + - section: "Bugs Fixed" + description: "Refactored OneLake DFS ListPath methods to follow ADLS Gen2 Path List API specification (directory as query parameter)" diff --git a/tools/Fabric.Mcp.Tools.OneLake/README.md b/tools/Fabric.Mcp.Tools.OneLake/README.md index fcdca41afe..35630d4190 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/README.md +++ b/tools/Fabric.Mcp.Tools.OneLake/README.md @@ -7,18 +7,24 @@ Microsoft Fabric OneLake MCP (Model Context Protocol) Tools - Manage and interac OneLake is Microsoft Fabric's built-in data lake that provides unified storage for all analytics workloads. This MCP tool provides operations for working with OneLake resources within your Fabric tenant, enabling AI agents to: - Manage OneLake folders and files -- Configure data access and permissions -- Monitor OneLake storage usage and performance -- Integrate with other Fabric workloads through OneLake +- Browse items, tables and namespaces +- Configure OneLake data access security (role-based) +- Create, list and manage shortcuts (including bulk + cache reset) +- Read and modify workspace-level OneLake settings (diagnostics, immutability) **Features:** -- 19 comprehensive OneLake commands with full MCP integration +- 31 comprehensive OneLake commands with full MCP integration - Complete coverage for OneLake table APIs: configuration, namespace discovery, and table metadata -- Friendly-name support for workspaces and items across data-plane commands ( `item-create` currently requires GUID IDs ) +- Data access security management: list, get, create/update, and delete roles +- Shortcut management: list, get, create/update, delete, and cache reset +- Workspace-level settings: diagnostics and immutability policy configuration +- Friendly-name support for workspaces and items across all commands - Robust error handling and authentication -- Production-ready with 100% test coverage (132 tests) +- Production-ready with 100% test coverage (309 tests) - Clean, focused API design optimized for AI agent interactions +> **Note:** Item creation has moved to the [Fabric.Mcp.Tools.Core](../Fabric.Mcp.Tools.Core) toolset as `core_create_item`. See [Core tools](../Fabric.Mcp.Tools.Core) for item creation operations. + ## Prerequisites - Microsoft Fabric workspace with OneLake enabled @@ -41,12 +47,14 @@ The OneLake MCP tools are configured to use the Microsoft Fabric production envi ``` OneLake Data Plane: https://api.onelake.fabric.microsoft.com -OneLake DFS API: https://onelake.dfs.fabric.microsoft.com -OneLake Blob API: https://onelake.blob.fabric.microsoft.com -OneLake Table API: https://onelake.table.fabric.microsoft.com -Fabric API: https://api.fabric.microsoft.com/v1 +OneLake DFS API: https://onelake.dfs.fabric.microsoft.com +OneLake Blob API: https://onelake.blob.fabric.microsoft.com +OneLake Table API: https://onelake.table.fabric.microsoft.com +Fabric Core API: https://api.fabric.microsoft.com/v1 ``` +> **Endpoint routing:** Security, shortcut and settings tools call the **Fabric Core API** (`api.fabric.microsoft.com`). All other tools target the OneLake data plane / DFS / Blob / Table endpoints. + ### Getting Started Simply use the commands without any environment configuration: @@ -90,7 +98,7 @@ You can verify which environment you're targeting by checking the endpoints in t ### Workspace and Item Identifiers -All commands except `item create` accept either GUID identifiers or friendly names via the `--workspace` and `--item` options. The existing `--workspace-id` and `--item-id` switches remain available for scripts that already depend on them. Friendly-name inputs are sent directly to the OneLake APIs without local GUID resolution; when using names, specify the item as `.` (for example, `SalesLakehouse.lakehouse`). `item create` currently requires the GUID-based `--workspace-id` option. Table-based commands additionally accept schema identifiers through `--namespace` or its alias `--schema`. +All commands accept either GUID identifiers or friendly names via the `--workspace` and `--item` options. The existing `--workspace-id` and `--item-id` switches remain available for scripts that already depend on them. Friendly-name inputs are sent directly to the OneLake APIs without local GUID resolution; when using names, specify the item as `.` (for example, `SalesLakehouse.lakehouse`). Table-based commands additionally accept schema identifiers through `--namespace` or its alias `--schema`. ```bash dotnet run -- onelake file list --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" --path "Files" @@ -172,18 +180,7 @@ dotnet run -- onelake item list-data --workspace-id "47242da5-ff3b-46fb-a94f-977 #### Create Item -Creates a new item (Lakehouse, Notebook, etc.) in a Microsoft Fabric workspace using the Fabric API. - -```bash -dotnet run -- onelake item create --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --display-name "NewLakehouse" --type "Lakehouse" -``` - -> **Note:** `item create` currently requires the GUID-based `--workspace-id` switch; friendly workspace names are not supported for this command yet. - -**Parameters:** -- `--workspace-id`: The ID of the Microsoft Fabric workspace -- `--display-name`: Display name for the new item -- `--type`: Type of item to create (e.g., Lakehouse, Notebook) +> **Moved:** The `item create` command has been moved to [Fabric.Mcp.Tools.Core](../Fabric.Mcp.Tools.Core) as `core_create_item`. Use the Core toolset for creating new items (Lakehouse, Notebook, etc.) in a Microsoft Fabric workspace. ### File Operations @@ -629,6 +626,172 @@ dotnet run -- onelake table get --workspace-id "47242da5-ff3b-46fb-a94f-977909b7 - `--namespace`/`--schema`: Namespace (schema) name - `--table`: Table name to retrieve +### Security — Data Access Roles + +These tools manage role-based data access policies on OneLake items (Lakehouse / Warehouse). All security tools call the **Fabric Core API** and require the caller to be a workspace Admin or Member. + +#### List Data Access Roles + +Lists all data access roles defined on a single item. + +```bash +dotnet run -- onelake security list --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" +``` + +**Parameters:** +- `--workspace`/`--workspace-id`: Workspace identifier +- `--item`/`--item-id`: Item identifier + +#### Get Data Access Role + +Gets the full definition of a single data access role — members, permissions, decision rules. + +```bash +dotnet run -- onelake security get --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" --role-name "DataAnalysts" +``` + +**Parameters:** +- `--workspace`/`--workspace-id`: Workspace identifier +- `--item`/`--item-id`: Item identifier +- `--role-name`: Name of the data access role to retrieve + +#### Create or Update Data Access Role + +Upserts a single data access role on a single item. Scoped to one role per call — does not affect other roles. + +```bash +dotnet run -- onelake security create-or-update --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" --role-name "DataAnalysts" --role-definition '{"members":{"fabricItemMembers":[{"itemAccessType":"ReadAll"}]},"decisionRules":[{"effect":"Permit","permission":[{"attributeName":"Path","attributeValueIncludedIn":["Tables/*"]}]}]}' +``` + +**Parameters:** +- `--workspace`/`--workspace-id`: Workspace identifier +- `--item`/`--item-id`: Item identifier +- `--role-name`: Name of the data access role +- `--role-definition`: JSON definition of the role (members + decision rules) + +#### Delete Data Access Role + +Deletes a single data access role from a single item. Destructive — principals that gained access only via this role lose it. + +```bash +dotnet run -- onelake security delete --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" --role-name "TempRole" +``` + +**Parameters:** +- `--workspace`/`--workspace-id`: Workspace identifier +- `--item`/`--item-id`: Item identifier +- `--role-name`: Name of the data access role to delete + +### Shortcut Operations + +Shortcuts are references to data stored in external or internal locations (ADLS Gen2, S3, GCS, OneLake, Dataverse, etc.). These tools call the **Fabric Core API**. + +#### List Shortcuts + +Lists shortcuts defined within an item, recursing through subfolders. + +```bash +dotnet run -- onelake shortcut list --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" +``` + +**Parameters:** +- `--workspace`/`--workspace-id`: Workspace identifier +- `--item`/`--item-id`: Item identifier +- `--parent-path`: (Optional) Parent path to scope the listing + +#### Get Shortcut + +Gets the properties of a single shortcut (name, path, target, configuration). + +```bash +dotnet run -- onelake shortcut get --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" --shortcut-name "ExternalData" --shortcut-path "Tables/ExternalData" +``` + +**Parameters:** +- `--workspace`/`--workspace-id`: Workspace identifier +- `--item`/`--item-id`: Item identifier +- `--shortcut-name`: Name of the shortcut +- `--shortcut-path`: Path of the shortcut within the item + +#### Create or Update Shortcuts + +Creates one or more shortcuts in a single call. Pass `--create-or-overwrite` to upsert (default fails on conflict). + +```bash +dotnet run -- onelake shortcut create-or-update --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" --shortcuts '[{"name":"ExternalData","path":"Tables/ExternalData","target":{"adlsGen2":{"location":"https://storageaccount.dfs.core.windows.net","subpath":"/container/path"}}}]' +``` + +**Parameters:** +- `--workspace`/`--workspace-id`: Workspace identifier +- `--item`/`--item-id`: Item identifier +- `--shortcuts`: JSON array of shortcut definitions +- `--create-or-overwrite`: (Optional) If set, overwrites existing shortcuts + +#### Delete Shortcut + +Deletes a single shortcut from an item. The destination data is preserved — only the shortcut reference is removed. + +```bash +dotnet run -- onelake shortcut delete --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" --shortcut-name "ExternalData" --shortcut-path "Tables/ExternalData" +``` + +**Parameters:** +- `--workspace`/`--workspace-id`: Workspace identifier +- `--item`/`--item-id`: Item identifier +- `--shortcut-name`: Name of the shortcut to delete +- `--shortcut-path`: Path of the shortcut + +#### Reset Shortcut Cache + +Drops cached shortcut reads for an item, forcing the next read to re-resolve from the destination. Use sparingly — primarily for debugging stale-cache issues. + +```bash +dotnet run -- onelake shortcut reset-cache --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" +``` + +**Parameters:** +- `--workspace`/`--workspace-id`: Workspace identifier +- `--item`/`--item-id`: Item identifier + +### Settings Operations + +Workspace-level OneLake settings for diagnostics and immutability policies. These tools call the **Fabric Core API**. + +#### Get Settings + +Gets the OneLake settings for a workspace — diagnostics configuration and immutability policy. + +```bash +dotnet run -- onelake settings get --workspace "Analytics Workspace" +``` + +**Parameters:** +- `--workspace`/`--workspace-id`: Workspace identifier + +#### Modify Diagnostics + +Modifies the diagnostic logging configuration for OneLake at the workspace scope. Replaces the existing diagnostics block; fetch with `get settings` first if you want to merge. + +```bash +dotnet run -- onelake settings modify-diagnostics --workspace "Analytics Workspace" --diagnostics-config '{"logAnalyticsWorkspaceId":"","level":"Verbose"}' +``` + +**Parameters:** +- `--workspace`/`--workspace-id`: Workspace identifier +- `--diagnostics-config`: JSON configuration for diagnostic settings + +#### Modify Immutability Policy + +Modifies the workspace-level OneLake immutability policy. **Warning:** Once enabled, immutability cannot be disabled — confirm with the user before applying. + +```bash +dotnet run -- onelake settings modify-immutability --workspace "Analytics Workspace" --immutability-policy '{"state":"Enabled"}' +``` + +**Parameters:** +- `--workspace`/`--workspace-id`: Workspace identifier +- `--immutability-policy`: JSON immutability policy configuration + ## Quick Reference - fabmcp.exe Commands For users with the compiled `fabmcp.exe` executable, here are ready-to-use commands: @@ -699,6 +862,45 @@ fabmcp.exe onelake table list --workspace "Analytics Workspace" --item "SalesLak fabmcp.exe onelake table get --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" --schema "sales" --table "transactions" ``` +### Security Operations +```cmd +# List data access roles on an item +fabmcp.exe onelake security list --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --item "0e67ed13-2bb6-49be-9c87-a1105a4ea342" + +# Get a specific role definition +fabmcp.exe onelake security get --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --item "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --role-name "DataAnalysts" + +# Delete a data access role +fabmcp.exe onelake security delete --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --item "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --role-name "TempRole" +``` + +### Shortcut Operations +```cmd +# List shortcuts on an item +fabmcp.exe onelake shortcut list --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --item "0e67ed13-2bb6-49be-9c87-a1105a4ea342" + +# Get a specific shortcut +fabmcp.exe onelake shortcut get --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --item "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --shortcut-name "ExternalData" --shortcut-path "Tables/ExternalData" + +# Delete a shortcut +fabmcp.exe onelake shortcut delete --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --item "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --shortcut-name "ExternalData" --shortcut-path "Tables/ExternalData" + +# Reset shortcut cache +fabmcp.exe onelake shortcut reset-cache --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --item "0e67ed13-2bb6-49be-9c87-a1105a4ea342" +``` + +### Settings Operations +```cmd +# Get workspace OneLake settings +fabmcp.exe onelake settings get --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" + +# Modify diagnostics configuration +fabmcp.exe onelake settings modify-diagnostics --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --diagnostics-config '{"logAnalyticsWorkspaceId":"","level":"Verbose"}' + +# Modify immutability policy +fabmcp.exe onelake settings modify-immutability --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --immutability-policy '{"state":"Enabled"}' +``` + **Note:** Replace the workspace identifier (`47242da5-ff3b-46fb-a94f-977909b773d5`) and item identifier (`0e67ed13-2bb6-49be-9c87-a1105a4ea342`) with your actual Fabric workspace and item values (names or IDs). ## Common Usage Patterns @@ -831,9 +1033,9 @@ This tool is part of the Microsoft MCP (Model Context Protocol) project. Please The OneLake MCP Tools include a comprehensive test suite with 100% command coverage: #### Test Structure -- **Total Tests**: 76 tests (all passing) -- **Command Tests**: 70 tests covering all 11 OneLake MCP commands -- **Service Architecture Tests**: 6 tests demonstrating testable patterns with dependency injection +- **Total Tests**: 309 tests (all passing) +- **Command Tests**: Covering all OneLake MCP commands including security, shortcuts, and settings +- **Service Architecture Tests**: Demonstrating testable patterns with dependency injection #### Running Tests ```bash diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs new file mode 100644 index 0000000000..3cb04c5c52 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Security; + +[CommandMetadata( + Id = "a1b2c3d4-1001-4000-8000-000000000003", + Name = "create_or_update_data_access_role", + Title = "Create or Update OneLake Data Access Role", + Description = """ + Upsert a single data access role on a single item. Scoped to one role on + one item per call — does not affect other roles on the item or any roles + on other items, so it is safe to call in a loop when multiple roles or + multiple items need changing. There is no bulk variant: the underlying + PUT-all API was intentionally not exposed because partial reads would + silently delete roles. Caller must be a workspace Admin or Member on the + item's workspace. Requires OneLake.ReadWrite.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = false, + Secret = false)] +public sealed class DataAccessRoleCreateOrUpdateCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); + command.Options.Add(FabricOptionDefinitions.RoleDefinition.AsRequired()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + + if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) + { + result.AddError("Item identifier is required. Provide --item or --item-id."); + } + }); + } + + protected override DataAccessRoleCreateOrUpdateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); + options.RoleDefinition = parseResult.GetValueOrDefault(FabricOptionDefinitions.RoleDefinitionName); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + try + { + var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) + ? options.WorkspaceId + : options.Workspace; + + var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) + ? options.ItemId + : options.Item; + + var result = await _oneLakeService.CreateOrUpdateDataAccessRoleAsync(workspaceIdentifier!, itemIdentifier!, options.RoleDefinition!, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.DataAccessRole); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating/updating data access role. Workspace: {Workspace}, Item: {Item}.", + options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleDeleteCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleDeleteCommand.cs new file mode 100644 index 0000000000..22177680f1 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleDeleteCommand.cs @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Security; + +[CommandMetadata( + Id = "a1b2c3d4-1001-4000-8000-000000000004", + Name = "delete_data_access_role", + Title = "Delete OneLake Data Access Role", + Description = """ + Delete a single data access role from a single item. Scoped to one role + on one item per call. Destructive — principals that gained access only + via this role lose it on this item. Does not affect roles on other items. + Caller must be a workspace Admin or Member on the item's workspace. + Requires OneLake.ReadWrite.All. + """, + Destructive = true, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = false, + Secret = false)] +public sealed class DataAccessRoleDeleteCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); + command.Options.Add(FabricOptionDefinitions.RoleName.AsRequired()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + + if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) + { + result.AddError("Item identifier is required. Provide --item or --item-id."); + } + }); + } + + protected override DataAccessRoleDeleteOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); + options.RoleName = parseResult.GetValueOrDefault(FabricOptionDefinitions.RoleNameName); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + try + { + var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) + ? options.WorkspaceId + : options.Workspace; + + var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) + ? options.ItemId + : options.Item; + + await _oneLakeService.DeleteDataAccessRoleAsync(workspaceIdentifier!, itemIdentifier!, options.RoleName!, cancellationToken); + var result = new DataAccessRoleDeleteCommandResult(options.RoleName!, "Data access role deleted successfully."); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.DataAccessRoleDeleteCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting data access role. Workspace: {Workspace}, Item: {Item}, Role: {Role}.", + options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item, options.RoleName); + HandleException(context, ex); + } + + return context.Response; + } + + public sealed class DataAccessRoleDeleteCommandResult + { + public string RoleName { get; init; } = string.Empty; + public string Message { get; init; } = string.Empty; + + public DataAccessRoleDeleteCommandResult() + { + } + + public DataAccessRoleDeleteCommandResult(string roleName, string message) + { + RoleName = roleName; + Message = message; + } + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs new file mode 100644 index 0000000000..5fa7e32100 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Security; + +[CommandMetadata( + Id = "a1b2c3d4-1001-4000-8000-000000000002", + Name = "get_data_access_role", + Title = "Get OneLake Data Access Role", + Description = """ + Get the full definition of a single data access role on a single item — + members, permissions, decision rules. Scoped to one role on one item per + call. Use after onelake_list_data_access_roles once you know which role + you need on which item. Distinct from onelake_get_principal_access, + which returns the effective (resolved) access for a given principal + across all roles on an item. Caller must be a workspace Admin or Member + on the item's workspace. Requires OneLake.Read.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = true, + Secret = false)] +public sealed class DataAccessRoleGetCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); + command.Options.Add(FabricOptionDefinitions.RoleName.AsRequired()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + + if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) + { + result.AddError("Item identifier is required. Provide --item or --item-id."); + } + }); + } + + protected override DataAccessRoleGetOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); + options.RoleName = parseResult.GetValueOrDefault(FabricOptionDefinitions.RoleNameName); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + try + { + var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) + ? options.WorkspaceId + : options.Workspace; + + var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) + ? options.ItemId + : options.Item; + + var result = await _oneLakeService.GetDataAccessRoleAsync(workspaceIdentifier!, itemIdentifier!, options.RoleName!, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.DataAccessRole); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting data access role. Workspace: {Workspace}, Item: {Item}, Role: {Role}.", + options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item, options.RoleName); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs new file mode 100644 index 0000000000..a25bc85b08 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Security; + +[CommandMetadata( + Id = "a1b2c3d4-1001-4000-8000-000000000001", + Name = "list_data_access_roles", + Title = "List OneLake Data Access Roles", + Description = """ + List all data access roles defined on a single item (Lakehouse / Warehouse) — + the role-based policies that gate Tables/Files access for that item. Scoped + to one item per call; to inspect roles across multiple items, call once per + item. For looking up a specific role by name, fetch the list and pick by + name; there is no server-side search. Caller must be a workspace Admin + or Member on the item's workspace. Requires OneLake.Read.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = true, + Secret = false)] +public sealed class DataAccessRoleListCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + + if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) + { + result.AddError("Item identifier is required. Provide --item or --item-id."); + } + }); + } + + protected override DataAccessRoleListOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + try + { + var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) + ? options.WorkspaceId + : options.Workspace; + + var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) + ? options.ItemId + : options.Item; + + var result = await _oneLakeService.ListDataAccessRolesAsync(workspaceIdentifier!, itemIdentifier!, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.DataAccessRoleListResponse); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error listing data access roles. Workspace: {Workspace}, Item: {Item}.", options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs new file mode 100644 index 0000000000..9c55484165 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Settings; + +[CommandMetadata( + Id = "a1b2c3d4-3001-4000-8000-000000000002", + Name = "modify_diagnostics", + Title = "Modify OneLake Diagnostics", + Description = """ + Modify the diagnostic logging configuration for OneLake at the workspace + scope. Replaces the existing diagnostics block; fetch with + onelake_get_settings first if you want to merge. Requires + OneLake.ReadWrite.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = false, + Secret = false)] +public sealed class DiagnosticsModifyCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); + command.Options.Add(FabricOptionDefinitions.DiagnosticsConfig.AsRequired()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + }); + } + + protected override DiagnosticsModifyOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + options.DiagnosticsConfig = parseResult.GetValueOrDefault(FabricOptionDefinitions.DiagnosticsConfigName); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + try + { + var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) + ? options.WorkspaceId + : options.Workspace; + + var result = await _oneLakeService.ModifyDiagnosticsAsync(workspaceIdentifier!, options.DiagnosticsConfig!, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeSettings); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error modifying OneLake diagnostics. Workspace: {Workspace}.", options.WorkspaceId ?? options.Workspace); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs new file mode 100644 index 0000000000..94152b5b93 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Settings; + +[CommandMetadata( + Id = "a1b2c3d4-3001-4000-8000-000000000003", + Name = "modify_immutability_policy", + Title = "Modify OneLake Immutability Policy", + Description = """ + Modify the workspace-level OneLake immutability policy. Once enabled, + immutability cannot be disabled — confirm with the user before applying. + Requires OneLake.ReadWrite.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = false, + Secret = false)] +public sealed class ImmutabilityPolicyModifyCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); + command.Options.Add(FabricOptionDefinitions.ImmutabilityPolicyConfig.AsRequired()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + }); + } + + protected override ImmutabilityPolicyModifyOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + options.ImmutabilityPolicyConfig = parseResult.GetValueOrDefault(FabricOptionDefinitions.ImmutabilityPolicyConfigName); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + try + { + var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) + ? options.WorkspaceId + : options.Workspace; + + var result = await _oneLakeService.ModifyImmutabilityPolicyAsync(workspaceIdentifier!, options.ImmutabilityPolicyConfig!, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeSettings); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error modifying OneLake immutability policy. Workspace: {Workspace}.", options.WorkspaceId ?? options.Workspace); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs new file mode 100644 index 0000000000..45d607b7e6 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Settings; + +[CommandMetadata( + Id = "a1b2c3d4-3001-4000-8000-000000000001", + Name = "get_settings", + Title = "Get OneLake Settings", + Description = """ + Get the OneLake settings for a workspace — diagnostics configuration and + immutability policy. Read-only; for changes use onelake_modify_diagnostics + or onelake_modify_immutability_policy. Requires OneLake.Read.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = true, + Secret = false)] +public sealed class SettingsGetCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + }); + } + + protected override SettingsGetOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + try + { + var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) + ? options.WorkspaceId + : options.Workspace; + + var result = await _oneLakeService.GetSettingsAsync(workspaceIdentifier!, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeSettings); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting OneLake settings. Workspace: {Workspace}.", options.WorkspaceId ?? options.Workspace); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOrUpdateCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOrUpdateCommand.cs new file mode 100644 index 0000000000..a04c41ac45 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOrUpdateCommand.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; + +[CommandMetadata( + Id = "a1b2c3d4-2001-4000-8000-000000000003", + Name = "create_or_update_shortcuts", + Title = "Create or Update OneLake Shortcuts", + Description = """ + Create one or more shortcuts in a single call (the underlying API is + bulk-only — there is no separate single-shortcut create). By default, + fails if any shortcut already exists; pass createOrOverwrite=true to + upsert. Use this for both initial creation and updates. Requires + OneLake.ReadWrite.All. + """, + Destructive = false, + Idempotent = false, + LocalRequired = false, + OpenWorld = false, + ReadOnly = false, + Secret = false)] +public sealed class ShortcutCreateOrUpdateCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); + command.Options.Add(FabricOptionDefinitions.ShortcutsDefinition.AsRequired()); + command.Options.Add(FabricOptionDefinitions.CreateOrOverwrite.AsOptional()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + + if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) + { + result.AddError("Item identifier is required. Provide --item or --item-id."); + } + }); + } + + protected override ShortcutCreateOrUpdateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); + options.ShortcutsDefinition = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutsDefinitionName); + options.CreateOrOverwrite = parseResult.GetValueOrDefault(FabricOptionDefinitions.CreateOrOverwriteName); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + try + { + var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) + ? options.WorkspaceId + : options.Workspace; + + var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) + ? options.ItemId + : options.Item; + + var result = await _oneLakeService.CreateOrUpdateShortcutsAsync(workspaceIdentifier!, itemIdentifier!, options.ShortcutsDefinition!, options.CreateOrOverwrite, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.ShortcutCreateOrUpdateResponse); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating/updating shortcuts. Workspace: {Workspace}, Item: {Item}.", + options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutDeleteCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutDeleteCommand.cs new file mode 100644 index 0000000000..eb9ec3de93 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutDeleteCommand.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; + +[CommandMetadata( + Id = "a1b2c3d4-2001-4000-8000-000000000004", + Name = "delete_shortcut", + Title = "Delete OneLake Shortcut", + Description = """ + Delete a single shortcut from an item. Destructive but the destination + data is preserved — only the shortcut reference is removed. Requires + OneLake.ReadWrite.All. + """, + Destructive = true, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = false, + Secret = false)] +public sealed class ShortcutDeleteCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); + command.Options.Add(FabricOptionDefinitions.ShortcutPath.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutName.AsRequired()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + + if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) + { + result.AddError("Item identifier is required. Provide --item or --item-id."); + } + }); + } + + protected override ShortcutDeleteOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); + options.ShortcutPath = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPathName); + options.ShortcutName = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutNameName); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + try + { + var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) + ? options.WorkspaceId + : options.Workspace; + + var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) + ? options.ItemId + : options.Item; + + await _oneLakeService.DeleteShortcutAsync(workspaceIdentifier!, itemIdentifier!, options.ShortcutPath!, options.ShortcutName!, cancellationToken); + var result = new ShortcutDeleteCommandResult(options.ShortcutPath!, options.ShortcutName!, "Shortcut deleted successfully."); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.ShortcutDeleteCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting shortcut. Workspace: {Workspace}, Item: {Item}, Path: {Path}, Name: {Name}.", + options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item, options.ShortcutPath, options.ShortcutName); + HandleException(context, ex); + } + + return context.Response; + } + + public sealed class ShortcutDeleteCommandResult + { + public string ShortcutPath { get; init; } = string.Empty; + public string ShortcutName { get; init; } = string.Empty; + public string Message { get; init; } = string.Empty; + + public ShortcutDeleteCommandResult() + { + } + + public ShortcutDeleteCommandResult(string shortcutPath, string shortcutName, string message) + { + ShortcutPath = shortcutPath; + ShortcutName = shortcutName; + Message = message; + } + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutGetCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutGetCommand.cs new file mode 100644 index 0000000000..f83864fb9c --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutGetCommand.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; + +[CommandMetadata( + Id = "a1b2c3d4-2001-4000-8000-000000000002", + Name = "get_shortcut", + Title = "Get OneLake Shortcut", + Description = """ + Get the properties of a single shortcut (name, path, target, + configuration). Requires OneLake.Read.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = true, + Secret = false)] +public sealed class ShortcutGetCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); + command.Options.Add(FabricOptionDefinitions.ShortcutPath.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutName.AsRequired()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + + if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) + { + result.AddError("Item identifier is required. Provide --item or --item-id."); + } + }); + } + + protected override ShortcutGetOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); + options.ShortcutPath = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPathName); + options.ShortcutName = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutNameName); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + try + { + var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) + ? options.WorkspaceId + : options.Workspace; + + var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) + ? options.ItemId + : options.Item; + + var result = await _oneLakeService.GetShortcutAsync(workspaceIdentifier!, itemIdentifier!, options.ShortcutPath!, options.ShortcutName!, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting shortcut. Workspace: {Workspace}, Item: {Item}, Path: {Path}, Name: {Name}.", + options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item, options.ShortcutPath, options.ShortcutName); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs new file mode 100644 index 0000000000..72e7ac5c92 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; + +[CommandMetadata( + Id = "a1b2c3d4-2001-4000-8000-000000000001", + Name = "list_shortcuts", + Title = "List OneLake Shortcuts", + Description = """ + List shortcuts defined within an item, recursing through subfolders. + Returns each shortcut's path and target. Requires OneLake.Read.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = true, + Secret = false)] +public sealed class ShortcutListCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); + command.Options.Add(FabricOptionDefinitions.ParentPath.AsOptional()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + + if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) + { + result.AddError("Item identifier is required. Provide --item or --item-id."); + } + }); + } + + protected override ShortcutListOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); + options.ParentPath = parseResult.GetValueOrDefault(FabricOptionDefinitions.ParentPathName); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + try + { + var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) + ? options.WorkspaceId + : options.Workspace; + + var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) + ? options.ItemId + : options.Item; + + var result = await _oneLakeService.ListShortcutsAsync(workspaceIdentifier!, itemIdentifier!, options.ParentPath, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.ShortcutListResponse); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error listing shortcuts. Workspace: {Workspace}, Item: {Item}.", + options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutResetCacheCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutResetCacheCommand.cs new file mode 100644 index 0000000000..31fdaa1070 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutResetCacheCommand.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; + +[CommandMetadata( + Id = "a1b2c3d4-2001-4000-8000-000000000005", + Name = "reset_shortcut_cache", + Title = "Reset OneLake Shortcut Cache", + Description = """ + Drop cached shortcut reads for an item, forcing the next read to + re-resolve from the destination. Use sparingly — primarily for debugging + stale-cache issues. Requires OneLake.ReadWrite.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = false, + Secret = false)] +public sealed class ShortcutResetCacheCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + + if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) + { + result.AddError("Item identifier is required. Provide --item or --item-id."); + } + }); + } + + protected override ShortcutResetCacheOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); + options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + try + { + var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) + ? options.WorkspaceId + : options.Workspace; + + var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) + ? options.ItemId + : options.Item; + + await _oneLakeService.ResetShortcutCacheAsync(workspaceIdentifier!, itemIdentifier!, cancellationToken); + var result = new ShortcutResetCacheCommandResult("Shortcut cache reset successfully."); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.ShortcutResetCacheCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error resetting shortcut cache. Workspace: {Workspace}, Item: {Item}.", + options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item); + HandleException(context, ex); + } + + return context.Response; + } + + public sealed class ShortcutResetCacheCommandResult + { + public string Message { get; init; } = string.Empty; + + public ShortcutResetCacheCommandResult() + { + } + + public ShortcutResetCacheCommandResult(string message) + { + Message = message; + } + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/FabricOneLakeSetup.cs b/tools/Fabric.Mcp.Tools.OneLake/src/FabricOneLakeSetup.cs index ccb1f7235d..d2bb1d856f 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/FabricOneLakeSetup.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/FabricOneLakeSetup.cs @@ -3,6 +3,9 @@ using Fabric.Mcp.Tools.OneLake.Commands.File; using Fabric.Mcp.Tools.OneLake.Commands.Item; +using Fabric.Mcp.Tools.OneLake.Commands.Security; +using Fabric.Mcp.Tools.OneLake.Commands.Settings; +using Fabric.Mcp.Tools.OneLake.Commands.Shortcut; using Fabric.Mcp.Tools.OneLake.Commands.Table; using Fabric.Mcp.Tools.OneLake.Commands.Workspace; using Fabric.Mcp.Tools.OneLake.Services; @@ -51,6 +54,24 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + + // Register data access security commands + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Register shortcut commands + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Register settings commands + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); } public CommandGroup RegisterCommands(IServiceProvider serviceProvider) @@ -83,6 +104,24 @@ Microsoft Fabric OneLake Operations - Manage and interact with OneLake data lake fabricOneLake.AddCommand(serviceProvider); fabricOneLake.AddCommand(serviceProvider); + // Register data access security commands + fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); + + // Register shortcut commands + fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); + + // Register settings commands + fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); + return fabricOneLake; } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Models/DataAccessRoleModels.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Models/DataAccessRoleModels.cs new file mode 100644 index 0000000000..3febbb4bd9 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Models/DataAccessRoleModels.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Fabric.Mcp.Tools.OneLake.Models; + +/// +/// Represents a data access role defined on a OneLake item. +/// +public class DataAccessRole +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("decisionRules")] + public List? DecisionRules { get; set; } + + [JsonPropertyName("members")] + public DataAccessRoleMembers? Members { get; set; } +} + +/// +/// Decision rule within a data access role defining access permissions. +/// +public class DecisionRule +{ + [JsonPropertyName("effect")] + public string? Effect { get; set; } + + [JsonPropertyName("permission")] + public string? Permission { get; set; } + + [JsonPropertyName("scope")] + public List? Scope { get; set; } +} + +/// +/// Scope definition for a decision rule. +/// +public class DecisionRuleScope +{ + [JsonPropertyName("attributeName")] + public string? AttributeName { get; set; } + + [JsonPropertyName("attributeValueIncludedIn")] + public List? AttributeValueIncludedIn { get; set; } +} + +/// +/// Members of a data access role. +/// +public class DataAccessRoleMembers +{ + [JsonPropertyName("fabricItemMembers")] + public List? FabricItemMembers { get; set; } + + [JsonPropertyName("microsoftEntraMembers")] + public List? MicrosoftEntraMembers { get; set; } +} + +/// +/// A Fabric item member in a data access role. +/// +public class FabricItemMember +{ + [JsonPropertyName("sourceItemId")] + public string? SourceItemId { get; set; } +} + +/// +/// A Microsoft Entra member in a data access role. +/// +public class MicrosoftEntraMember +{ + [JsonPropertyName("objectId")] + public string? ObjectId { get; set; } + + [JsonPropertyName("tenantId")] + public string? TenantId { get; set; } +} + +/// +/// Response from the List Data Access Roles API. +/// +public class DataAccessRoleListResponse +{ + [JsonPropertyName("value")] + public List? Value { get; set; } + + [JsonPropertyName("continuationToken")] + public string? ContinuationToken { get; set; } + + [JsonPropertyName("continuationUri")] + public string? ContinuationUri { get; set; } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs index 7c336dfddd..77ced0173a 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs @@ -5,6 +5,9 @@ using System.Text.Json.Serialization; using Fabric.Mcp.Tools.OneLake.Commands.File; using Fabric.Mcp.Tools.OneLake.Commands.Item; +using Fabric.Mcp.Tools.OneLake.Commands.Security; +using Fabric.Mcp.Tools.OneLake.Commands.Settings; +using Fabric.Mcp.Tools.OneLake.Commands.Shortcut; using Fabric.Mcp.Tools.OneLake.Commands.Table; using Fabric.Mcp.Tools.OneLake.Commands.Workspace; @@ -62,5 +65,36 @@ namespace Fabric.Mcp.Tools.OneLake.Models; [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] +// Data Access Security types +[JsonSerializable(typeof(DataAccessRole))] +[JsonSerializable(typeof(DataAccessRoleListResponse))] +[JsonSerializable(typeof(DataAccessRoleMembers))] +[JsonSerializable(typeof(DecisionRule))] +[JsonSerializable(typeof(DecisionRuleScope))] +[JsonSerializable(typeof(FabricItemMember))] +[JsonSerializable(typeof(MicrosoftEntraMember))] +[JsonSerializable(typeof(DataAccessRoleDeleteCommand.DataAccessRoleDeleteCommandResult))] +// Shortcut types +[JsonSerializable(typeof(OneLakeShortcut))] +[JsonSerializable(typeof(ShortcutTarget))] +[JsonSerializable(typeof(OneLakeShortcutTarget))] +[JsonSerializable(typeof(AdlsGen2ShortcutTarget))] +[JsonSerializable(typeof(AmazonS3ShortcutTarget))] +[JsonSerializable(typeof(GoogleCloudStorageShortcutTarget))] +[JsonSerializable(typeof(DataverseShortcutTarget))] +[JsonSerializable(typeof(S3CompatibleShortcutTarget))] +[JsonSerializable(typeof(ExternalDataShareShortcutTarget))] +[JsonSerializable(typeof(ShortcutListResponse))] +[JsonSerializable(typeof(ShortcutCreateOrUpdateRequest))] +[JsonSerializable(typeof(ShortcutCreateOrUpdateResponse))] +[JsonSerializable(typeof(ShortcutDeleteCommand.ShortcutDeleteCommandResult))] +[JsonSerializable(typeof(ShortcutResetCacheCommand.ShortcutResetCacheCommandResult))] +// Settings types +[JsonSerializable(typeof(OneLakeSettings))] +[JsonSerializable(typeof(DiagnosticsSettings))] +[JsonSerializable(typeof(DiagnosticsCategory))] +[JsonSerializable(typeof(ImmutabilityPolicySettings))] +[JsonSerializable(typeof(DiagnosticsModifyRequest))] +[JsonSerializable(typeof(ImmutabilityPolicyModifyRequest))] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, WriteIndented = true)] internal partial class OneLakeJsonContext : JsonSerializerContext; diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Models/SettingsModels.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Models/SettingsModels.cs new file mode 100644 index 0000000000..e006b5c1c7 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Models/SettingsModels.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Fabric.Mcp.Tools.OneLake.Models; + +/// +/// OneLake settings for a workspace. +/// +public class OneLakeSettings +{ + [JsonPropertyName("diagnostics")] + public DiagnosticsSettings? Diagnostics { get; set; } + + [JsonPropertyName("immutabilityPolicy")] + public ImmutabilityPolicySettings? ImmutabilityPolicy { get; set; } +} + +/// +/// Diagnostic logging configuration for OneLake. +/// +public class DiagnosticsSettings +{ + [JsonPropertyName("enabled")] + public bool? Enabled { get; set; } + + [JsonPropertyName("logAnalyticsWorkspaceId")] + public string? LogAnalyticsWorkspaceId { get; set; } + + [JsonPropertyName("categories")] + public List? Categories { get; set; } +} + +/// +/// A diagnostic logging category. +/// +public class DiagnosticsCategory +{ + [JsonPropertyName("category")] + public string? Category { get; set; } + + [JsonPropertyName("enabled")] + public bool? Enabled { get; set; } +} + +/// +/// Immutability policy settings for OneLake. +/// +public class ImmutabilityPolicySettings +{ + [JsonPropertyName("state")] + public string? State { get; set; } + + [JsonPropertyName("immutabilityPeriodSinceCreationInDays")] + public int? ImmutabilityPeriodSinceCreationInDays { get; set; } +} + +/// +/// Request body for modifying diagnostics. +/// +public class DiagnosticsModifyRequest +{ + [JsonPropertyName("diagnostics")] + public DiagnosticsSettings? Diagnostics { get; set; } +} + +/// +/// Request body for modifying immutability policy. +/// +public class ImmutabilityPolicyModifyRequest +{ + [JsonPropertyName("immutabilityPolicy")] + public ImmutabilityPolicySettings? ImmutabilityPolicy { get; set; } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Models/ShortcutModels.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Models/ShortcutModels.cs new file mode 100644 index 0000000000..d4cd7f0755 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Models/ShortcutModels.cs @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Fabric.Mcp.Tools.OneLake.Models; + +/// +/// Represents a OneLake shortcut. +/// +public class OneLakeShortcut +{ + [JsonPropertyName("path")] + public string? Path { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("target")] + public ShortcutTarget? Target { get; set; } +} + +/// +/// Target configuration for a shortcut. +/// +public class ShortcutTarget +{ + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("oneLake")] + public OneLakeShortcutTarget? OneLake { get; set; } + + [JsonPropertyName("adlsGen2")] + public AdlsGen2ShortcutTarget? AdlsGen2 { get; set; } + + [JsonPropertyName("amazonS3")] + public AmazonS3ShortcutTarget? AmazonS3 { get; set; } + + [JsonPropertyName("googleCloudStorage")] + public GoogleCloudStorageShortcutTarget? GoogleCloudStorage { get; set; } + + [JsonPropertyName("dataverse")] + public DataverseShortcutTarget? Dataverse { get; set; } + + [JsonPropertyName("s3Compatible")] + public S3CompatibleShortcutTarget? S3Compatible { get; set; } + + [JsonPropertyName("externalDataShare")] + public ExternalDataShareShortcutTarget? ExternalDataShare { get; set; } +} + +/// +/// OneLake shortcut target pointing to another OneLake location. +/// +public class OneLakeShortcutTarget +{ + [JsonPropertyName("workspaceId")] + public string? WorkspaceId { get; set; } + + [JsonPropertyName("itemId")] + public string? ItemId { get; set; } + + [JsonPropertyName("path")] + public string? Path { get; set; } +} + +/// +/// ADLS Gen2 shortcut target. +/// +public class AdlsGen2ShortcutTarget +{ + [JsonPropertyName("location")] + public string? Location { get; set; } + + [JsonPropertyName("subpath")] + public string? Subpath { get; set; } + + [JsonPropertyName("connectionId")] + public string? ConnectionId { get; set; } +} + +/// +/// Amazon S3 shortcut target. +/// +public class AmazonS3ShortcutTarget +{ + [JsonPropertyName("location")] + public string? Location { get; set; } + + [JsonPropertyName("subpath")] + public string? Subpath { get; set; } + + [JsonPropertyName("connectionId")] + public string? ConnectionId { get; set; } +} + +/// +/// Google Cloud Storage shortcut target. +/// +public class GoogleCloudStorageShortcutTarget +{ + [JsonPropertyName("location")] + public string? Location { get; set; } + + [JsonPropertyName("subpath")] + public string? Subpath { get; set; } + + [JsonPropertyName("connectionId")] + public string? ConnectionId { get; set; } +} + +/// +/// Dataverse shortcut target. +/// +public class DataverseShortcutTarget +{ + [JsonPropertyName("environmentDomain")] + public string? EnvironmentDomain { get; set; } + + [JsonPropertyName("deltaLakeFolder")] + public string? DeltaLakeFolder { get; set; } + + [JsonPropertyName("connectionId")] + public string? ConnectionId { get; set; } +} + +/// +/// S3-compatible shortcut target. +/// +public class S3CompatibleShortcutTarget +{ + [JsonPropertyName("location")] + public string? Location { get; set; } + + [JsonPropertyName("subpath")] + public string? Subpath { get; set; } + + [JsonPropertyName("connectionId")] + public string? ConnectionId { get; set; } +} + +/// +/// External data share shortcut target. +/// +public class ExternalDataShareShortcutTarget +{ + [JsonPropertyName("connectionId")] + public string? ConnectionId { get; set; } +} + +/// +/// Response from the List Shortcuts API. +/// +public class ShortcutListResponse +{ + [JsonPropertyName("value")] + public List? Value { get; set; } + + [JsonPropertyName("continuationToken")] + public string? ContinuationToken { get; set; } + + [JsonPropertyName("continuationUri")] + public string? ContinuationUri { get; set; } +} + +/// +/// Request body for the Create Or Update Shortcuts API. +/// +public class ShortcutCreateOrUpdateRequest +{ + [JsonPropertyName("shortcuts")] + public List? Shortcuts { get; set; } + + [JsonPropertyName("createOrOverwrite")] + public bool? CreateOrOverwrite { get; set; } +} + +/// +/// Response from the Create Or Update Shortcuts API. +/// +public class ShortcutCreateOrUpdateResponse +{ + [JsonPropertyName("value")] + public List? Value { get; set; } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleCreateOrUpdateOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleCreateOrUpdateOptions.cs new file mode 100644 index 0000000000..90941dd0c2 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleCreateOrUpdateOptions.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Mcp.Core.Options; + +namespace Fabric.Mcp.Tools.OneLake.Options; + +public sealed class DataAccessRoleCreateOrUpdateOptions : GlobalOptions +{ + public string? WorkspaceId { get; set; } + public string? Workspace { get; set; } + public string? ItemId { get; set; } + public string? Item { get; set; } + public string? RoleDefinition { get; set; } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleDeleteOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleDeleteOptions.cs new file mode 100644 index 0000000000..1f010b34f8 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleDeleteOptions.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Mcp.Core.Options; + +namespace Fabric.Mcp.Tools.OneLake.Options; + +public sealed class DataAccessRoleDeleteOptions : GlobalOptions +{ + public string? WorkspaceId { get; set; } + public string? Workspace { get; set; } + public string? ItemId { get; set; } + public string? Item { get; set; } + public string? RoleName { get; set; } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleGetOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleGetOptions.cs new file mode 100644 index 0000000000..5cbb378fd4 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleGetOptions.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Mcp.Core.Options; + +namespace Fabric.Mcp.Tools.OneLake.Options; + +public sealed class DataAccessRoleGetOptions : GlobalOptions +{ + public string? WorkspaceId { get; set; } + public string? Workspace { get; set; } + public string? ItemId { get; set; } + public string? Item { get; set; } + public string? RoleName { get; set; } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleListOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleListOptions.cs new file mode 100644 index 0000000000..354dd250e9 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleListOptions.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Mcp.Core.Options; + +namespace Fabric.Mcp.Tools.OneLake.Options; + +public sealed class DataAccessRoleListOptions : GlobalOptions +{ + public string? WorkspaceId { get; set; } + public string? Workspace { get; set; } + public string? ItemId { get; set; } + public string? Item { get; set; } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DiagnosticsModifyOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DiagnosticsModifyOptions.cs new file mode 100644 index 0000000000..d996930871 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DiagnosticsModifyOptions.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Mcp.Core.Options; + +namespace Fabric.Mcp.Tools.OneLake.Options; + +public sealed class DiagnosticsModifyOptions : GlobalOptions +{ + public string? WorkspaceId { get; set; } + public string? Workspace { get; set; } + public string? DiagnosticsConfig { get; set; } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs index d1f1576b3c..63948593ca 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs @@ -178,4 +178,70 @@ public static class FabricOptionDefinitions Description = "The table name exposed by the OneLake table API.", Required = true }; + + // Data access security options + public const string RoleNameName = "role-name"; + public static readonly Option RoleName = new($"--{RoleNameName}") + { + Description = "The name of the data access role.", + Required = true + }; + + public const string RoleDefinitionName = "role-definition"; + public static readonly Option RoleDefinition = new($"--{RoleDefinitionName}") + { + Description = "JSON definition of the data access role including members and decision rules.", + Required = true + }; + + // Shortcut options + public const string ShortcutNameName = "shortcut-name"; + public static readonly Option ShortcutName = new($"--{ShortcutNameName}") + { + Description = "The name of the shortcut.", + Required = true + }; + + public const string ShortcutPathName = "shortcut-path"; + public static readonly Option ShortcutPath = new($"--{ShortcutPathName}") + { + Description = "The path of the shortcut within the item.", + Required = true + }; + + public const string ParentPathName = "parent-path"; + public static readonly Option ParentPath = new($"--{ParentPathName}") + { + Description = "The parent path under which to list shortcuts.", + Required = false + }; + + public const string CreateOrOverwriteName = "create-or-overwrite"; + public static readonly Option CreateOrOverwrite = new($"--{CreateOrOverwriteName}") + { + Description = "When true, overwrites existing shortcuts. When false (default), fails if a shortcut already exists.", + Required = false + }; + + public const string ShortcutsDefinitionName = "shortcuts"; + public static readonly Option ShortcutsDefinition = new($"--{ShortcutsDefinitionName}") + { + Description = "JSON array of shortcut definitions to create or update.", + Required = true + }; + + // Settings options + public const string DiagnosticsConfigName = "diagnostics-config"; + public static readonly Option DiagnosticsConfig = new($"--{DiagnosticsConfigName}") + { + Description = "JSON configuration for OneLake diagnostic logging.", + Required = true + }; + + public const string ImmutabilityPolicyConfigName = "immutability-policy"; + public static readonly Option ImmutabilityPolicyConfig = new($"--{ImmutabilityPolicyConfigName}") + { + Description = "JSON configuration for OneLake immutability policy.", + Required = true + }; } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ImmutabilityPolicyModifyOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ImmutabilityPolicyModifyOptions.cs new file mode 100644 index 0000000000..c5e4cc0513 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ImmutabilityPolicyModifyOptions.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Mcp.Core.Options; + +namespace Fabric.Mcp.Tools.OneLake.Options; + +public sealed class ImmutabilityPolicyModifyOptions : GlobalOptions +{ + public string? WorkspaceId { get; set; } + public string? Workspace { get; set; } + public string? ImmutabilityPolicyConfig { get; set; } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/SettingsGetOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/SettingsGetOptions.cs new file mode 100644 index 0000000000..793940f548 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/SettingsGetOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Mcp.Core.Options; + +namespace Fabric.Mcp.Tools.OneLake.Options; + +public sealed class SettingsGetOptions : GlobalOptions +{ + public string? WorkspaceId { get; set; } + public string? Workspace { get; set; } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOrUpdateOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOrUpdateOptions.cs new file mode 100644 index 0000000000..e3e203a82b --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOrUpdateOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Mcp.Core.Options; + +namespace Fabric.Mcp.Tools.OneLake.Options; + +public sealed class ShortcutCreateOrUpdateOptions : GlobalOptions +{ + public string? WorkspaceId { get; set; } + public string? Workspace { get; set; } + public string? ItemId { get; set; } + public string? Item { get; set; } + public string? ShortcutsDefinition { get; set; } + public bool CreateOrOverwrite { get; set; } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutDeleteOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutDeleteOptions.cs new file mode 100644 index 0000000000..29c23bc7f7 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutDeleteOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Mcp.Core.Options; + +namespace Fabric.Mcp.Tools.OneLake.Options; + +public sealed class ShortcutDeleteOptions : GlobalOptions +{ + public string? WorkspaceId { get; set; } + public string? Workspace { get; set; } + public string? ItemId { get; set; } + public string? Item { get; set; } + public string? ShortcutName { get; set; } + public string? ShortcutPath { get; set; } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutGetOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutGetOptions.cs new file mode 100644 index 0000000000..4130f26f43 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutGetOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Mcp.Core.Options; + +namespace Fabric.Mcp.Tools.OneLake.Options; + +public sealed class ShortcutGetOptions : GlobalOptions +{ + public string? WorkspaceId { get; set; } + public string? Workspace { get; set; } + public string? ItemId { get; set; } + public string? Item { get; set; } + public string? ShortcutName { get; set; } + public string? ShortcutPath { get; set; } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutListOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutListOptions.cs new file mode 100644 index 0000000000..04c2b949be --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutListOptions.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Mcp.Core.Options; + +namespace Fabric.Mcp.Tools.OneLake.Options; + +public sealed class ShortcutListOptions : GlobalOptions +{ + public string? WorkspaceId { get; set; } + public string? Workspace { get; set; } + public string? ItemId { get; set; } + public string? Item { get; set; } + public string? ParentPath { get; set; } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutResetCacheOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutResetCacheOptions.cs new file mode 100644 index 0000000000..cd4cbb33e8 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutResetCacheOptions.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Mcp.Core.Options; + +namespace Fabric.Mcp.Tools.OneLake.Options; + +public sealed class ShortcutResetCacheOptions : GlobalOptions +{ + public string? WorkspaceId { get; set; } + public string? Workspace { get; set; } + public string? ItemId { get; set; } + public string? Item { get; set; } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs index f80dd8e7a2..57c05e7720 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs @@ -46,4 +46,22 @@ public interface IOneLakeService Task GetTableNamespaceAsync(string workspaceIdentifier, string itemIdentifier, string namespaceName, CancellationToken cancellationToken = default); Task ListTablesAsync(string workspaceIdentifier, string itemIdentifier, string namespaceName, CancellationToken cancellationToken = default); Task GetTableAsync(string workspaceIdentifier, string itemIdentifier, string namespaceName, string tableName, CancellationToken cancellationToken = default); + + // Data Access Security Operations + Task ListDataAccessRolesAsync(string workspaceId, string itemId, CancellationToken cancellationToken = default); + Task GetDataAccessRoleAsync(string workspaceId, string itemId, string roleName, CancellationToken cancellationToken = default); + Task CreateOrUpdateDataAccessRoleAsync(string workspaceId, string itemId, string roleDefinitionJson, CancellationToken cancellationToken = default); + Task DeleteDataAccessRoleAsync(string workspaceId, string itemId, string roleName, CancellationToken cancellationToken = default); + + // Shortcut Operations + Task ListShortcutsAsync(string workspaceId, string itemId, string? parentPath = null, CancellationToken cancellationToken = default); + Task GetShortcutAsync(string workspaceId, string itemId, string shortcutPath, string shortcutName, CancellationToken cancellationToken = default); + Task CreateOrUpdateShortcutsAsync(string workspaceId, string itemId, string shortcutsJson, bool createOrOverwrite = false, CancellationToken cancellationToken = default); + Task DeleteShortcutAsync(string workspaceId, string itemId, string shortcutPath, string shortcutName, CancellationToken cancellationToken = default); + Task ResetShortcutCacheAsync(string workspaceId, string itemId, CancellationToken cancellationToken = default); + + // Settings Operations + Task GetSettingsAsync(string workspaceId, CancellationToken cancellationToken = default); + Task ModifyDiagnosticsAsync(string workspaceId, string diagnosticsConfigJson, CancellationToken cancellationToken = default); + Task ModifyImmutabilityPolicyAsync(string workspaceId, string immutabilityPolicyJson, CancellationToken cancellationToken = default); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs index d55ab423e7..3bf5a0d4cb 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs @@ -232,34 +232,10 @@ public async Task> ListBlobsAsync(string workspaceI return await ListBlobsIntelligentAsync(normalizedWorkspaceId, normalizedItemId, recursive, cancellationToken); } - // Use the OneLake blob endpoint to list files for specific path - var url = $"{OneLakeEndpoints.OneLakeDataPlaneBaseUrl}/{normalizedWorkspaceId}/{normalizedItemId}"; - - // If path is specified, check if it's a top-level folder (Tables, Files, etc.) - // or a sub-path within Files - var trimmedPath = path.TrimStart('/'); - if (trimmedPath.StartsWith("Files/", StringComparison.OrdinalIgnoreCase)) - { - // Path already includes Files prefix - url += $"/{trimmedPath}"; - } - else if (trimmedPath.Equals("Files", StringComparison.OrdinalIgnoreCase)) - { - // Explicitly requesting Files folder - url += "/Files"; - } - else if (IsTopLevelFolder(trimmedPath)) - { - // Top-level folder like Tables, Files, etc. - url += $"/{trimmedPath}"; - } - else - { - // Assume it's a sub-path within Files for backward compatibility - url += $"/Files/{trimmedPath}"; - } - - url += $"?restype=container&comp=list"; + // Blob API: path is part of the URL path per Blob Storage List Blobs spec + var directory = ResolveDirectoryPath(path); + var url = $"{OneLakeEndpoints.OneLakeDataPlaneBaseUrl}/{normalizedWorkspaceId}/{normalizedItemId}/{directory}"; + url += "?restype=container&comp=list"; if (recursive) { url += "&recursive=true"; @@ -600,8 +576,7 @@ public async Task> ListPathIntelligentAsync(string workspac { try { - var url = $"{OneLakeEndpoints.OneLakeDataPlaneDfsBaseUrl}/{normalizedWorkspaceId}/{normalizedItemId}/{folder}"; - url += $"?resource=filesystem&recursive={recursive.ToString().ToLowerInvariant()}"; + var url = BuildAdlsListPathUrl(normalizedWorkspaceId, normalizedItemId, directory: folder, recursive: recursive); var response = await SendDataPlaneRequestAsync(HttpMethod.Get, url, cancellationToken: cancellationToken); var content = await response.Content.ReadAsStringAsync(cancellationToken); @@ -719,34 +694,9 @@ public async Task> ListPathAsync(string workspaceId, string return await ListPathIntelligentAsync(normalizedWorkspaceId, normalizedItemId, recursive, cancellationToken); } - // Use ADLS Gen2 filesystem API format instead of blob container format - var url = $"{OneLakeEndpoints.OneLakeDataPlaneDfsBaseUrl}/{normalizedWorkspaceId}/{normalizedItemId}"; - - // If path is specified, check if it's a top-level folder (Tables, Files, etc.) - // or a sub-path within Files - var trimmedPath = path.TrimStart('/'); - if (trimmedPath.StartsWith("Files/", StringComparison.OrdinalIgnoreCase)) - { - // Path already includes Files prefix - url += $"/{trimmedPath}"; - } - else if (trimmedPath.Equals("Files", StringComparison.OrdinalIgnoreCase)) - { - // Explicitly requesting Files folder - url += "/Files"; - } - else if (IsTopLevelFolder(trimmedPath)) - { - // Top-level folder like Tables, Files, etc. - url += $"/{trimmedPath}"; - } - else - { - // Assume it's a sub-path within Files for backward compatibility - url += $"/Files/{trimmedPath}"; - } - - url += $"?resource=filesystem&recursive={recursive.ToString().ToLowerInvariant()}"; + // Build ADLS Gen2 compliant URL with directory as a query parameter + var directory = ResolveDirectoryPath(path); + var url = BuildAdlsListPathUrl(normalizedWorkspaceId, normalizedItemId, directory: directory, recursive: recursive); var response = await SendDataPlaneRequestAsync(HttpMethod.Get, url, cancellationToken: cancellationToken); var content = await response.Content.ReadAsStringAsync(cancellationToken); @@ -894,7 +844,7 @@ public async Task ListBlobsRawAsync(string workspaceId, string itemId, s try { var url = $"{OneLakeEndpoints.OneLakeDataPlaneBaseUrl}/{normalizedWorkspaceId}/{normalizedItemId}/{folder}"; - url += $"?restype=container&comp=list"; + url += "?restype=container&comp=list"; if (recursive) { url += "&recursive=true"; @@ -913,34 +863,10 @@ public async Task ListBlobsRawAsync(string workspaceId, string itemId, s return string.Join("\n\n", allResponses); } - // Use the OneLake blob endpoint to list files for specific path - var singleUrl = $"{OneLakeEndpoints.OneLakeDataPlaneBaseUrl}/{normalizedWorkspaceId}/{normalizedItemId}"; - - // If path is specified, check if it's a top-level folder (Tables, Files, etc.) - // or a sub-path within Files - var trimmedPath = path.TrimStart('/'); - if (trimmedPath.StartsWith("Files/", StringComparison.OrdinalIgnoreCase)) - { - // Path already includes Files prefix - singleUrl += $"/{trimmedPath}"; - } - else if (trimmedPath.Equals("Files", StringComparison.OrdinalIgnoreCase)) - { - // Explicitly requesting Files folder - singleUrl += "/Files"; - } - else if (IsTopLevelFolder(trimmedPath)) - { - // Top-level folder like Tables, Files, etc. - singleUrl += $"/{trimmedPath}"; - } - else - { - // Assume it's a sub-path within Files for backward compatibility - singleUrl += $"/Files/{trimmedPath}"; - } - - singleUrl += $"?restype=container&comp=list"; + // Blob API: path is part of the URL path per Blob Storage List Blobs spec + var directory = ResolveDirectoryPath(path); + var singleUrl = $"{OneLakeEndpoints.OneLakeDataPlaneBaseUrl}/{normalizedWorkspaceId}/{normalizedItemId}/{directory}"; + singleUrl += "?restype=container&comp=list"; if (recursive) { singleUrl += "&recursive=true"; @@ -966,8 +892,7 @@ public async Task ListPathRawAsync(string workspaceId, string itemId, st { try { - var url = $"{OneLakeEndpoints.OneLakeDataPlaneDfsBaseUrl}/{normalizedWorkspaceId}/{normalizedItemId}/{folder}"; - url += $"?resource=filesystem&recursive={recursive.ToString().ToLowerInvariant()}"; + var url = BuildAdlsListPathUrl(normalizedWorkspaceId, normalizedItemId, directory: folder, recursive: recursive); var response = await SendDataPlaneRequestAsync(HttpMethod.Get, url, cancellationToken: cancellationToken); var content = await response.Content.ReadAsStringAsync(cancellationToken); @@ -982,34 +907,9 @@ public async Task ListPathRawAsync(string workspaceId, string itemId, st return string.Join("\n\n", allResponses); } - // Use ADLS Gen2 filesystem API format instead of blob container format - var singleUrl = $"{OneLakeEndpoints.OneLakeDataPlaneDfsBaseUrl}/{normalizedWorkspaceId}/{normalizedItemId}"; - - // If path is specified, check if it's a top-level folder (Tables, Files, etc.) - // or a sub-path within Files - var trimmedPath = path.TrimStart('/'); - if (trimmedPath.StartsWith("Files/", StringComparison.OrdinalIgnoreCase)) - { - // Path already includes Files prefix - singleUrl += $"/{trimmedPath}"; - } - else if (trimmedPath.Equals("Files", StringComparison.OrdinalIgnoreCase)) - { - // Explicitly requesting Files folder - singleUrl += "/Files"; - } - else if (IsTopLevelFolder(trimmedPath)) - { - // Top-level folder like Tables, Files, etc. - singleUrl += $"/{trimmedPath}"; - } - else - { - // Assume it's a sub-path within Files for backward compatibility - singleUrl += $"/Files/{trimmedPath}"; - } - - singleUrl += $"?resource=filesystem&recursive={recursive.ToString().ToLowerInvariant()}"; + // Build ADLS Gen2 compliant URL with directory as a query parameter + var directory = ResolveDirectoryPath(path); + var singleUrl = BuildAdlsListPathUrl(normalizedWorkspaceId, normalizedItemId, directory: directory, recursive: recursive); var singleResponse = await SendDataPlaneRequestAsync(HttpMethod.Get, singleUrl, cancellationToken: cancellationToken); return await singleResponse.Content.ReadAsStringAsync(cancellationToken); @@ -1651,6 +1551,73 @@ public async Task CreateDirectoryAsync(string workspaceId, string itemId, string } // Private helper methods + + /// + /// Builds an ADLS Gen2 Path List API-compliant URL. + /// Per https://learn.microsoft.com/en-us/rest/api/storageservices/datalakestoragegen2/path/list + /// the directory is a query parameter, not part of the URL path. + /// URL format: GET {dfsBase}/{filesystem}?resource=filesystem&directory={dir}&recursive={recursive} + /// In OneLake, filesystem = workspaceId; directory = itemId/path. + /// + private static string BuildAdlsListPathUrl( + string workspaceId, + string itemId, + string? directory, + bool recursive, + int? maxResults = null, + string? continuation = null, + bool? upn = null) + { + // filesystem = workspaceId + var url = $"{OneLakeEndpoints.OneLakeDataPlaneDfsBaseUrl}/{workspaceId}?resource=filesystem"; + + // Build the directory path: itemId + optional subdirectory + var directoryPath = itemId; + if (!string.IsNullOrEmpty(directory)) + { + directoryPath = $"{itemId}/{directory.TrimStart('/')}"; + } + + url += $"&directory={Uri.EscapeDataString(directoryPath)}"; + url += $"&recursive={recursive.ToString().ToLowerInvariant()}"; + + if (maxResults.HasValue) + { + url += $"&maxResults={maxResults.Value}"; + } + + if (!string.IsNullOrEmpty(continuation)) + { + url += $"&continuation={Uri.EscapeDataString(continuation)}"; + } + + if (upn.HasValue) + { + url += $"&upn={upn.Value.ToString().ToLowerInvariant()}"; + } + + return url; + } + + /// + /// Resolves a user-provided path to the ADLS directory value relative to the item root. + /// Handles top-level folders (Files, Tables) and defaults to Files/ prefix for backward compatibility. + /// + private static string ResolveDirectoryPath(string path) + { + var trimmedPath = path.TrimStart('/'); + + if (trimmedPath.StartsWith("Files/", StringComparison.OrdinalIgnoreCase) || + trimmedPath.Equals("Files", StringComparison.OrdinalIgnoreCase) || + IsTopLevelFolder(trimmedPath)) + { + return trimmedPath; + } + + // Assume it's a sub-path within Files for backward compatibility + return $"Files/{trimmedPath}"; + } + private static void ValidatePathForTraversal(string path, string paramName) { // Decode percent-encoding so that %2e%2e or %2E%2E variants are caught @@ -1839,6 +1806,117 @@ public void Dispose() // DefaultAzureCredential doesn't need disposal } + // Data Access Security Operations + public async Task ListDataAccessRolesAsync(string workspaceId, string itemId, CancellationToken cancellationToken = default) + { + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/dataAccessRoles"; + var response = await SendFabricApiRequestAsync(HttpMethod.Get, url, cancellationToken: cancellationToken); + return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.DataAccessRoleListResponse, cancellationToken) ?? new DataAccessRoleListResponse(); + } + + public async Task GetDataAccessRoleAsync(string workspaceId, string itemId, string roleName, CancellationToken cancellationToken = default) + { + var encodedRoleName = Uri.EscapeDataString(roleName); + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/dataAccessRoles/{encodedRoleName}"; + var response = await SendFabricApiRequestAsync(HttpMethod.Get, url, cancellationToken: cancellationToken); + return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.DataAccessRole, cancellationToken) ?? new DataAccessRole(); + } + + public async Task CreateOrUpdateDataAccessRoleAsync(string workspaceId, string itemId, string roleDefinitionJson, CancellationToken cancellationToken = default) + { + var roleDefinition = JsonSerializer.Deserialize(roleDefinitionJson, OneLakeJsonContext.Default.DataAccessRole) + ?? throw new ArgumentException("Invalid role definition JSON.", nameof(roleDefinitionJson)); + + var encodedRoleName = Uri.EscapeDataString(roleDefinition.Name); + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/dataAccessRoles/{encodedRoleName}"; + var response = await SendFabricApiRequestAsync(HttpMethod.Put, url, roleDefinitionJson, cancellationToken: cancellationToken); + return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.DataAccessRole, cancellationToken) ?? new DataAccessRole(); + } + + public async Task DeleteDataAccessRoleAsync(string workspaceId, string itemId, string roleName, CancellationToken cancellationToken = default) + { + var encodedRoleName = Uri.EscapeDataString(roleName); + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/dataAccessRoles/{encodedRoleName}"; + await SendFabricApiDeleteRequestAsync(url, cancellationToken); + } + + // Shortcut Operations + public async Task ListShortcutsAsync(string workspaceId, string itemId, string? parentPath = null, CancellationToken cancellationToken = default) + { + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/shortcuts"; + if (!string.IsNullOrWhiteSpace(parentPath)) + { + url += $"?parentPath={Uri.EscapeDataString(parentPath)}"; + } + var response = await SendFabricApiRequestAsync(HttpMethod.Get, url, cancellationToken: cancellationToken); + return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.ShortcutListResponse, cancellationToken) ?? new ShortcutListResponse(); + } + + public async Task GetShortcutAsync(string workspaceId, string itemId, string shortcutPath, string shortcutName, CancellationToken cancellationToken = default) + { + var encodedPath = Uri.EscapeDataString(shortcutPath); + var encodedName = Uri.EscapeDataString(shortcutName); + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/shortcuts/{encodedPath}/{encodedName}"; + var response = await SendFabricApiRequestAsync(HttpMethod.Get, url, cancellationToken: cancellationToken); + return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.OneLakeShortcut, cancellationToken) ?? new OneLakeShortcut(); + } + + public async Task CreateOrUpdateShortcutsAsync(string workspaceId, string itemId, string shortcutsJson, bool createOrOverwrite = false, CancellationToken cancellationToken = default) + { + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/shortcuts?createOrOverwrite={createOrOverwrite.ToString().ToLowerInvariant()}"; + var response = await SendFabricApiRequestAsync(HttpMethod.Post, url, shortcutsJson, cancellationToken: cancellationToken); + return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.ShortcutCreateOrUpdateResponse, cancellationToken) ?? new ShortcutCreateOrUpdateResponse(); + } + + public async Task DeleteShortcutAsync(string workspaceId, string itemId, string shortcutPath, string shortcutName, CancellationToken cancellationToken = default) + { + var encodedPath = Uri.EscapeDataString(shortcutPath); + var encodedName = Uri.EscapeDataString(shortcutName); + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/shortcuts/{encodedPath}/{encodedName}"; + await SendFabricApiDeleteRequestAsync(url, cancellationToken); + } + + public async Task ResetShortcutCacheAsync(string workspaceId, string itemId, CancellationToken cancellationToken = default) + { + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/shortcuts/resetCache"; + await SendFabricApiRequestAsync(HttpMethod.Post, url, cancellationToken: cancellationToken); + } + + // Settings Operations + public async Task GetSettingsAsync(string workspaceId, CancellationToken cancellationToken = default) + { + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/onelake/settings"; + var response = await SendFabricApiRequestAsync(HttpMethod.Get, url, cancellationToken: cancellationToken); + return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.OneLakeSettings, cancellationToken) ?? new OneLakeSettings(); + } + + public async Task ModifyDiagnosticsAsync(string workspaceId, string diagnosticsConfigJson, CancellationToken cancellationToken = default) + { + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/onelake/settings/diagnostics"; + var response = await SendFabricApiRequestAsync(new HttpMethod("PATCH"), url, diagnosticsConfigJson, cancellationToken: cancellationToken); + return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.OneLakeSettings, cancellationToken) ?? new OneLakeSettings(); + } + + public async Task ModifyImmutabilityPolicyAsync(string workspaceId, string immutabilityPolicyJson, CancellationToken cancellationToken = default) + { + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/onelake/settings/immutabilityPolicy"; + var response = await SendFabricApiRequestAsync(new HttpMethod("PATCH"), url, immutabilityPolicyJson, cancellationToken: cancellationToken); + return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.OneLakeSettings, cancellationToken) ?? new OneLakeSettings(); + } + + private async Task SendFabricApiDeleteRequestAsync(string url, CancellationToken cancellationToken) + { + var tokenContext = new TokenRequestContext(new[] { OneLakeEndpoints.GetFabricScope() }); + var token = await _credential.GetTokenAsync(tokenContext, cancellationToken); + + using var request = new HttpRequestMessage(HttpMethod.Delete, url); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); + ApplyUserAgent(request); + + var response = await _httpClient.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + } + private static string ExtractWarehouseQueryValue(string warehousePrefix) { const string WarehousePrefixRoot = "warehouse/"; diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleCreateOrUpdateCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleCreateOrUpdateCommandTests.cs new file mode 100644 index 0000000000..ba1cb05077 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleCreateOrUpdateCommandTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Commands.Security; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Mcp.Tests.Client; + +namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Security; + +public class DataAccessRoleCreateOrUpdateCommandTests : CommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + Assert.Equal("create_or_update_data_access_role", Command.Name); + Assert.Equal("Create or Update OneLake Data Access Role", Command.Title); + Assert.Contains("Upsert a single data access role", Command.Description); + Assert.False(Command.Metadata.ReadOnly); + Assert.False(Command.Metadata.Destructive); + Assert.True(Command.Metadata.Idempotent); + } + + [Fact] + public void GetCommand_ReturnsValidCommand() + { + Assert.Equal("create_or_update_data_access_role", CommandDefinition.Name); + Assert.NotNull(CommandDefinition.Description); + Assert.NotEmpty(CommandDefinition.Options); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenLoggerIsNull() + { + Assert.Throws(() => new DataAccessRoleCreateOrUpdateCommand(null!, Service)); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenOneLakeServiceIsNull() + { + Assert.Throws(() => new DataAccessRoleCreateOrUpdateCommand(Logger, null!)); + } + + [Fact] + public void Metadata_HasCorrectProperties() + { + var metadata = Command.Metadata; + + Assert.False(metadata.Destructive); + Assert.True(metadata.Idempotent); + Assert.False(metadata.LocalRequired); + Assert.False(metadata.OpenWorld); + Assert.False(metadata.ReadOnly); + Assert.False(metadata.Secret); + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleDeleteCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleDeleteCommandTests.cs new file mode 100644 index 0000000000..217b43d380 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleDeleteCommandTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Commands.Security; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Mcp.Tests.Client; + +namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Security; + +public class DataAccessRoleDeleteCommandTests : CommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + Assert.Equal("delete_data_access_role", Command.Name); + Assert.Equal("Delete OneLake Data Access Role", Command.Title); + Assert.Contains("Delete a single data access role", Command.Description); + Assert.False(Command.Metadata.ReadOnly); + Assert.True(Command.Metadata.Destructive); + Assert.True(Command.Metadata.Idempotent); + } + + [Fact] + public void GetCommand_ReturnsValidCommand() + { + Assert.Equal("delete_data_access_role", CommandDefinition.Name); + Assert.NotNull(CommandDefinition.Description); + Assert.NotEmpty(CommandDefinition.Options); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenLoggerIsNull() + { + Assert.Throws(() => new DataAccessRoleDeleteCommand(null!, Service)); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenOneLakeServiceIsNull() + { + Assert.Throws(() => new DataAccessRoleDeleteCommand(Logger, null!)); + } + + [Fact] + public void Metadata_HasCorrectProperties() + { + var metadata = Command.Metadata; + + Assert.True(metadata.Destructive); + Assert.True(metadata.Idempotent); + Assert.False(metadata.LocalRequired); + Assert.False(metadata.OpenWorld); + Assert.False(metadata.ReadOnly); + Assert.False(metadata.Secret); + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleGetCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleGetCommandTests.cs new file mode 100644 index 0000000000..6b913499a3 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleGetCommandTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Commands.Security; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Mcp.Tests.Client; + +namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Security; + +public class DataAccessRoleGetCommandTests : CommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + Assert.Equal("get_data_access_role", Command.Name); + Assert.Equal("Get OneLake Data Access Role", Command.Title); + Assert.Contains("Get the full definition of a single data access role", Command.Description); + Assert.True(Command.Metadata.ReadOnly); + Assert.False(Command.Metadata.Destructive); + Assert.True(Command.Metadata.Idempotent); + } + + [Fact] + public void GetCommand_ReturnsValidCommand() + { + Assert.Equal("get_data_access_role", CommandDefinition.Name); + Assert.NotNull(CommandDefinition.Description); + Assert.NotEmpty(CommandDefinition.Options); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenLoggerIsNull() + { + Assert.Throws(() => new DataAccessRoleGetCommand(null!, Service)); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenOneLakeServiceIsNull() + { + Assert.Throws(() => new DataAccessRoleGetCommand(Logger, null!)); + } + + [Fact] + public void Metadata_HasCorrectProperties() + { + var metadata = Command.Metadata; + + Assert.False(metadata.Destructive); + Assert.True(metadata.Idempotent); + Assert.False(metadata.LocalRequired); + Assert.False(metadata.OpenWorld); + Assert.True(metadata.ReadOnly); + Assert.False(metadata.Secret); + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleListCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleListCommandTests.cs new file mode 100644 index 0000000000..58d4399b7f --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleListCommandTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Commands.Security; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Mcp.Tests.Client; + +namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Security; + +public class DataAccessRoleListCommandTests : CommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + Assert.Equal("list_data_access_roles", Command.Name); + Assert.Equal("List OneLake Data Access Roles", Command.Title); + Assert.Contains("List all data access roles", Command.Description); + Assert.True(Command.Metadata.ReadOnly); + Assert.False(Command.Metadata.Destructive); + Assert.True(Command.Metadata.Idempotent); + } + + [Fact] + public void GetCommand_ReturnsValidCommand() + { + Assert.Equal("list_data_access_roles", CommandDefinition.Name); + Assert.NotNull(CommandDefinition.Description); + Assert.NotEmpty(CommandDefinition.Options); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenLoggerIsNull() + { + Assert.Throws(() => new DataAccessRoleListCommand(null!, Service)); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenOneLakeServiceIsNull() + { + Assert.Throws(() => new DataAccessRoleListCommand(Logger, null!)); + } + + [Fact] + public void Metadata_HasCorrectProperties() + { + var metadata = Command.Metadata; + + Assert.False(metadata.Destructive); + Assert.True(metadata.Idempotent); + Assert.False(metadata.LocalRequired); + Assert.False(metadata.OpenWorld); + Assert.True(metadata.ReadOnly); + Assert.False(metadata.Secret); + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/DiagnosticsModifyCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/DiagnosticsModifyCommandTests.cs new file mode 100644 index 0000000000..6bac062595 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/DiagnosticsModifyCommandTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Commands.Settings; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Mcp.Tests.Client; + +namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Settings; + +public class DiagnosticsModifyCommandTests : CommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + Assert.Equal("modify_diagnostics", Command.Name); + Assert.Equal("Modify OneLake Diagnostics", Command.Title); + Assert.Contains("Modify the diagnostic logging configuration", Command.Description); + Assert.False(Command.Metadata.ReadOnly); + Assert.False(Command.Metadata.Destructive); + Assert.True(Command.Metadata.Idempotent); + } + + [Fact] + public void GetCommand_ReturnsValidCommand() + { + Assert.Equal("modify_diagnostics", CommandDefinition.Name); + Assert.NotNull(CommandDefinition.Description); + Assert.NotEmpty(CommandDefinition.Options); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenLoggerIsNull() + { + Assert.Throws(() => new DiagnosticsModifyCommand(null!, Service)); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenOneLakeServiceIsNull() + { + Assert.Throws(() => new DiagnosticsModifyCommand(Logger, null!)); + } + + [Fact] + public void Metadata_HasCorrectProperties() + { + var metadata = Command.Metadata; + + Assert.False(metadata.Destructive); + Assert.True(metadata.Idempotent); + Assert.False(metadata.LocalRequired); + Assert.False(metadata.OpenWorld); + Assert.False(metadata.ReadOnly); + Assert.False(metadata.Secret); + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs new file mode 100644 index 0000000000..6f900044a0 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Commands.Settings; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Mcp.Tests.Client; + +namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Settings; + +public class ImmutabilityPolicyModifyCommandTests : CommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + Assert.Equal("modify_immutability_policy", Command.Name); + Assert.Equal("Modify OneLake Immutability Policy", Command.Title); + Assert.Contains("Modify the workspace-level OneLake immutability policy", Command.Description); + Assert.False(Command.Metadata.ReadOnly); + Assert.False(Command.Metadata.Destructive); + Assert.True(Command.Metadata.Idempotent); + } + + [Fact] + public void GetCommand_ReturnsValidCommand() + { + Assert.Equal("modify_immutability_policy", CommandDefinition.Name); + Assert.NotNull(CommandDefinition.Description); + Assert.NotEmpty(CommandDefinition.Options); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenLoggerIsNull() + { + Assert.Throws(() => new ImmutabilityPolicyModifyCommand(null!, Service)); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenOneLakeServiceIsNull() + { + Assert.Throws(() => new ImmutabilityPolicyModifyCommand(Logger, null!)); + } + + [Fact] + public void Metadata_HasCorrectProperties() + { + var metadata = Command.Metadata; + + Assert.False(metadata.Destructive); + Assert.True(metadata.Idempotent); + Assert.False(metadata.LocalRequired); + Assert.False(metadata.OpenWorld); + Assert.False(metadata.ReadOnly); + Assert.False(metadata.Secret); + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/SettingsGetCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/SettingsGetCommandTests.cs new file mode 100644 index 0000000000..ed369ac407 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/SettingsGetCommandTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Commands.Settings; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Mcp.Tests.Client; + +namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Settings; + +public class SettingsGetCommandTests : CommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + Assert.Equal("get_settings", Command.Name); + Assert.Equal("Get OneLake Settings", Command.Title); + Assert.Contains("Get the OneLake settings for a workspace", Command.Description); + Assert.True(Command.Metadata.ReadOnly); + Assert.False(Command.Metadata.Destructive); + Assert.True(Command.Metadata.Idempotent); + } + + [Fact] + public void GetCommand_ReturnsValidCommand() + { + Assert.Equal("get_settings", CommandDefinition.Name); + Assert.NotNull(CommandDefinition.Description); + Assert.NotEmpty(CommandDefinition.Options); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenLoggerIsNull() + { + Assert.Throws(() => new SettingsGetCommand(null!, Service)); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenOneLakeServiceIsNull() + { + Assert.Throws(() => new SettingsGetCommand(Logger, null!)); + } + + [Fact] + public void Metadata_HasCorrectProperties() + { + var metadata = Command.Metadata; + + Assert.False(metadata.Destructive); + Assert.True(metadata.Idempotent); + Assert.False(metadata.LocalRequired); + Assert.False(metadata.OpenWorld); + Assert.True(metadata.ReadOnly); + Assert.False(metadata.Secret); + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateOrUpdateCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateOrUpdateCommandTests.cs new file mode 100644 index 0000000000..dbd6ae2329 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateOrUpdateCommandTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Commands.Shortcut; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Mcp.Tests.Client; + +namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Shortcut; + +public class ShortcutCreateOrUpdateCommandTests : CommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + Assert.Equal("create_or_update_shortcuts", Command.Name); + Assert.Equal("Create or Update OneLake Shortcuts", Command.Title); + Assert.Contains("Create one or more shortcuts in a single call", Command.Description); + Assert.False(Command.Metadata.ReadOnly); + Assert.False(Command.Metadata.Destructive); + Assert.False(Command.Metadata.Idempotent); + } + + [Fact] + public void GetCommand_ReturnsValidCommand() + { + Assert.Equal("create_or_update_shortcuts", CommandDefinition.Name); + Assert.NotNull(CommandDefinition.Description); + Assert.NotEmpty(CommandDefinition.Options); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenLoggerIsNull() + { + Assert.Throws(() => new ShortcutCreateOrUpdateCommand(null!, Service)); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenOneLakeServiceIsNull() + { + Assert.Throws(() => new ShortcutCreateOrUpdateCommand(Logger, null!)); + } + + [Fact] + public void Metadata_HasCorrectProperties() + { + var metadata = Command.Metadata; + + Assert.False(metadata.Destructive); + Assert.False(metadata.Idempotent); + Assert.False(metadata.LocalRequired); + Assert.False(metadata.OpenWorld); + Assert.False(metadata.ReadOnly); + Assert.False(metadata.Secret); + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutDeleteCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutDeleteCommandTests.cs new file mode 100644 index 0000000000..cc39c18ca1 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutDeleteCommandTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Commands.Shortcut; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Mcp.Tests.Client; + +namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Shortcut; + +public class ShortcutDeleteCommandTests : CommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + Assert.Equal("delete_shortcut", Command.Name); + Assert.Equal("Delete OneLake Shortcut", Command.Title); + Assert.Contains("Delete a single shortcut from an item", Command.Description); + Assert.False(Command.Metadata.ReadOnly); + Assert.True(Command.Metadata.Destructive); + Assert.True(Command.Metadata.Idempotent); + } + + [Fact] + public void GetCommand_ReturnsValidCommand() + { + Assert.Equal("delete_shortcut", CommandDefinition.Name); + Assert.NotNull(CommandDefinition.Description); + Assert.NotEmpty(CommandDefinition.Options); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenLoggerIsNull() + { + Assert.Throws(() => new ShortcutDeleteCommand(null!, Service)); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenOneLakeServiceIsNull() + { + Assert.Throws(() => new ShortcutDeleteCommand(Logger, null!)); + } + + [Fact] + public void Metadata_HasCorrectProperties() + { + var metadata = Command.Metadata; + + Assert.True(metadata.Destructive); + Assert.True(metadata.Idempotent); + Assert.False(metadata.LocalRequired); + Assert.False(metadata.OpenWorld); + Assert.False(metadata.ReadOnly); + Assert.False(metadata.Secret); + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutGetCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutGetCommandTests.cs new file mode 100644 index 0000000000..ee4e308f24 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutGetCommandTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Commands.Shortcut; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Mcp.Tests.Client; + +namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Shortcut; + +public class ShortcutGetCommandTests : CommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + Assert.Equal("get_shortcut", Command.Name); + Assert.Equal("Get OneLake Shortcut", Command.Title); + Assert.Contains("Get the properties of a single shortcut", Command.Description); + Assert.True(Command.Metadata.ReadOnly); + Assert.False(Command.Metadata.Destructive); + Assert.True(Command.Metadata.Idempotent); + } + + [Fact] + public void GetCommand_ReturnsValidCommand() + { + Assert.Equal("get_shortcut", CommandDefinition.Name); + Assert.NotNull(CommandDefinition.Description); + Assert.NotEmpty(CommandDefinition.Options); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenLoggerIsNull() + { + Assert.Throws(() => new ShortcutGetCommand(null!, Service)); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenOneLakeServiceIsNull() + { + Assert.Throws(() => new ShortcutGetCommand(Logger, null!)); + } + + [Fact] + public void Metadata_HasCorrectProperties() + { + var metadata = Command.Metadata; + + Assert.False(metadata.Destructive); + Assert.True(metadata.Idempotent); + Assert.False(metadata.LocalRequired); + Assert.False(metadata.OpenWorld); + Assert.True(metadata.ReadOnly); + Assert.False(metadata.Secret); + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutListCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutListCommandTests.cs new file mode 100644 index 0000000000..930642dbea --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutListCommandTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Commands.Shortcut; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Mcp.Tests.Client; + +namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Shortcut; + +public class ShortcutListCommandTests : CommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + Assert.Equal("list_shortcuts", Command.Name); + Assert.Equal("List OneLake Shortcuts", Command.Title); + Assert.Contains("List shortcuts defined within an item", Command.Description); + Assert.True(Command.Metadata.ReadOnly); + Assert.False(Command.Metadata.Destructive); + Assert.True(Command.Metadata.Idempotent); + } + + [Fact] + public void GetCommand_ReturnsValidCommand() + { + Assert.Equal("list_shortcuts", CommandDefinition.Name); + Assert.NotNull(CommandDefinition.Description); + Assert.NotEmpty(CommandDefinition.Options); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenLoggerIsNull() + { + Assert.Throws(() => new ShortcutListCommand(null!, Service)); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenOneLakeServiceIsNull() + { + Assert.Throws(() => new ShortcutListCommand(Logger, null!)); + } + + [Fact] + public void Metadata_HasCorrectProperties() + { + var metadata = Command.Metadata; + + Assert.False(metadata.Destructive); + Assert.True(metadata.Idempotent); + Assert.False(metadata.LocalRequired); + Assert.False(metadata.OpenWorld); + Assert.True(metadata.ReadOnly); + Assert.False(metadata.Secret); + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutResetCacheCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutResetCacheCommandTests.cs new file mode 100644 index 0000000000..093c137686 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutResetCacheCommandTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Commands.Shortcut; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Mcp.Tests.Client; + +namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Shortcut; + +public class ShortcutResetCacheCommandTests : CommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + Assert.Equal("reset_shortcut_cache", Command.Name); + Assert.Equal("Reset OneLake Shortcut Cache", Command.Title); + Assert.Contains("Drop cached shortcut reads", Command.Description); + Assert.False(Command.Metadata.ReadOnly); + Assert.False(Command.Metadata.Destructive); + Assert.True(Command.Metadata.Idempotent); + } + + [Fact] + public void GetCommand_ReturnsValidCommand() + { + Assert.Equal("reset_shortcut_cache", CommandDefinition.Name); + Assert.NotNull(CommandDefinition.Description); + Assert.NotEmpty(CommandDefinition.Options); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenLoggerIsNull() + { + Assert.Throws(() => new ShortcutResetCacheCommand(null!, Service)); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_WhenOneLakeServiceIsNull() + { + Assert.Throws(() => new ShortcutResetCacheCommand(Logger, null!)); + } + + [Fact] + public void Metadata_HasCorrectProperties() + { + var metadata = Command.Metadata; + + Assert.False(metadata.Destructive); + Assert.True(metadata.Idempotent); + Assert.False(metadata.LocalRequired); + Assert.False(metadata.OpenWorld); + Assert.False(metadata.ReadOnly); + Assert.False(metadata.Secret); + } +} From 5a024c7dd24494808a4e267b84db09b18caca97c Mon Sep 17 00:00:00 2001 From: Srinivas Thatipamula Date: Mon, 11 May 2026 13:43:56 -0700 Subject: [PATCH 02/15] Fix review feedback: validation, disposal, and docs - Validate role definition JSON: catch JsonException, check non-empty name - Dispose HttpResponseMessage in SendFabricApiDeleteRequestAsync - Fix broken link in OneLake README (Fabric.Mcp.Tools.Core/src -> Core) - Remove incorrect --role-name param from create-or-update docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/Fabric.Mcp.Tools.OneLake/README.md | 7 +++---- .../src/Services/OneLakeService.cs | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/tools/Fabric.Mcp.Tools.OneLake/README.md b/tools/Fabric.Mcp.Tools.OneLake/README.md index 35630d4190..569b280607 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/README.md +++ b/tools/Fabric.Mcp.Tools.OneLake/README.md @@ -657,17 +657,16 @@ dotnet run -- onelake security get --workspace "Analytics Workspace" --item "Sal #### Create or Update Data Access Role -Upserts a single data access role on a single item. Scoped to one role per call — does not affect other roles. +Upserts a single data access role on a single item. Scoped to one role per call — does not affect other roles. The role name is derived from the `name` field in the JSON definition. ```bash -dotnet run -- onelake security create-or-update --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" --role-name "DataAnalysts" --role-definition '{"members":{"fabricItemMembers":[{"itemAccessType":"ReadAll"}]},"decisionRules":[{"effect":"Permit","permission":[{"attributeName":"Path","attributeValueIncludedIn":["Tables/*"]}]}]}' +dotnet run -- onelake security create-or-update --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" --role-definition '{"name":"DataAnalysts","members":{"fabricItemMembers":[{"itemAccessType":"ReadAll"}]},"decisionRules":[{"effect":"Permit","permission":[{"attributeName":"Path","attributeValueIncludedIn":["Tables/*"]}]}]}' ``` **Parameters:** - `--workspace`/`--workspace-id`: Workspace identifier - `--item`/`--item-id`: Item identifier -- `--role-name`: Name of the data access role -- `--role-definition`: JSON definition of the role (members + decision rules) +- `--role-definition`: JSON definition of the role (must include `name`, `members`, and `decisionRules`) #### Delete Data Access Role diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs index 3bf5a0d4cb..b626a5d168 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs @@ -1824,8 +1824,21 @@ public async Task GetDataAccessRoleAsync(string workspaceId, str public async Task CreateOrUpdateDataAccessRoleAsync(string workspaceId, string itemId, string roleDefinitionJson, CancellationToken cancellationToken = default) { - var roleDefinition = JsonSerializer.Deserialize(roleDefinitionJson, OneLakeJsonContext.Default.DataAccessRole) - ?? throw new ArgumentException("Invalid role definition JSON.", nameof(roleDefinitionJson)); + DataAccessRole roleDefinition; + try + { + roleDefinition = JsonSerializer.Deserialize(roleDefinitionJson, OneLakeJsonContext.Default.DataAccessRole) + ?? throw new ArgumentException("Invalid role definition JSON: deserialized to null.", nameof(roleDefinitionJson)); + } + catch (JsonException ex) + { + throw new ArgumentException($"Invalid role definition JSON: {ex.Message}", nameof(roleDefinitionJson), ex); + } + + if (string.IsNullOrWhiteSpace(roleDefinition.Name)) + { + throw new ArgumentException("Role definition must include a non-empty 'name' property.", nameof(roleDefinitionJson)); + } var encodedRoleName = Uri.EscapeDataString(roleDefinition.Name); var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/dataAccessRoles/{encodedRoleName}"; @@ -1913,7 +1926,7 @@ private async Task SendFabricApiDeleteRequestAsync(string url, CancellationToken request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); ApplyUserAgent(request); - var response = await _httpClient.SendAsync(request, cancellationToken); + using var response = await _httpClient.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); } From 0c0f48672208b5a16cdd6b09d4970faf9a366672 Mon Sep 17 00:00:00 2001 From: Srinivas Thatipamula Date: Mon, 11 May 2026 14:11:02 -0700 Subject: [PATCH 03/15] Fix broken links: use absolute GitHub URLs instead of relative paths Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/Fabric.Mcp.Tools.OneLake/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/Fabric.Mcp.Tools.OneLake/README.md b/tools/Fabric.Mcp.Tools.OneLake/README.md index 569b280607..67f4629427 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/README.md +++ b/tools/Fabric.Mcp.Tools.OneLake/README.md @@ -23,7 +23,7 @@ OneLake is Microsoft Fabric's built-in data lake that provides unified storage f - Production-ready with 100% test coverage (309 tests) - Clean, focused API design optimized for AI agent interactions -> **Note:** Item creation has moved to the [Fabric.Mcp.Tools.Core](../Fabric.Mcp.Tools.Core) toolset as `core_create_item`. See [Core tools](../Fabric.Mcp.Tools.Core) for item creation operations. +> **Note:** Item creation has moved to the [Fabric.Mcp.Tools.Core](https://github.com/microsoft/mcp/tree/main/tools/Fabric.Mcp.Tools.Core) toolset as `core_create_item`. See [Core tools](https://github.com/microsoft/mcp/tree/main/tools/Fabric.Mcp.Tools.Core) for item creation operations. ## Prerequisites @@ -180,7 +180,7 @@ dotnet run -- onelake item list-data --workspace-id "47242da5-ff3b-46fb-a94f-977 #### Create Item -> **Moved:** The `item create` command has been moved to [Fabric.Mcp.Tools.Core](../Fabric.Mcp.Tools.Core) as `core_create_item`. Use the Core toolset for creating new items (Lakehouse, Notebook, etc.) in a Microsoft Fabric workspace. +> **Moved:** The `item create` command has been moved to [Fabric.Mcp.Tools.Core](https://github.com/microsoft/mcp/tree/main/tools/Fabric.Mcp.Tools.Core) as `core_create_item`. Use the Core toolset for creating new items (Lakehouse, Notebook, etc.) in a Microsoft Fabric workspace. ### File Operations From 73d888027ee2786b3b9b78b6b799e74e73573c05 Mon Sep 17 00:00:00 2001 From: Srinivas Thatipamula Date: Mon, 11 May 2026 16:22:55 -0700 Subject: [PATCH 04/15] fix(onelake): fix bulkCreate empty response, add LRO polling support, add LRO unit tests - Fix CreateOrUpdateShortcutsAsync returning empty response when bulkCreate returns 200 with no body - Add 202 Accepted (LRO) handling to SendFabricApiRequestAsync: detect Location header, poll operations endpoint until Succeeded/Failed - Add PollFabricLroAsync and GetFabricLroResultAsync private methods with configurable retry logic - Add LroModels.cs (OperationState, OperationError) registered in OneLakeJsonContext for AOT safety - Add OneLakeServiceLroTests.cs with 6 unit tests covering: single-poll success, multi-poll success, succeeded with no result URL, failed operation throws, 202 with no Location header, synchronous 200 path - Fix data access security model bugs across security/shortcut/settings commands and tests --- .../DataAccessRoleCreateOrUpdateCommand.cs | 18 +- .../Security/DataAccessRoleDeleteCommand.cs | 18 +- .../Security/DataAccessRoleGetCommand.cs | 18 +- .../Security/DataAccessRoleListCommand.cs | 16 +- .../Settings/DiagnosticsModifyCommand.cs | 21 +- .../ImmutabilityPolicyModifyCommand.cs | 21 +- .../Commands/Settings/SettingsGetCommand.cs | 8 +- .../Shortcut/ShortcutCreateOrUpdateCommand.cs | 38 +-- .../Shortcut/ShortcutDeleteCommand.cs | 20 +- .../Commands/Shortcut/ShortcutGetCommand.cs | 20 +- .../Commands/Shortcut/ShortcutListCommand.cs | 18 +- .../Shortcut/ShortcutResetCacheCommand.cs | 29 +- .../src/Models/DataAccessRoleModels.cs | 84 ++++- .../src/Models/LroModels.cs | 40 +++ .../src/Models/OneLakeJsonContext.cs | 22 +- .../src/Models/ShortcutModels.cs | 144 ++++++++- .../src/Options/FabricOptionDefinitions.cs | 6 +- .../Options/ShortcutCreateOrUpdateOptions.cs | 2 +- .../src/Options/ShortcutResetCacheOptions.cs | 2 - .../src/Services/IOneLakeService.cs | 8 +- .../src/Services/OneLakeService.cs | 171 ++++++++-- ...ataAccessRoleCreateOrUpdateCommandTests.cs | 71 +++++ .../Settings/DiagnosticsModifyCommandTests.cs | 54 ++++ .../ImmutabilityPolicyModifyCommandTests.cs | 54 ++++ .../ShortcutCreateOrUpdateCommandTests.cs | 88 +++++- .../ShortcutResetCacheCommandTests.cs | 62 ++++ .../Services/OneLakeServiceLroTests.cs | 296 ++++++++++++++++++ 27 files changed, 1178 insertions(+), 171 deletions(-) create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Models/LroModels.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Services/OneLakeServiceLroTests.cs diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs index 3cb04c5c52..dba432d0c8 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs @@ -47,10 +47,10 @@ protected override void RegisterOptions(Command command) command.Options.Add(FabricOptionDefinitions.RoleDefinition.AsRequired()); command.Validators.Add(result => { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + var item = result.GetValueOrDefault(FabricOptionDefinitions.Item.Name); if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) { @@ -67,11 +67,11 @@ protected override void RegisterOptions(Command command) protected override DataAccessRoleCreateOrUpdateOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); - options.RoleDefinition = parseResult.GetValueOrDefault(FabricOptionDefinitions.RoleDefinitionName); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.Item.Name); + options.RoleDefinition = parseResult.GetValueOrDefault(FabricOptionDefinitions.RoleDefinition.Name); return options; } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleDeleteCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleDeleteCommand.cs index 22177680f1..8a8e18fbaa 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleDeleteCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleDeleteCommand.cs @@ -45,10 +45,10 @@ protected override void RegisterOptions(Command command) command.Options.Add(FabricOptionDefinitions.RoleName.AsRequired()); command.Validators.Add(result => { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + var item = result.GetValueOrDefault(FabricOptionDefinitions.Item.Name); if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) { @@ -65,11 +65,11 @@ protected override void RegisterOptions(Command command) protected override DataAccessRoleDeleteOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); - options.RoleName = parseResult.GetValueOrDefault(FabricOptionDefinitions.RoleNameName); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.Item.Name); + options.RoleName = parseResult.GetValueOrDefault(FabricOptionDefinitions.RoleName.Name); return options; } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs index 5fa7e32100..d5888a85fb 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs @@ -47,10 +47,10 @@ protected override void RegisterOptions(Command command) command.Options.Add(FabricOptionDefinitions.RoleName.AsRequired()); command.Validators.Add(result => { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + var item = result.GetValueOrDefault(FabricOptionDefinitions.Item.Name); if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) { @@ -67,11 +67,11 @@ protected override void RegisterOptions(Command command) protected override DataAccessRoleGetOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); - options.RoleName = parseResult.GetValueOrDefault(FabricOptionDefinitions.RoleNameName); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.Item.Name); + options.RoleName = parseResult.GetValueOrDefault(FabricOptionDefinitions.RoleName.Name); return options; } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs index a25bc85b08..86466a32fe 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs @@ -45,10 +45,10 @@ protected override void RegisterOptions(Command command) command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); command.Validators.Add(result => { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + var item = result.GetValueOrDefault(FabricOptionDefinitions.Item.Name); if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) { @@ -65,10 +65,10 @@ protected override void RegisterOptions(Command command) protected override DataAccessRoleListOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.Item.Name); return options; } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs index 9c55484165..f65a4d5c7e 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs @@ -42,8 +42,8 @@ protected override void RegisterOptions(Command command) command.Options.Add(FabricOptionDefinitions.DiagnosticsConfig.AsRequired()); command.Validators.Add(result => { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) { @@ -55,9 +55,9 @@ protected override void RegisterOptions(Command command) protected override DiagnosticsModifyOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - options.DiagnosticsConfig = parseResult.GetValueOrDefault(FabricOptionDefinitions.DiagnosticsConfigName); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + options.DiagnosticsConfig = parseResult.GetValueOrDefault(FabricOptionDefinitions.DiagnosticsConfig.Name); return options; } @@ -75,8 +75,8 @@ public override async Task ExecuteAsync(CommandContext context, ? options.WorkspaceId : options.Workspace; - var result = await _oneLakeService.ModifyDiagnosticsAsync(workspaceIdentifier!, options.DiagnosticsConfig!, cancellationToken); - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeSettings); + await _oneLakeService.ModifyDiagnosticsAsync(workspaceIdentifier!, options.DiagnosticsConfig!, cancellationToken); + context.Response.Results = ResponseResult.Create(new DiagnosticsModifyCommandResult("Diagnostics settings modified successfully."), OneLakeJsonContext.Default.DiagnosticsModifyCommandResult); } catch (Exception ex) { @@ -86,4 +86,11 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed class DiagnosticsModifyCommandResult + { + public string Message { get; init; } = string.Empty; + public DiagnosticsModifyCommandResult() { } + public DiagnosticsModifyCommandResult(string message) { Message = message; } + } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs index 94152b5b93..d65ddb10a2 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs @@ -41,8 +41,8 @@ protected override void RegisterOptions(Command command) command.Options.Add(FabricOptionDefinitions.ImmutabilityPolicyConfig.AsRequired()); command.Validators.Add(result => { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) { @@ -54,9 +54,9 @@ protected override void RegisterOptions(Command command) protected override ImmutabilityPolicyModifyOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - options.ImmutabilityPolicyConfig = parseResult.GetValueOrDefault(FabricOptionDefinitions.ImmutabilityPolicyConfigName); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + options.ImmutabilityPolicyConfig = parseResult.GetValueOrDefault(FabricOptionDefinitions.ImmutabilityPolicyConfig.Name); return options; } @@ -74,8 +74,8 @@ public override async Task ExecuteAsync(CommandContext context, ? options.WorkspaceId : options.Workspace; - var result = await _oneLakeService.ModifyImmutabilityPolicyAsync(workspaceIdentifier!, options.ImmutabilityPolicyConfig!, cancellationToken); - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeSettings); + await _oneLakeService.ModifyImmutabilityPolicyAsync(workspaceIdentifier!, options.ImmutabilityPolicyConfig!, cancellationToken); + context.Response.Results = ResponseResult.Create(new ImmutabilityPolicyModifyCommandResult("Immutability policy modified successfully."), OneLakeJsonContext.Default.ImmutabilityPolicyModifyCommandResult); } catch (Exception ex) { @@ -85,4 +85,11 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed class ImmutabilityPolicyModifyCommandResult + { + public string Message { get; init; } = string.Empty; + public ImmutabilityPolicyModifyCommandResult() { } + public ImmutabilityPolicyModifyCommandResult(string message) { Message = message; } + } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs index 45d607b7e6..10e65d4c92 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs @@ -40,8 +40,8 @@ protected override void RegisterOptions(Command command) command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); command.Validators.Add(result => { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) { @@ -53,8 +53,8 @@ protected override void RegisterOptions(Command command) protected override SettingsGetOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); return options; } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOrUpdateCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOrUpdateCommand.cs index a04c41ac45..73144171f5 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOrUpdateCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOrUpdateCommand.cs @@ -16,11 +16,13 @@ namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; Name = "create_or_update_shortcuts", Title = "Create or Update OneLake Shortcuts", Description = """ - Create one or more shortcuts in a single call (the underlying API is - bulk-only — there is no separate single-shortcut create). By default, - fails if any shortcut already exists; pass createOrOverwrite=true to - upsert. Use this for both initial creation and updates. Requires - OneLake.ReadWrite.All. + Create one or more shortcuts in a single call using the bulk create API + (POST /shortcuts/bulkCreate). Pass a JSON object with a + "createShortcutRequests" array — one entry for a single shortcut, many + entries for bulk. Use --shortcut-conflict-policy to control behaviour + when a shortcut with the same name and path already exists: Abort + (default), CreateOrOverwrite, OverwriteOnly, or GenerateUniqueName. + Requires OneLake.ReadWrite.All. """, Destructive = false, Idempotent = false, @@ -43,13 +45,13 @@ protected override void RegisterOptions(Command command) command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); command.Options.Add(FabricOptionDefinitions.ShortcutsDefinition.AsRequired()); - command.Options.Add(FabricOptionDefinitions.CreateOrOverwrite.AsOptional()); + command.Options.Add(FabricOptionDefinitions.ShortcutConflictPolicy.AsOptional()); command.Validators.Add(result => { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + var item = result.GetValueOrDefault(FabricOptionDefinitions.Item.Name); if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) { @@ -66,12 +68,12 @@ protected override void RegisterOptions(Command command) protected override ShortcutCreateOrUpdateOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); - options.ShortcutsDefinition = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutsDefinitionName); - options.CreateOrOverwrite = parseResult.GetValueOrDefault(FabricOptionDefinitions.CreateOrOverwriteName); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.Item.Name); + options.ShortcutsDefinition = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutsDefinition.Name); + options.ShortcutConflictPolicy = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutConflictPolicy.Name); return options; } @@ -93,8 +95,8 @@ public override async Task ExecuteAsync(CommandContext context, ? options.ItemId : options.Item; - var result = await _oneLakeService.CreateOrUpdateShortcutsAsync(workspaceIdentifier!, itemIdentifier!, options.ShortcutsDefinition!, options.CreateOrOverwrite, cancellationToken); - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.ShortcutCreateOrUpdateResponse); + var result = await _oneLakeService.CreateOrUpdateShortcutsAsync(workspaceIdentifier!, itemIdentifier!, options.ShortcutsDefinition!, options.ShortcutConflictPolicy, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.BulkCreateShortcutResponse); } catch (Exception ex) { diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutDeleteCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutDeleteCommand.cs index eb9ec3de93..6a6abf291f 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutDeleteCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutDeleteCommand.cs @@ -44,10 +44,10 @@ protected override void RegisterOptions(Command command) command.Options.Add(FabricOptionDefinitions.ShortcutName.AsRequired()); command.Validators.Add(result => { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + var item = result.GetValueOrDefault(FabricOptionDefinitions.Item.Name); if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) { @@ -64,12 +64,12 @@ protected override void RegisterOptions(Command command) protected override ShortcutDeleteOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); - options.ShortcutPath = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPathName); - options.ShortcutName = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutNameName); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.Item.Name); + options.ShortcutPath = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPath.Name); + options.ShortcutName = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutName.Name); return options; } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutGetCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutGetCommand.cs index f83864fb9c..84d28d61c6 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutGetCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutGetCommand.cs @@ -43,10 +43,10 @@ protected override void RegisterOptions(Command command) command.Options.Add(FabricOptionDefinitions.ShortcutName.AsRequired()); command.Validators.Add(result => { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + var item = result.GetValueOrDefault(FabricOptionDefinitions.Item.Name); if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) { @@ -63,12 +63,12 @@ protected override void RegisterOptions(Command command) protected override ShortcutGetOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); - options.ShortcutPath = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPathName); - options.ShortcutName = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutNameName); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.Item.Name); + options.ShortcutPath = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPath.Name); + options.ShortcutName = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutName.Name); return options; } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs index 72e7ac5c92..2afea6519c 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs @@ -42,10 +42,10 @@ protected override void RegisterOptions(Command command) command.Options.Add(FabricOptionDefinitions.ParentPath.AsOptional()); command.Validators.Add(result => { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + var item = result.GetValueOrDefault(FabricOptionDefinitions.Item.Name); if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) { @@ -62,11 +62,11 @@ protected override void RegisterOptions(Command command) protected override ShortcutListOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); - options.ParentPath = parseResult.GetValueOrDefault(FabricOptionDefinitions.ParentPathName); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.Item.Name); + options.ParentPath = parseResult.GetValueOrDefault(FabricOptionDefinitions.ParentPath.Name); return options; } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutResetCacheCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutResetCacheCommand.cs index 31fdaa1070..fdc677570b 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutResetCacheCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutResetCacheCommand.cs @@ -38,34 +38,23 @@ protected override void RegisterOptions(Command command) base.RegisterOptions(command); command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); - command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); command.Validators.Add(result => { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - var item = result.GetValueOrDefault(FabricOptionDefinitions.ItemName); + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) { result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); } - - if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) - { - result.AddError("Item identifier is required. Provide --item or --item-id."); - } }); } protected override ShortcutResetCacheOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceIdName); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceName); - options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemIdName); - options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemName); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); return options; } @@ -83,18 +72,14 @@ public override async Task ExecuteAsync(CommandContext context, ? options.WorkspaceId : options.Workspace; - var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) - ? options.ItemId - : options.Item; - - await _oneLakeService.ResetShortcutCacheAsync(workspaceIdentifier!, itemIdentifier!, cancellationToken); + await _oneLakeService.ResetShortcutCacheAsync(workspaceIdentifier!, cancellationToken); var result = new ShortcutResetCacheCommandResult("Shortcut cache reset successfully."); context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.ShortcutResetCacheCommandResult); } catch (Exception ex) { - _logger.LogError(ex, "Error resetting shortcut cache. Workspace: {Workspace}, Item: {Item}.", - options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item); + _logger.LogError(ex, "Error resetting shortcut cache. Workspace: {Workspace}.", + options.WorkspaceId ?? options.Workspace); HandleException(context, ex); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Models/DataAccessRoleModels.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Models/DataAccessRoleModels.cs index 3febbb4bd9..61b0200446 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Models/DataAccessRoleModels.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Models/DataAccessRoleModels.cs @@ -14,8 +14,17 @@ public class DataAccessRole public string Name { get; set; } = string.Empty; [JsonPropertyName("id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Id { get; set; } + [JsonPropertyName("eTag")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ETag { get; set; } + + [JsonPropertyName("kind")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Kind { get; set; } + [JsonPropertyName("decisionRules")] public List? DecisionRules { get; set; } @@ -32,14 +41,59 @@ public class DecisionRule public string? Effect { get; set; } [JsonPropertyName("permission")] - public string? Permission { get; set; } + public List? Permission { get; set; } + + [JsonPropertyName("constraints")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Constraints? Constraints { get; set; } +} + +/// +/// Row and column constraints for a decision rule (row/column level security). +/// +public class Constraints +{ + [JsonPropertyName("columns")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Columns { get; set; } + + [JsonPropertyName("rows")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Rows { get; set; } +} + +/// +/// Column-level security constraint for a specific table. +/// +public class ColumnConstraint +{ + [JsonPropertyName("tablePath")] + public string? TablePath { get; set; } + + [JsonPropertyName("columnNames")] + public List? ColumnNames { get; set; } + + [JsonPropertyName("columnEffect")] + public string? ColumnEffect { get; set; } - [JsonPropertyName("scope")] - public List? Scope { get; set; } + [JsonPropertyName("columnAction")] + public List? ColumnAction { get; set; } } /// -/// Scope definition for a decision rule. +/// Row-level security constraint for a specific table using a T-SQL predicate. +/// +public class RowConstraint +{ + [JsonPropertyName("tablePath")] + public string? TablePath { get; set; } + + [JsonPropertyName("value")] + public string? Value { get; set; } +} + +/// +/// Scope definition for a decision rule permission. /// public class DecisionRuleScope { @@ -67,8 +121,11 @@ public class DataAccessRoleMembers /// public class FabricItemMember { - [JsonPropertyName("sourceItemId")] - public string? SourceItemId { get; set; } + [JsonPropertyName("sourcePath")] + public string? SourcePath { get; set; } + + [JsonPropertyName("itemAccess")] + public List? ItemAccess { get; set; } } /// @@ -79,6 +136,10 @@ public class MicrosoftEntraMember [JsonPropertyName("objectId")] public string? ObjectId { get; set; } + [JsonPropertyName("objectType")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ObjectType { get; set; } + [JsonPropertyName("tenantId")] public string? TenantId { get; set; } } @@ -92,8 +153,19 @@ public class DataAccessRoleListResponse public List? Value { get; set; } [JsonPropertyName("continuationToken")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? ContinuationToken { get; set; } [JsonPropertyName("continuationUri")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? ContinuationUri { get; set; } } + +/// +/// Request body for the PUT Data Access Roles API (bulk replace). Only 'value' is accepted. +/// +public class DataAccessRolePutRequest +{ + [JsonPropertyName("value")] + public List Value { get; set; } = []; +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Models/LroModels.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Models/LroModels.cs new file mode 100644 index 0000000000..d19c2d0fd4 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Models/LroModels.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Fabric.Mcp.Tools.OneLake.Models; + +/// +/// Describes the current state of a Fabric long running operation. +/// Returned by GET /v1/operations/{operationId}. +/// +public class OperationState +{ + [JsonPropertyName("status")] + public string? Status { get; set; } + + [JsonPropertyName("percentComplete")] + public int? PercentComplete { get; set; } + + [JsonPropertyName("createdTimeUtc")] + public string? CreatedTimeUtc { get; set; } + + [JsonPropertyName("lastUpdatedTimeUtc")] + public string? LastUpdatedTimeUtc { get; set; } + + [JsonPropertyName("error")] + public OperationError? Error { get; set; } +} + +/// +/// Error details returned when a long running operation fails. +/// +public class OperationError +{ + [JsonPropertyName("errorCode")] + public string? ErrorCode { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs index 77ced0173a..a2208749bf 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs @@ -68,9 +68,14 @@ namespace Fabric.Mcp.Tools.OneLake.Models; // Data Access Security types [JsonSerializable(typeof(DataAccessRole))] [JsonSerializable(typeof(DataAccessRoleListResponse))] +[JsonSerializable(typeof(DataAccessRolePutRequest))] [JsonSerializable(typeof(DataAccessRoleMembers))] [JsonSerializable(typeof(DecisionRule))] [JsonSerializable(typeof(DecisionRuleScope))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(Constraints))] +[JsonSerializable(typeof(ColumnConstraint))] +[JsonSerializable(typeof(RowConstraint))] [JsonSerializable(typeof(FabricItemMember))] [JsonSerializable(typeof(MicrosoftEntraMember))] [JsonSerializable(typeof(DataAccessRoleDeleteCommand.DataAccessRoleDeleteCommandResult))] @@ -84,11 +89,21 @@ namespace Fabric.Mcp.Tools.OneLake.Models; [JsonSerializable(typeof(DataverseShortcutTarget))] [JsonSerializable(typeof(S3CompatibleShortcutTarget))] [JsonSerializable(typeof(ExternalDataShareShortcutTarget))] +[JsonSerializable(typeof(AzureBlobStorageShortcutTarget))] +[JsonSerializable(typeof(OneDriveSharePointShortcutTarget))] [JsonSerializable(typeof(ShortcutListResponse))] -[JsonSerializable(typeof(ShortcutCreateOrUpdateRequest))] -[JsonSerializable(typeof(ShortcutCreateOrUpdateResponse))] +[JsonSerializable(typeof(BulkCreateShortcutsRequest))] +[JsonSerializable(typeof(CreateShortcutWithTransformRequest))] +[JsonSerializable(typeof(CsvToDeltaTransform))] +[JsonSerializable(typeof(CsvToDeltaTransformProperties))] +[JsonSerializable(typeof(BulkCreateShortcutResponse))] +[JsonSerializable(typeof(CreateShortcutResponse))] +[JsonSerializable(typeof(ShortcutRequestInfo))] +[JsonSerializable(typeof(ShortcutCreateError))] [JsonSerializable(typeof(ShortcutDeleteCommand.ShortcutDeleteCommandResult))] [JsonSerializable(typeof(ShortcutResetCacheCommand.ShortcutResetCacheCommandResult))] +[JsonSerializable(typeof(DiagnosticsModifyCommand.DiagnosticsModifyCommandResult))] +[JsonSerializable(typeof(ImmutabilityPolicyModifyCommand.ImmutabilityPolicyModifyCommandResult))] // Settings types [JsonSerializable(typeof(OneLakeSettings))] [JsonSerializable(typeof(DiagnosticsSettings))] @@ -96,5 +111,8 @@ namespace Fabric.Mcp.Tools.OneLake.Models; [JsonSerializable(typeof(ImmutabilityPolicySettings))] [JsonSerializable(typeof(DiagnosticsModifyRequest))] [JsonSerializable(typeof(ImmutabilityPolicyModifyRequest))] +// Long running operation types +[JsonSerializable(typeof(OperationState))] +[JsonSerializable(typeof(OperationError))] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, WriteIndented = true)] internal partial class OneLakeJsonContext : JsonSerializerContext; diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Models/ShortcutModels.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Models/ShortcutModels.cs index d4cd7f0755..f9edf02ff2 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Models/ShortcutModels.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Models/ShortcutModels.cs @@ -48,6 +48,12 @@ public class ShortcutTarget [JsonPropertyName("externalDataShare")] public ExternalDataShareShortcutTarget? ExternalDataShare { get; set; } + + [JsonPropertyName("azureBlobStorage")] + public AzureBlobStorageShortcutTarget? AzureBlobStorage { get; set; } + + [JsonPropertyName("oneDriveSharePoint")] + public OneDriveSharePointShortcutTarget? OneDriveSharePoint { get; set; } } /// @@ -149,6 +155,39 @@ public class ExternalDataShareShortcutTarget public string? ConnectionId { get; set; } } +/// +/// Azure Blob Storage shortcut target. +/// +public class AzureBlobStorageShortcutTarget +{ + [JsonPropertyName("location")] + public string? Location { get; set; } + + [JsonPropertyName("subpath")] + public string? Subpath { get; set; } + + [JsonPropertyName("connectionId")] + public string? ConnectionId { get; set; } +} + +/// +/// OneDrive / SharePoint Online shortcut target. +/// +public class OneDriveSharePointShortcutTarget +{ + [JsonPropertyName("location")] + public string? Location { get; set; } + + [JsonPropertyName("subpath")] + public string? Subpath { get; set; } + + [JsonPropertyName("connectionId")] + public string? ConnectionId { get; set; } + + [JsonPropertyName("updateFabricItemSensitivity")] + public bool? UpdateFabricItemSensitivity { get; set; } +} + /// /// Response from the List Shortcuts API. /// @@ -165,22 +204,109 @@ public class ShortcutListResponse } /// -/// Request body for the Create Or Update Shortcuts API. +/// Request body for the bulk create shortcuts API (POST /shortcuts/bulkCreate). /// -public class ShortcutCreateOrUpdateRequest +public class BulkCreateShortcutsRequest { - [JsonPropertyName("shortcuts")] - public List? Shortcuts { get; set; } + [JsonPropertyName("createShortcutRequests")] + public List? CreateShortcutRequests { get; set; } +} + +/// +/// A single shortcut creation request (with optional transform). +/// +public class CreateShortcutWithTransformRequest +{ + [JsonPropertyName("path")] + public string? Path { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("target")] + public ShortcutTarget? Target { get; set; } - [JsonPropertyName("createOrOverwrite")] - public bool? CreateOrOverwrite { get; set; } + [JsonPropertyName("transform")] + public CsvToDeltaTransform? Transform { get; set; } } /// -/// Response from the Create Or Update Shortcuts API. +/// CSV-to-Delta transform definition. /// -public class ShortcutCreateOrUpdateResponse +public class CsvToDeltaTransform +{ + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("includeSubfolders")] + public bool? IncludeSubfolders { get; set; } + + [JsonPropertyName("properties")] + public CsvToDeltaTransformProperties? Properties { get; set; } +} + +/// +/// Properties for the CSV-to-Delta transform. +/// +public class CsvToDeltaTransformProperties +{ + [JsonPropertyName("delimiter")] + public string? Delimiter { get; set; } + + [JsonPropertyName("skipFilesWithErrors")] + public bool? SkipFilesWithErrors { get; set; } + + [JsonPropertyName("useFirstRowAsHeader")] + public bool? UseFirstRowAsHeader { get; set; } +} + +/// +/// Response from POST /shortcuts/bulkCreate. +/// +public class BulkCreateShortcutResponse { [JsonPropertyName("value")] - public List? Value { get; set; } + public List? Value { get; set; } +} + +/// +/// Per-shortcut result in a bulk create response. +/// +public class CreateShortcutResponse +{ + [JsonPropertyName("request")] + public ShortcutRequestInfo? Request { get; set; } + + [JsonPropertyName("result")] + public OneLakeShortcut? Result { get; set; } + + [JsonPropertyName("status")] + public string? Status { get; set; } + + [JsonPropertyName("error")] + public ShortcutCreateError? Error { get; set; } +} + +/// +/// Original name/path echoed back in a bulk create response item. +/// +public class ShortcutRequestInfo +{ + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("path")] + public string? Path { get; set; } +} + +/// +/// Error details for a failed shortcut in a bulk create response. +/// +public class ShortcutCreateError +{ + [JsonPropertyName("errorCode")] + public string? ErrorCode { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs index 63948593ca..f43c8047a9 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs @@ -216,10 +216,10 @@ public static class FabricOptionDefinitions Required = false }; - public const string CreateOrOverwriteName = "create-or-overwrite"; - public static readonly Option CreateOrOverwrite = new($"--{CreateOrOverwriteName}") + public const string ShortcutConflictPolicyName = "shortcut-conflict-policy"; + public static readonly Option ShortcutConflictPolicy = new($"--{ShortcutConflictPolicyName}") { - Description = "When true, overwrites existing shortcuts. When false (default), fails if a shortcut already exists.", + Description = "Action when a shortcut with the same name and path already exists. One of: Abort (default), CreateOrOverwrite, OverwriteOnly, GenerateUniqueName.", Required = false }; diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOrUpdateOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOrUpdateOptions.cs index e3e203a82b..12a684c4ef 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOrUpdateOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOrUpdateOptions.cs @@ -12,5 +12,5 @@ public sealed class ShortcutCreateOrUpdateOptions : GlobalOptions public string? ItemId { get; set; } public string? Item { get; set; } public string? ShortcutsDefinition { get; set; } - public bool CreateOrOverwrite { get; set; } + public string? ShortcutConflictPolicy { get; set; } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutResetCacheOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutResetCacheOptions.cs index cd4cbb33e8..763ab04d6b 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutResetCacheOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutResetCacheOptions.cs @@ -9,6 +9,4 @@ public sealed class ShortcutResetCacheOptions : GlobalOptions { public string? WorkspaceId { get; set; } public string? Workspace { get; set; } - public string? ItemId { get; set; } - public string? Item { get; set; } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs index 57c05e7720..a3d11c3da7 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs @@ -56,12 +56,12 @@ public interface IOneLakeService // Shortcut Operations Task ListShortcutsAsync(string workspaceId, string itemId, string? parentPath = null, CancellationToken cancellationToken = default); Task GetShortcutAsync(string workspaceId, string itemId, string shortcutPath, string shortcutName, CancellationToken cancellationToken = default); - Task CreateOrUpdateShortcutsAsync(string workspaceId, string itemId, string shortcutsJson, bool createOrOverwrite = false, CancellationToken cancellationToken = default); + Task CreateOrUpdateShortcutsAsync(string workspaceId, string itemId, string shortcutsJson, string? shortcutConflictPolicy = null, CancellationToken cancellationToken = default); Task DeleteShortcutAsync(string workspaceId, string itemId, string shortcutPath, string shortcutName, CancellationToken cancellationToken = default); - Task ResetShortcutCacheAsync(string workspaceId, string itemId, CancellationToken cancellationToken = default); + Task ResetShortcutCacheAsync(string workspaceId, CancellationToken cancellationToken = default); // Settings Operations Task GetSettingsAsync(string workspaceId, CancellationToken cancellationToken = default); - Task ModifyDiagnosticsAsync(string workspaceId, string diagnosticsConfigJson, CancellationToken cancellationToken = default); - Task ModifyImmutabilityPolicyAsync(string workspaceId, string immutabilityPolicyJson, CancellationToken cancellationToken = default); + Task ModifyDiagnosticsAsync(string workspaceId, string diagnosticsConfigJson, CancellationToken cancellationToken = default); + Task ModifyImmutabilityPolicyAsync(string workspaceId, string immutabilityPolicyJson, CancellationToken cancellationToken = default); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs index b626a5d168..b650f51cc9 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs @@ -1661,7 +1661,115 @@ private async Task SendFabricApiRequestAsync(HttpMethod method, string u } var response = await _httpClient.SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); + + // Handle long running operations (202 Accepted) + if (response.StatusCode == System.Net.HttpStatusCode.Accepted) + { + var locationUrl = response.Headers.Location?.ToString(); + if (!string.IsNullOrEmpty(locationUrl)) + { + return await PollFabricLroAsync(locationUrl, cancellationToken) ?? Stream.Null; + } + return Stream.Null; + } + + if (!response.IsSuccessStatusCode) + { + var errorBody = await response.Content.ReadAsStringAsync(cancellationToken); + throw new HttpRequestException( + $"Response status code does not indicate success: {(int)response.StatusCode} ({response.ReasonPhrase}). Error details: {errorBody}", + null, + response.StatusCode); + } + + return await response.Content.ReadAsStreamAsync(cancellationToken); + } + + /// + /// Polls a Fabric long running operation URL until it succeeds or fails, + /// then fetches and returns the result stream. + /// + private async Task PollFabricLroAsync(string operationUrl, CancellationToken cancellationToken) + { + const int MaxAttempts = 120; // ~10 minutes at 5s default + var retryDelay = TimeSpan.FromSeconds(5); + + for (var attempt = 0; attempt < MaxAttempts; attempt++) + { + await Task.Delay(retryDelay, cancellationToken); + + var tokenContext = new TokenRequestContext(new[] { OneLakeEndpoints.GetFabricScope() }); + var token = await _credential.GetTokenAsync(tokenContext, cancellationToken); + + using var pollRequest = new HttpRequestMessage(HttpMethod.Get, operationUrl); + pollRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); + ApplyUserAgent(pollRequest); + + var pollResponse = await _httpClient.SendAsync(pollRequest, cancellationToken); + if (!pollResponse.IsSuccessStatusCode) + { + var errorBody = await pollResponse.Content.ReadAsStringAsync(cancellationToken); + throw new HttpRequestException( + $"LRO polling failed: {(int)pollResponse.StatusCode} ({pollResponse.ReasonPhrase}). {errorBody}", + null, + pollResponse.StatusCode); + } + + // Update retry delay from Retry-After header + if (pollResponse.Headers.TryGetValues("Retry-After", out var retryAfterValues) + && int.TryParse(retryAfterValues.FirstOrDefault(), out var retryAfterSecs)) + { + retryDelay = TimeSpan.FromSeconds(Math.Max(1, retryAfterSecs)); + } + + var stateStream = await pollResponse.Content.ReadAsStreamAsync(cancellationToken); + var state = await JsonSerializer.DeserializeAsync(stateStream, OneLakeJsonContext.Default.OperationState, cancellationToken); + + switch (state?.Status) + { + case "Succeeded": + // Location header on the completed poll response points to the result URL + var resultUrl = pollResponse.Headers.Location?.ToString(); + if (!string.IsNullOrEmpty(resultUrl)) + { + return await GetFabricLroResultAsync(resultUrl, cancellationToken); + } + return Stream.Null; + + case "Failed": + var errorMessage = state.Error?.Message ?? "Long running operation failed."; + var errorCode = state.Error?.ErrorCode ?? "Unknown"; + throw new HttpRequestException( + $"Long running operation failed ({errorCode}): {errorMessage}", + null, + System.Net.HttpStatusCode.InternalServerError); + } + // NotStarted / Running / Undefined: keep polling + } + + throw new HttpRequestException( + "Long running operation timed out after maximum polling attempts.", + null, + System.Net.HttpStatusCode.RequestTimeout); + } + + /// + /// GETs the result URL of a completed Fabric long running operation. + /// + private async Task GetFabricLroResultAsync(string resultUrl, CancellationToken cancellationToken) + { + var tokenContext = new TokenRequestContext(new[] { OneLakeEndpoints.GetFabricScope() }); + var token = await _credential.GetTokenAsync(tokenContext, cancellationToken); + + using var request = new HttpRequestMessage(HttpMethod.Get, resultUrl); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); + ApplyUserAgent(request); + + var response = await _httpClient.SendAsync(request, cancellationToken); + if (!response.IsSuccessStatusCode) + { + return Stream.Null; + } return await response.Content.ReadAsStreamAsync(cancellationToken); } @@ -1817,7 +1925,7 @@ public async Task ListDataAccessRolesAsync(string wo public async Task GetDataAccessRoleAsync(string workspaceId, string itemId, string roleName, CancellationToken cancellationToken = default) { var encodedRoleName = Uri.EscapeDataString(roleName); - var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/dataAccessRoles/{encodedRoleName}"; + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/dataAccessRoles/{encodedRoleName}?preview=true"; var response = await SendFabricApiRequestAsync(HttpMethod.Get, url, cancellationToken: cancellationToken); return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.DataAccessRole, cancellationToken) ?? new DataAccessRole(); } @@ -1840,16 +1948,19 @@ public async Task CreateOrUpdateDataAccessRoleAsync(string works throw new ArgumentException("Role definition must include a non-empty 'name' property.", nameof(roleDefinitionJson)); } - var encodedRoleName = Uri.EscapeDataString(roleDefinition.Name); - var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/dataAccessRoles/{encodedRoleName}"; - var response = await SendFabricApiRequestAsync(HttpMethod.Put, url, roleDefinitionJson, cancellationToken: cancellationToken); - return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.DataAccessRole, cancellationToken) ?? new DataAccessRole(); + // Use the single-role POST endpoint (preview API) with Overwrite conflict policy. + // This creates the role if it doesn't exist, or replaces it if it does — without + // touching other roles on the item (unlike the bulk PUT approach). + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/dataAccessRoles?preview=true&dataAccessRoleConflictPolicy=Overwrite"; + var requestBody = JsonSerializer.Serialize(roleDefinition, OneLakeJsonContext.Default.DataAccessRole); + await SendFabricApiRequestAsync(HttpMethod.Post, url, requestBody, cancellationToken: cancellationToken); + return roleDefinition; } public async Task DeleteDataAccessRoleAsync(string workspaceId, string itemId, string roleName, CancellationToken cancellationToken = default) { var encodedRoleName = Uri.EscapeDataString(roleName); - var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/dataAccessRoles/{encodedRoleName}"; + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/dataAccessRoles/{encodedRoleName}?preview=true"; await SendFabricApiDeleteRequestAsync(url, cancellationToken); } @@ -1874,11 +1985,31 @@ public async Task GetShortcutAsync(string workspaceId, string i return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.OneLakeShortcut, cancellationToken) ?? new OneLakeShortcut(); } - public async Task CreateOrUpdateShortcutsAsync(string workspaceId, string itemId, string shortcutsJson, bool createOrOverwrite = false, CancellationToken cancellationToken = default) + public async Task CreateOrUpdateShortcutsAsync(string workspaceId, string itemId, string shortcutsJson, string? shortcutConflictPolicy = null, CancellationToken cancellationToken = default) { - var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/shortcuts?createOrOverwrite={createOrOverwrite.ToString().ToLowerInvariant()}"; - var response = await SendFabricApiRequestAsync(HttpMethod.Post, url, shortcutsJson, cancellationToken: cancellationToken); - return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.ShortcutCreateOrUpdateResponse, cancellationToken) ?? new ShortcutCreateOrUpdateResponse(); + BulkCreateShortcutsRequest request; + try + { + request = JsonSerializer.Deserialize(shortcutsJson, OneLakeJsonContext.Default.BulkCreateShortcutsRequest) + ?? throw new ArgumentException("Invalid shortcuts JSON: deserialized to null.", nameof(shortcutsJson)); + } + catch (JsonException ex) + { + throw new ArgumentException($"Invalid shortcuts JSON: {ex.Message}", nameof(shortcutsJson), ex); + } + + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/shortcuts/bulkCreate"; + if (!string.IsNullOrWhiteSpace(shortcutConflictPolicy)) + url += $"?shortcutConflictPolicy={Uri.EscapeDataString(shortcutConflictPolicy)}"; + + var body = JsonSerializer.Serialize(request, OneLakeJsonContext.Default.BulkCreateShortcutsRequest); + var response = await SendFabricApiRequestAsync(HttpMethod.Post, url, body, cancellationToken: cancellationToken); + using var reader = new StreamReader(response); + var responseBody = await reader.ReadToEndAsync(cancellationToken); + if (string.IsNullOrWhiteSpace(responseBody)) + return new BulkCreateShortcutResponse(); + return JsonSerializer.Deserialize(responseBody, OneLakeJsonContext.Default.BulkCreateShortcutResponse) + ?? new BulkCreateShortcutResponse(); } public async Task DeleteShortcutAsync(string workspaceId, string itemId, string shortcutPath, string shortcutName, CancellationToken cancellationToken = default) @@ -1889,9 +2020,9 @@ public async Task DeleteShortcutAsync(string workspaceId, string itemId, string await SendFabricApiDeleteRequestAsync(url, cancellationToken); } - public async Task ResetShortcutCacheAsync(string workspaceId, string itemId, CancellationToken cancellationToken = default) + public async Task ResetShortcutCacheAsync(string workspaceId, CancellationToken cancellationToken = default) { - var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/shortcuts/resetCache"; + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/onelake/resetShortcutCache"; await SendFabricApiRequestAsync(HttpMethod.Post, url, cancellationToken: cancellationToken); } @@ -1903,18 +2034,16 @@ public async Task GetSettingsAsync(string workspaceId, Cancella return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.OneLakeSettings, cancellationToken) ?? new OneLakeSettings(); } - public async Task ModifyDiagnosticsAsync(string workspaceId, string diagnosticsConfigJson, CancellationToken cancellationToken = default) + public async Task ModifyDiagnosticsAsync(string workspaceId, string diagnosticsConfigJson, CancellationToken cancellationToken = default) { - var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/onelake/settings/diagnostics"; - var response = await SendFabricApiRequestAsync(new HttpMethod("PATCH"), url, diagnosticsConfigJson, cancellationToken: cancellationToken); - return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.OneLakeSettings, cancellationToken) ?? new OneLakeSettings(); + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/onelake/settings/modifyDiagnostics"; + await SendFabricApiRequestAsync(HttpMethod.Post, url, diagnosticsConfigJson, cancellationToken: cancellationToken); } - public async Task ModifyImmutabilityPolicyAsync(string workspaceId, string immutabilityPolicyJson, CancellationToken cancellationToken = default) + public async Task ModifyImmutabilityPolicyAsync(string workspaceId, string immutabilityPolicyJson, CancellationToken cancellationToken = default) { - var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/onelake/settings/immutabilityPolicy"; - var response = await SendFabricApiRequestAsync(new HttpMethod("PATCH"), url, immutabilityPolicyJson, cancellationToken: cancellationToken); - return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.OneLakeSettings, cancellationToken) ?? new OneLakeSettings(); + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/onelake/settings/modifyImmutabilityPolicy"; + await SendFabricApiRequestAsync(HttpMethod.Post, url, immutabilityPolicyJson, cancellationToken: cancellationToken); } private async Task SendFabricApiDeleteRequestAsync(string url, CancellationToken cancellationToken) diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleCreateOrUpdateCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleCreateOrUpdateCommandTests.cs index ba1cb05077..30c8a4687a 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleCreateOrUpdateCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleCreateOrUpdateCommandTests.cs @@ -1,9 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Net; using Fabric.Mcp.Tools.OneLake.Commands.Security; +using Fabric.Mcp.Tools.OneLake.Models; using Fabric.Mcp.Tools.OneLake.Services; using Microsoft.Mcp.Tests.Client; +using NSubstitute; +using NSubstitute.ExceptionExtensions; namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Security; @@ -52,4 +56,71 @@ public void Metadata_HasCorrectProperties() Assert.False(metadata.ReadOnly); Assert.False(metadata.Secret); } + + private const string ValidRoleJson = "{\"name\":\"TestRole\",\"decisionRules\":[{\"effect\":\"Permit\",\"permission\":[{\"attributeName\":\"Action\",\"attributeValueIncludedIn\":[\"Read\"]},{\"attributeName\":\"Path\",\"attributeValueIncludedIn\":[\"*\"]}]}],\"members\":{\"fabricItemMembers\":[],\"microsoftEntraMembers\":[]}}"; + + [Theory] + [InlineData("--workspace-id ws1 --item-id item1", true)] + [InlineData("--workspace ws1 --item item1", true)] + [InlineData("--item-id item1", false)] // missing workspace + [InlineData("--workspace-id ws1", false)] // missing item + [InlineData("", false)] + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + var roleJson = ValidRoleJson; + if (shouldSucceed) + { + Service.CreateOrUpdateDataAccessRoleAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(new DataAccessRole { Name = "TestRole" }); + } + + var fullArgs = string.IsNullOrWhiteSpace(args) + ? $"--role-definition {roleJson}" + : $"{args} --role-definition {roleJson}"; + + var response = await ExecuteCommandAsync(fullArgs); + + Assert.NotNull(response); + if (shouldSucceed) + Assert.Equal(HttpStatusCode.OK, response.Status); + else + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + + [Fact] + public async Task ExecuteAsync_SuccessfulUpsert_ReturnsRole() + { + var expected = new DataAccessRole + { + Name = "TestRole", + DecisionRules = [new DecisionRule { Effect = "Permit" }] + }; + + Service.CreateOrUpdateDataAccessRoleAsync("ws1", "item1", Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync( + "--workspace-id", "ws1", + "--item-id", "item1", + "--role-definition", ValidRoleJson); + + var result = ValidateAndDeserializeResponse(response, OneLakeJsonContext.Default.DataAccessRole); + Assert.Equal("TestRole", result.Name); + await Service.Received(1).CreateOrUpdateDataAccessRoleAsync("ws1", "item1", Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + Service.CreateOrUpdateDataAccessRoleAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new HttpRequestException("Bad request")); + + var response = await ExecuteCommandAsync( + "--workspace-id", "ws1", + "--item-id", "item1", + "--role-definition", ValidRoleJson); + + Assert.NotNull(response); + Assert.NotEqual(HttpStatusCode.OK, response.Status); + } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/DiagnosticsModifyCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/DiagnosticsModifyCommandTests.cs index 6bac062595..cffca59938 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/DiagnosticsModifyCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/DiagnosticsModifyCommandTests.cs @@ -1,9 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Net; using Fabric.Mcp.Tools.OneLake.Commands.Settings; +using Fabric.Mcp.Tools.OneLake.Models; using Fabric.Mcp.Tools.OneLake.Services; using Microsoft.Mcp.Tests.Client; +using NSubstitute; +using NSubstitute.ExceptionExtensions; namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Settings; @@ -52,4 +56,54 @@ public void Metadata_HasCorrectProperties() Assert.False(metadata.ReadOnly); Assert.False(metadata.Secret); } + + [Theory] + [InlineData("--workspace-id ws1 --diagnostics-config {\"status\":\"Disabled\"}", true)] + [InlineData("--workspace ws1 --diagnostics-config {\"status\":\"Disabled\"}", true)] + [InlineData("--diagnostics-config {\"status\":\"Disabled\"}", false)] + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + Service.ModifyDiagnosticsAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + } + + var response = await ExecuteCommandAsync(args); + + Assert.NotNull(response); + if (shouldSucceed) + Assert.Equal(HttpStatusCode.OK, response.Status); + else + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + + [Fact] + public async Task ExecuteAsync_SuccessfulModification_ReturnsSuccessMessage() + { + Service.ModifyDiagnosticsAsync("ws1", Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + + var response = await ExecuteCommandAsync( + "--workspace-id", "ws1", + "--diagnostics-config", "{\"status\":\"Disabled\"}"); + + var result = ValidateAndDeserializeResponse(response, OneLakeJsonContext.Default.DiagnosticsModifyCommandResult); + Assert.Contains("successfully", result.Message, StringComparison.OrdinalIgnoreCase); + await Service.Received(1).ModifyDiagnosticsAsync("ws1", Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + Service.ModifyDiagnosticsAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new HttpRequestException("Forbidden")); + + var response = await ExecuteCommandAsync( + "--workspace-id", "ws1", + "--diagnostics-config", "{\"status\":\"Disabled\"}"); + + Assert.NotNull(response); + Assert.NotEqual(HttpStatusCode.OK, response.Status); + } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs index 6f900044a0..5791e2f018 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs @@ -1,9 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Net; using Fabric.Mcp.Tools.OneLake.Commands.Settings; +using Fabric.Mcp.Tools.OneLake.Models; using Fabric.Mcp.Tools.OneLake.Services; using Microsoft.Mcp.Tests.Client; +using NSubstitute; +using NSubstitute.ExceptionExtensions; namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Settings; @@ -52,4 +56,54 @@ public void Metadata_HasCorrectProperties() Assert.False(metadata.ReadOnly); Assert.False(metadata.Secret); } + + [Theory] + [InlineData("--workspace-id ws1 --immutability-policy {\"scope\":\"DiagnosticLogs\",\"retentionDays\":7}", true)] + [InlineData("--workspace ws1 --immutability-policy {\"scope\":\"DiagnosticLogs\",\"retentionDays\":7}", true)] + [InlineData("--immutability-policy {\"scope\":\"DiagnosticLogs\",\"retentionDays\":7}", false)] + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + Service.ModifyImmutabilityPolicyAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + } + + var response = await ExecuteCommandAsync(args); + + Assert.NotNull(response); + if (shouldSucceed) + Assert.Equal(HttpStatusCode.OK, response.Status); + else + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + + [Fact] + public async Task ExecuteAsync_SuccessfulModification_ReturnsSuccessMessage() + { + Service.ModifyImmutabilityPolicyAsync("ws1", Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + + var response = await ExecuteCommandAsync( + "--workspace-id", "ws1", + "--immutability-policy", "{\"scope\":\"DiagnosticLogs\",\"retentionDays\":7}"); + + var result = ValidateAndDeserializeResponse(response, OneLakeJsonContext.Default.ImmutabilityPolicyModifyCommandResult); + Assert.Contains("successfully", result.Message, StringComparison.OrdinalIgnoreCase); + await Service.Received(1).ModifyImmutabilityPolicyAsync("ws1", Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + Service.ModifyImmutabilityPolicyAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new HttpRequestException("Forbidden")); + + var response = await ExecuteCommandAsync( + "--workspace-id", "ws1", + "--immutability-policy", "{\"scope\":\"DiagnosticLogs\",\"retentionDays\":7}"); + + Assert.NotNull(response); + Assert.NotEqual(HttpStatusCode.OK, response.Status); + } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateOrUpdateCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateOrUpdateCommandTests.cs index dbd6ae2329..ee4ec00711 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateOrUpdateCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateOrUpdateCommandTests.cs @@ -1,20 +1,26 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Net; using Fabric.Mcp.Tools.OneLake.Commands.Shortcut; +using Fabric.Mcp.Tools.OneLake.Models; using Fabric.Mcp.Tools.OneLake.Services; using Microsoft.Mcp.Tests.Client; +using NSubstitute; +using NSubstitute.ExceptionExtensions; namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Shortcut; public class ShortcutCreateOrUpdateCommandTests : CommandUnitTestsBase { + private const string ValidJson = "{\"createShortcutRequests\":[{\"path\":\"Files/folder\",\"name\":\"sc1\",\"target\":{\"oneLake\":{\"workspaceId\":\"ws2\",\"itemId\":\"item2\",\"path\":\"Tables\"}}}]}"; + [Fact] public void Constructor_InitializesCommandCorrectly() { Assert.Equal("create_or_update_shortcuts", Command.Name); Assert.Equal("Create or Update OneLake Shortcuts", Command.Title); - Assert.Contains("Create one or more shortcuts in a single call", Command.Description); + Assert.Contains("bulk create", Command.Description, StringComparison.OrdinalIgnoreCase); Assert.False(Command.Metadata.ReadOnly); Assert.False(Command.Metadata.Destructive); Assert.False(Command.Metadata.Idempotent); @@ -52,4 +58,84 @@ public void Metadata_HasCorrectProperties() Assert.False(metadata.ReadOnly); Assert.False(metadata.Secret); } + + [Theory] + [InlineData("--workspace-id ws1 --item-id item1", true)] + [InlineData("--workspace ws1 --item item1", true)] + [InlineData("--item-id item1", false)] // missing workspace + [InlineData("--workspace-id ws1", false)] // missing item + [InlineData("", false)] + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + Service.CreateOrUpdateShortcutsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(new BulkCreateShortcutResponse()); + } + + var fullArgs = string.IsNullOrWhiteSpace(args) + ? $"--shortcuts {ValidJson}" + : $"{args} --shortcuts {ValidJson}"; + + var response = await ExecuteCommandAsync(fullArgs); + + Assert.NotNull(response); + if (shouldSucceed) + Assert.Equal(HttpStatusCode.OK, response.Status); + else + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + + [Fact] + public async Task ExecuteAsync_SingleShortcut_CallsBulkCreateOnce() + { + var expected = new BulkCreateShortcutResponse + { + Value = [new CreateShortcutResponse { Status = "Succeeded" }] + }; + + Service.CreateOrUpdateShortcutsAsync("ws1", "item1", Arg.Any(), null, Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync( + "--workspace-id", "ws1", + "--item-id", "item1", + "--shortcuts", ValidJson); + + var result = ValidateAndDeserializeResponse(response, OneLakeJsonContext.Default.BulkCreateShortcutResponse); + Assert.Single(result.Value!); + Assert.Equal("Succeeded", result.Value![0].Status); + await Service.Received(1).CreateOrUpdateShortcutsAsync("ws1", "item1", Arg.Any(), null, Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_WithConflictPolicy_PassesPolicyToService() + { + Service.CreateOrUpdateShortcutsAsync("ws1", "item1", Arg.Any(), "CreateOrOverwrite", Arg.Any()) + .Returns(new BulkCreateShortcutResponse { Value = [] }); + + var response = await ExecuteCommandAsync( + "--workspace-id", "ws1", + "--item-id", "item1", + "--shortcuts", ValidJson, + "--shortcut-conflict-policy", "CreateOrOverwrite"); + + Assert.Equal(HttpStatusCode.OK, response.Status); + await Service.Received(1).CreateOrUpdateShortcutsAsync("ws1", "item1", Arg.Any(), "CreateOrOverwrite", Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + Service.CreateOrUpdateShortcutsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new HttpRequestException("Conflict")); + + var response = await ExecuteCommandAsync( + "--workspace-id", "ws1", + "--item-id", "item1", + "--shortcuts", ValidJson); + + Assert.NotNull(response); + Assert.NotEqual(HttpStatusCode.OK, response.Status); + } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutResetCacheCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutResetCacheCommandTests.cs index 093c137686..60325cf338 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutResetCacheCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutResetCacheCommandTests.cs @@ -1,9 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Net; using Fabric.Mcp.Tools.OneLake.Commands.Shortcut; +using Fabric.Mcp.Tools.OneLake.Models; using Fabric.Mcp.Tools.OneLake.Services; using Microsoft.Mcp.Tests.Client; +using NSubstitute; +using NSubstitute.ExceptionExtensions; namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Shortcut; @@ -52,4 +56,62 @@ public void Metadata_HasCorrectProperties() Assert.False(metadata.ReadOnly); Assert.False(metadata.Secret); } + + [Theory] + [InlineData("--workspace-id ws1", true)] + [InlineData("--workspace ws1", true)] + [InlineData("", false)] + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + Service.ResetShortcutCacheAsync(Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + } + + var response = await ExecuteCommandAsync(args); + + Assert.NotNull(response); + if (shouldSucceed) + Assert.Equal(HttpStatusCode.OK, response.Status); + else + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + + [Fact] + public async Task ExecuteAsync_NoItemRequired_SucceedsWithWorkspaceOnly() + { + Service.ResetShortcutCacheAsync("ws1", Arg.Any()) + .Returns(Task.CompletedTask); + + var response = await ExecuteCommandAsync("--workspace-id", "ws1"); + + var result = ValidateAndDeserializeResponse(response, OneLakeJsonContext.Default.ShortcutResetCacheCommandResult); + Assert.Contains("successfully", result.Message, StringComparison.OrdinalIgnoreCase); + await Service.Received(1).ResetShortcutCacheAsync("ws1", Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + Service.ResetShortcutCacheAsync(Arg.Any(), Arg.Any()) + .ThrowsAsync(new HttpRequestException("Service unavailable")); + + var response = await ExecuteCommandAsync("--workspace-id", "ws1"); + + Assert.NotNull(response); + Assert.NotEqual(HttpStatusCode.OK, response.Status); + } + + [Fact] + public void BindOptions_UsesWorkspaceName_WhenWorkspaceIdNotProvided() + { + Service.ResetShortcutCacheAsync(Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + + // Validator should pass with --workspace (friendly name) + var parseResult = CommandDefinition.Parse("--workspace myWorkspace"); + var isValid = Command.Validate(parseResult.CommandResult); + Assert.True(isValid.IsValid); + } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Services/OneLakeServiceLroTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Services/OneLakeServiceLroTests.cs new file mode 100644 index 0000000000..a312044f6b --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Services/OneLakeServiceLroTests.cs @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text; +using System.Text.Json; +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Services; +using Fabric.Mcp.Tools.OneLake.Tests.TestSupport; + +namespace Fabric.Mcp.Tools.OneLake.Tests.Services; + +/// +/// Tests that OneLakeService correctly handles Fabric long running operations (LRO) +/// that return 202 Accepted with a Location header. +/// +public class OneLakeServiceLroTests +{ + private const string WorkspaceId = "ws-lro-test"; + private const string ItemId = "item-lro-test"; + private const string OperationId = "op-lro-12345"; + private const string OperationUrl = $"https://dailyapi.fabric.microsoft.com/v1/operations/{OperationId}"; + private const string ResultUrl = $"https://dailyapi.fabric.microsoft.com/v1/operations/{OperationId}/result"; + private const string ShortcutsUrl = $"https://dailyapi.fabric.microsoft.com/v1/workspaces/{WorkspaceId}/items/{ItemId}/shortcuts/bulkCreate"; + + private static OneLakeService CreateService(Func handler) + { + var httpClient = new HttpClient(new CapturingHttpMessageHandler(handler)); + return new OneLakeService(httpClient, new FakeTokenCredential()); + } + + private static string RunningStateJson => JsonSerializer.Serialize( + new { status = "Running", percentComplete = 50 }); + + private static string SucceededStateJson => JsonSerializer.Serialize( + new { status = "Succeeded", percentComplete = 100 }); + + private static string FailedStateJson => JsonSerializer.Serialize( + new { status = "Failed", percentComplete = 0, error = new { errorCode = "InternalError", message = "Something went wrong" } }); + + private static string BulkCreateResultJson => JsonSerializer.Serialize( + new + { + value = new[] + { + new { status = "Succeeded", result = new { path = "/Files", name = "lro-test-shortcut" } } + } + }); + + /// + /// Single-poll success: 202 → poll returns Succeeded → result fetched. + /// + [Fact] + public async Task CreateOrUpdateShortcutsAsync_202WithLocation_PollsAndReturnsResult() + { + var callCount = 0; + var service = CreateService(request => + { + callCount++; + var url = request.RequestUri!.ToString(); + + // First call: the bulk create → 202 with Location + if (url.Contains("bulkCreate") && callCount == 1) + { + var accepted = new HttpResponseMessage(HttpStatusCode.Accepted); + accepted.Headers.Location = new Uri(OperationUrl); + return accepted; + } + + // Second call: polling → Succeeded with Location pointing to result + if (url == OperationUrl) + { + var poll = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(SucceededStateJson, Encoding.UTF8, "application/json") + }; + poll.Headers.Location = new Uri(ResultUrl); + return poll; + } + + // Third call: result fetch + if (url == ResultUrl) + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(BulkCreateResultJson, Encoding.UTF8, "application/json") + }; + } + + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); + + var result = await service.CreateOrUpdateShortcutsAsync( + WorkspaceId, ItemId, + "{\"createShortcutRequests\":[{\"path\":\"/Files\",\"name\":\"lro-test-shortcut\",\"target\":{\"oneLake\":{\"workspaceId\":\"ws2\",\"itemId\":\"item2\",\"path\":\"Files\"}}}]}", + cancellationToken: CancellationToken.None); + + Assert.NotNull(result); + Assert.NotNull(result.Value); + Assert.Single(result.Value); + Assert.Equal("Succeeded", result.Value[0].Status); + Assert.Equal(3, callCount); // bulk create + poll + result + } + + /// + /// Multi-poll: 202 → Running → Succeeded → result fetched. + /// + [Fact] + public async Task CreateOrUpdateShortcutsAsync_202_MultiplePolls_EventuallySucceeds() + { + var pollCount = 0; + var service = CreateService(request => + { + var url = request.RequestUri!.ToString(); + + if (url.Contains("bulkCreate")) + { + var accepted = new HttpResponseMessage(HttpStatusCode.Accepted); + accepted.Headers.Location = new Uri(OperationUrl); + return accepted; + } + + if (url == OperationUrl) + { + pollCount++; + if (pollCount < 3) + { + // First two polls: still running + var running = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(RunningStateJson, Encoding.UTF8, "application/json") + }; + running.Headers.Location = new Uri(OperationUrl); + return running; + } + // Third poll: succeeded + var succeeded = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(SucceededStateJson, Encoding.UTF8, "application/json") + }; + succeeded.Headers.Location = new Uri(ResultUrl); + return succeeded; + } + + if (url == ResultUrl) + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(BulkCreateResultJson, Encoding.UTF8, "application/json") + }; + } + + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); + + var result = await service.CreateOrUpdateShortcutsAsync( + WorkspaceId, ItemId, + "{\"createShortcutRequests\":[{\"path\":\"/Files\",\"name\":\"lro-test-shortcut\",\"target\":{\"oneLake\":{\"workspaceId\":\"ws2\",\"itemId\":\"item2\",\"path\":\"Files\"}}}]}", + cancellationToken: CancellationToken.None); + + Assert.NotNull(result); + Assert.NotNull(result.Value); + Assert.Equal(3, pollCount); + } + + /// + /// 202 → poll returns Succeeded but no Location header → returns empty response (no crash). + /// + [Fact] + public async Task CreateOrUpdateShortcutsAsync_202_SucceededWithNoResultLocation_ReturnsEmpty() + { + var service = CreateService(request => + { + var url = request.RequestUri!.ToString(); + + if (url.Contains("bulkCreate")) + { + var accepted = new HttpResponseMessage(HttpStatusCode.Accepted); + accepted.Headers.Location = new Uri(OperationUrl); + return accepted; + } + + if (url == OperationUrl) + { + // Succeeded but no Location header for result + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(SucceededStateJson, Encoding.UTF8, "application/json") + }; + } + + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); + + var result = await service.CreateOrUpdateShortcutsAsync( + WorkspaceId, ItemId, + "{\"createShortcutRequests\":[{\"path\":\"/Files\",\"name\":\"lro-test-shortcut\",\"target\":{\"oneLake\":{\"workspaceId\":\"ws2\",\"itemId\":\"item2\",\"path\":\"Files\"}}}]}", + cancellationToken: CancellationToken.None); + + Assert.NotNull(result); + Assert.Null(result.Value); // empty response from Stream.Null + } + + /// + /// 202 → poll returns Failed → HttpRequestException thrown. + /// + [Fact] + public async Task CreateOrUpdateShortcutsAsync_202_OperationFails_ThrowsHttpRequestException() + { + var service = CreateService(request => + { + var url = request.RequestUri!.ToString(); + + if (url.Contains("bulkCreate")) + { + var accepted = new HttpResponseMessage(HttpStatusCode.Accepted); + accepted.Headers.Location = new Uri(OperationUrl); + return accepted; + } + + if (url == OperationUrl) + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(FailedStateJson, Encoding.UTF8, "application/json") + }; + } + + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); + + var ex = await Assert.ThrowsAsync(() => + service.CreateOrUpdateShortcutsAsync( + WorkspaceId, ItemId, + "{\"createShortcutRequests\":[{\"path\":\"/Files\",\"name\":\"lro-test-shortcut\",\"target\":{\"oneLake\":{\"workspaceId\":\"ws2\",\"itemId\":\"item2\",\"path\":\"Files\"}}}]}", + cancellationToken: CancellationToken.None)); + + Assert.Contains("InternalError", ex.Message); + Assert.Contains("Something went wrong", ex.Message); + } + + /// + /// 202 with no Location header → returns empty response gracefully. + /// + [Fact] + public async Task CreateOrUpdateShortcutsAsync_202_NoLocationHeader_ReturnsEmpty() + { + var service = CreateService(request => + { + if (request.RequestUri!.ToString().Contains("bulkCreate")) + { + // 202 but without Location header + return new HttpResponseMessage(HttpStatusCode.Accepted); + } + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); + + var result = await service.CreateOrUpdateShortcutsAsync( + WorkspaceId, ItemId, + "{\"createShortcutRequests\":[{\"path\":\"/Files\",\"name\":\"lro-test-shortcut\",\"target\":{\"oneLake\":{\"workspaceId\":\"ws2\",\"itemId\":\"item2\",\"path\":\"Files\"}}}]}", + cancellationToken: CancellationToken.None); + + Assert.NotNull(result); + Assert.Null(result.Value); + } + + /// + /// Synchronous 200 response (non-LRO path) still works correctly. + /// + [Fact] + public async Task CreateOrUpdateShortcutsAsync_200_ReturnsResultDirectly() + { + var resultJson = JsonSerializer.Serialize(new + { + value = new[] + { + new { status = "Succeeded", result = new { path = "/Files", name = "sync-shortcut" } } + } + }); + + var service = CreateService(_ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(resultJson, Encoding.UTF8, "application/json") + }); + + var result = await service.CreateOrUpdateShortcutsAsync( + WorkspaceId, ItemId, + "{\"createShortcutRequests\":[{\"path\":\"/Files\",\"name\":\"sync-shortcut\",\"target\":{\"oneLake\":{\"workspaceId\":\"ws2\",\"itemId\":\"item2\",\"path\":\"Files\"}}}]}", + cancellationToken: CancellationToken.None); + + Assert.NotNull(result); + Assert.NotNull(result.Value); + Assert.Single(result.Value); + Assert.Equal("Succeeded", result.Value[0].Status); + } +} From e3442328f92e0c2883e46c7575b9c82cc814c63a Mon Sep 17 00:00:00 2001 From: Srinivas Thatipamula Date: Wed, 13 May 2026 10:32:11 -0700 Subject: [PATCH 05/15] Address PR review feedback: GUID-only refactor, pagination, record simplifications, description fixes - Refactor 12 OneLake security/shortcut/settings commands to GUID-only (workspace-id, item-id) - Add pagination support (continuationToken) for list_data_access_roles and list_shortcuts - Simplify result classes to sealed records - Use new(...) shorthand for ResponseResult.Create calls - Remove cross-tool name references from descriptions - Remove CLI formatting from shortcut conflict policy description Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/Fabric.Mcp.Tools.OneLake/README.md | 101 +++++++++--------- .../DataAccessRoleCreateOrUpdateCommand.cs | 36 +------ .../Security/DataAccessRoleDeleteCommand.cs | 54 ++-------- .../Security/DataAccessRoleGetCommand.cs | 36 +------ .../Security/DataAccessRoleListCommand.cs | 39 ++----- .../Settings/DiagnosticsModifyCommand.cs | 31 ++---- .../ImmutabilityPolicyModifyCommand.cs | 31 ++---- .../Commands/Settings/SettingsGetCommand.cs | 25 +---- .../Shortcut/ShortcutCreateOrUpdateCommand.cs | 38 ++----- .../Shortcut/ShortcutDeleteCommand.cs | 54 ++-------- .../Commands/Shortcut/ShortcutGetCommand.cs | 36 +------ .../Commands/Shortcut/ShortcutListCommand.cs | 39 ++----- .../Shortcut/ShortcutResetCacheCommand.cs | 38 ++----- .../DataAccessRoleCreateOrUpdateOptions.cs | 3 +- .../Options/DataAccessRoleDeleteOptions.cs | 3 +- .../src/Options/DataAccessRoleGetOptions.cs | 3 +- .../src/Options/DataAccessRoleListOptions.cs | 4 +- .../src/Options/DiagnosticsModifyOptions.cs | 2 +- .../ImmutabilityPolicyModifyOptions.cs | 2 +- .../src/Options/SettingsGetOptions.cs | 2 +- .../Options/ShortcutCreateOrUpdateOptions.cs | 3 +- .../src/Options/ShortcutDeleteOptions.cs | 3 +- .../src/Options/ShortcutGetOptions.cs | 3 +- .../src/Options/ShortcutListOptions.cs | 4 +- .../src/Options/ShortcutResetCacheOptions.cs | 2 +- .../src/Services/IOneLakeService.cs | 4 +- .../src/Services/OneLakeService.cs | 23 +++- ...ataAccessRoleCreateOrUpdateCommandTests.cs | 2 +- .../DataAccessRoleListCommandTests.cs | 7 ++ .../Settings/DiagnosticsModifyCommandTests.cs | 2 +- .../ImmutabilityPolicyModifyCommandTests.cs | 2 +- .../ShortcutCreateOrUpdateCommandTests.cs | 2 +- .../Shortcut/ShortcutListCommandTests.cs | 7 ++ .../ShortcutResetCacheCommandTests.cs | 12 +-- 34 files changed, 179 insertions(+), 474 deletions(-) diff --git a/tools/Fabric.Mcp.Tools.OneLake/README.md b/tools/Fabric.Mcp.Tools.OneLake/README.md index 67f4629427..9395b8b4d6 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/README.md +++ b/tools/Fabric.Mcp.Tools.OneLake/README.md @@ -98,10 +98,10 @@ You can verify which environment you're targeting by checking the endpoints in t ### Workspace and Item Identifiers -All commands accept either GUID identifiers or friendly names via the `--workspace` and `--item` options. The existing `--workspace-id` and `--item-id` switches remain available for scripts that already depend on them. Friendly-name inputs are sent directly to the OneLake APIs without local GUID resolution; when using names, specify the item as `.` (for example, `SalesLakehouse.lakehouse`). Table-based commands additionally accept schema identifiers through `--namespace` or its alias `--schema`. +The Fabric Core REST APIs used by the Security, Shortcuts, and Settings commands require GUID identifiers. Use `--workspace-id` for workspace scope and `--item-id` for item scope. Table-based commands additionally accept schema identifiers through `--namespace` or its alias `--schema`. ```bash -dotnet run -- onelake file list --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" --path "Files" +dotnet run -- onelake shortcut list --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" ``` ## Available Commands @@ -632,27 +632,28 @@ These tools manage role-based data access policies on OneLake items (Lakehouse / #### List Data Access Roles -Lists all data access roles defined on a single item. +Lists all data access roles defined on a single item. Supports pagination via `--continuation-token`. ```bash -dotnet run -- onelake security list --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" +dotnet run -- onelake security list --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" ``` **Parameters:** -- `--workspace`/`--workspace-id`: Workspace identifier -- `--item`/`--item-id`: Item identifier +- `--workspace-id`: Workspace ID (GUID) +- `--item-id`: Item ID (GUID) +- `--continuation-token`: (Optional) Token for retrieving the next page of results #### Get Data Access Role Gets the full definition of a single data access role — members, permissions, decision rules. ```bash -dotnet run -- onelake security get --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" --role-name "DataAnalysts" +dotnet run -- onelake security get --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --role-name "DataAnalysts" ``` **Parameters:** -- `--workspace`/`--workspace-id`: Workspace identifier -- `--item`/`--item-id`: Item identifier +- `--workspace-id`: Workspace ID (GUID) +- `--item-id`: Item ID (GUID) - `--role-name`: Name of the data access role to retrieve #### Create or Update Data Access Role @@ -660,12 +661,12 @@ dotnet run -- onelake security get --workspace "Analytics Workspace" --item "Sal Upserts a single data access role on a single item. Scoped to one role per call — does not affect other roles. The role name is derived from the `name` field in the JSON definition. ```bash -dotnet run -- onelake security create-or-update --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" --role-definition '{"name":"DataAnalysts","members":{"fabricItemMembers":[{"itemAccessType":"ReadAll"}]},"decisionRules":[{"effect":"Permit","permission":[{"attributeName":"Path","attributeValueIncludedIn":["Tables/*"]}]}]}' +dotnet run -- onelake security create-or-update --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --role-definition '{"name":"DataAnalysts","members":{"fabricItemMembers":[{"itemAccessType":"ReadAll"}]},"decisionRules":[{"effect":"Permit","permission":[{"attributeName":"Path","attributeValueIncludedIn":["Tables/*"]}]}]}' ``` **Parameters:** -- `--workspace`/`--workspace-id`: Workspace identifier -- `--item`/`--item-id`: Item identifier +- `--workspace-id`: Workspace ID (GUID) +- `--item-id`: Item ID (GUID) - `--role-definition`: JSON definition of the role (must include `name`, `members`, and `decisionRules`) #### Delete Data Access Role @@ -673,12 +674,12 @@ dotnet run -- onelake security create-or-update --workspace "Analytics Workspace Deletes a single data access role from a single item. Destructive — principals that gained access only via this role lose it. ```bash -dotnet run -- onelake security delete --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" --role-name "TempRole" +dotnet run -- onelake security delete --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --role-name "TempRole" ``` **Parameters:** -- `--workspace`/`--workspace-id`: Workspace identifier -- `--item`/`--item-id`: Item identifier +- `--workspace-id`: Workspace ID (GUID) +- `--item-id`: Item ID (GUID) - `--role-name`: Name of the data access role to delete ### Shortcut Operations @@ -687,28 +688,29 @@ Shortcuts are references to data stored in external or internal locations (ADLS #### List Shortcuts -Lists shortcuts defined within an item, recursing through subfolders. +Lists shortcuts defined within an item, recursing through subfolders. Supports pagination via `--continuation-token`. ```bash -dotnet run -- onelake shortcut list --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" +dotnet run -- onelake shortcut list --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" ``` **Parameters:** -- `--workspace`/`--workspace-id`: Workspace identifier -- `--item`/`--item-id`: Item identifier +- `--workspace-id`: Workspace ID (GUID) +- `--item-id`: Item ID (GUID) - `--parent-path`: (Optional) Parent path to scope the listing +- `--continuation-token`: (Optional) Token for retrieving the next page of results #### Get Shortcut Gets the properties of a single shortcut (name, path, target, configuration). ```bash -dotnet run -- onelake shortcut get --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" --shortcut-name "ExternalData" --shortcut-path "Tables/ExternalData" +dotnet run -- onelake shortcut get --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --shortcut-name "ExternalData" --shortcut-path "Tables/ExternalData" ``` **Parameters:** -- `--workspace`/`--workspace-id`: Workspace identifier -- `--item`/`--item-id`: Item identifier +- `--workspace-id`: Workspace ID (GUID) +- `--item-id`: Item ID (GUID) - `--shortcut-name`: Name of the shortcut - `--shortcut-path`: Path of the shortcut within the item @@ -717,12 +719,12 @@ dotnet run -- onelake shortcut get --workspace "Analytics Workspace" --item "Sal Creates one or more shortcuts in a single call. Pass `--create-or-overwrite` to upsert (default fails on conflict). ```bash -dotnet run -- onelake shortcut create-or-update --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" --shortcuts '[{"name":"ExternalData","path":"Tables/ExternalData","target":{"adlsGen2":{"location":"https://storageaccount.dfs.core.windows.net","subpath":"/container/path"}}}]' +dotnet run -- onelake shortcut create-or-update --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --shortcuts '[{"name":"ExternalData","path":"Tables/ExternalData","target":{"adlsGen2":{"location":"https://storageaccount.dfs.core.windows.net","subpath":"/container/path"}}}]' ``` **Parameters:** -- `--workspace`/`--workspace-id`: Workspace identifier -- `--item`/`--item-id`: Item identifier +- `--workspace-id`: Workspace ID (GUID) +- `--item-id`: Item ID (GUID) - `--shortcuts`: JSON array of shortcut definitions - `--create-or-overwrite`: (Optional) If set, overwrites existing shortcuts @@ -731,26 +733,25 @@ dotnet run -- onelake shortcut create-or-update --workspace "Analytics Workspace Deletes a single shortcut from an item. The destination data is preserved — only the shortcut reference is removed. ```bash -dotnet run -- onelake shortcut delete --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" --shortcut-name "ExternalData" --shortcut-path "Tables/ExternalData" +dotnet run -- onelake shortcut delete --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --shortcut-name "ExternalData" --shortcut-path "Tables/ExternalData" ``` **Parameters:** -- `--workspace`/`--workspace-id`: Workspace identifier -- `--item`/`--item-id`: Item identifier +- `--workspace-id`: Workspace ID (GUID) +- `--item-id`: Item ID (GUID) - `--shortcut-name`: Name of the shortcut to delete - `--shortcut-path`: Path of the shortcut #### Reset Shortcut Cache -Drops cached shortcut reads for an item, forcing the next read to re-resolve from the destination. Use sparingly — primarily for debugging stale-cache issues. +Drops cached shortcut reads for a workspace, forcing the next read to re-resolve from the destination. Use sparingly — primarily for debugging stale-cache issues. ```bash -dotnet run -- onelake shortcut reset-cache --workspace "Analytics Workspace" --item "SalesLakehouse.lakehouse" +dotnet run -- onelake shortcut reset-cache --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" ``` **Parameters:** -- `--workspace`/`--workspace-id`: Workspace identifier -- `--item`/`--item-id`: Item identifier +- `--workspace-id`: Workspace ID (GUID) ### Settings Operations @@ -761,22 +762,22 @@ Workspace-level OneLake settings for diagnostics and immutability policies. Thes Gets the OneLake settings for a workspace — diagnostics configuration and immutability policy. ```bash -dotnet run -- onelake settings get --workspace "Analytics Workspace" +dotnet run -- onelake settings get --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" ``` **Parameters:** -- `--workspace`/`--workspace-id`: Workspace identifier +- `--workspace-id`: Workspace ID (GUID) #### Modify Diagnostics Modifies the diagnostic logging configuration for OneLake at the workspace scope. Replaces the existing diagnostics block; fetch with `get settings` first if you want to merge. ```bash -dotnet run -- onelake settings modify-diagnostics --workspace "Analytics Workspace" --diagnostics-config '{"logAnalyticsWorkspaceId":"","level":"Verbose"}' +dotnet run -- onelake settings modify-diagnostics --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --diagnostics-config '{"logAnalyticsWorkspaceId":"","level":"Verbose"}' ``` **Parameters:** -- `--workspace`/`--workspace-id`: Workspace identifier +- `--workspace-id`: Workspace ID (GUID) - `--diagnostics-config`: JSON configuration for diagnostic settings #### Modify Immutability Policy @@ -784,11 +785,11 @@ dotnet run -- onelake settings modify-diagnostics --workspace "Analytics Workspa Modifies the workspace-level OneLake immutability policy. **Warning:** Once enabled, immutability cannot be disabled — confirm with the user before applying. ```bash -dotnet run -- onelake settings modify-immutability --workspace "Analytics Workspace" --immutability-policy '{"state":"Enabled"}' +dotnet run -- onelake settings modify-immutability --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --immutability-policy '{"state":"Enabled"}' ``` **Parameters:** -- `--workspace`/`--workspace-id`: Workspace identifier +- `--workspace-id`: Workspace ID (GUID) - `--immutability-policy`: JSON immutability policy configuration ## Quick Reference - fabmcp.exe Commands @@ -864,43 +865,43 @@ fabmcp.exe onelake table get --workspace "Analytics Workspace" --item "SalesLake ### Security Operations ```cmd # List data access roles on an item -fabmcp.exe onelake security list --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --item "0e67ed13-2bb6-49be-9c87-a1105a4ea342" +fabmcp.exe onelake security list --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" # Get a specific role definition -fabmcp.exe onelake security get --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --item "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --role-name "DataAnalysts" +fabmcp.exe onelake security get --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --role-name "DataAnalysts" # Delete a data access role -fabmcp.exe onelake security delete --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --item "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --role-name "TempRole" +fabmcp.exe onelake security delete --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --role-name "TempRole" ``` ### Shortcut Operations ```cmd # List shortcuts on an item -fabmcp.exe onelake shortcut list --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --item "0e67ed13-2bb6-49be-9c87-a1105a4ea342" +fabmcp.exe onelake shortcut list --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" # Get a specific shortcut -fabmcp.exe onelake shortcut get --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --item "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --shortcut-name "ExternalData" --shortcut-path "Tables/ExternalData" +fabmcp.exe onelake shortcut get --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --shortcut-name "ExternalData" --shortcut-path "Tables/ExternalData" # Delete a shortcut -fabmcp.exe onelake shortcut delete --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --item "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --shortcut-name "ExternalData" --shortcut-path "Tables/ExternalData" +fabmcp.exe onelake shortcut delete --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --shortcut-name "ExternalData" --shortcut-path "Tables/ExternalData" # Reset shortcut cache -fabmcp.exe onelake shortcut reset-cache --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --item "0e67ed13-2bb6-49be-9c87-a1105a4ea342" +fabmcp.exe onelake shortcut reset-cache --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" ``` ### Settings Operations ```cmd # Get workspace OneLake settings -fabmcp.exe onelake settings get --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" +fabmcp.exe onelake settings get --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" # Modify diagnostics configuration -fabmcp.exe onelake settings modify-diagnostics --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --diagnostics-config '{"logAnalyticsWorkspaceId":"","level":"Verbose"}' +fabmcp.exe onelake settings modify-diagnostics --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --diagnostics-config '{"logAnalyticsWorkspaceId":"","level":"Verbose"}' # Modify immutability policy -fabmcp.exe onelake settings modify-immutability --workspace "47242da5-ff3b-46fb-a94f-977909b773d5" --immutability-policy '{"state":"Enabled"}' +fabmcp.exe onelake settings modify-immutability --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --immutability-policy '{"state":"Enabled"}' ``` -**Note:** Replace the workspace identifier (`47242da5-ff3b-46fb-a94f-977909b773d5`) and item identifier (`0e67ed13-2bb6-49be-9c87-a1105a4ea342`) with your actual Fabric workspace and item values (names or IDs). +**Note:** Replace the workspace ID (`47242da5-ff3b-46fb-a94f-977909b773d5`) and item ID (`0e67ed13-2bb6-49be-9c87-a1105a4ea342`) with your actual Fabric GUIDs. ## Common Usage Patterns @@ -1064,4 +1065,4 @@ The test suite includes examples of testable service architecture patterns follo - Follow the established command patterns (see existing commands as examples) - Use proper error handling with meaningful HTTP status codes - Include comprehensive parameter validation -- Write tests that verify both success and error scenarios \ No newline at end of file +- Write tests that verify both success and error scenarios diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs index dba432d0c8..e41dc0a3bb 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs @@ -40,37 +40,16 @@ public sealed class DataAccessRoleCreateOrUpdateCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); - command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); command.Options.Add(FabricOptionDefinitions.RoleDefinition.AsRequired()); - command.Validators.Add(result => - { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); - var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - var item = result.GetValueOrDefault(FabricOptionDefinitions.Item.Name); - - if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) - { - result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); - } - - if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) - { - result.AddError("Item identifier is required. Provide --item or --item-id."); - } - }); } protected override DataAccessRoleCreateOrUpdateOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.Item.Name); options.RoleDefinition = parseResult.GetValueOrDefault(FabricOptionDefinitions.RoleDefinition.Name); return options; } @@ -85,24 +64,19 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); try { - var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) - ? options.WorkspaceId - : options.Workspace; - var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) - ? options.ItemId - : options.Item; - var result = await _oneLakeService.CreateOrUpdateDataAccessRoleAsync(workspaceIdentifier!, itemIdentifier!, options.RoleDefinition!, cancellationToken); + var result = await _oneLakeService.CreateOrUpdateDataAccessRoleAsync(options.WorkspaceId!, options.ItemId!, options.RoleDefinition!, cancellationToken); context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.DataAccessRole); } catch (Exception ex) { _logger.LogError(ex, "Error creating/updating data access role. Workspace: {Workspace}, Item: {Item}.", - options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item); + options.WorkspaceId, options.ItemId); HandleException(context, ex); } return context.Response; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleDeleteCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleDeleteCommand.cs index 8a8e18fbaa..44d6fa1d43 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleDeleteCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleDeleteCommand.cs @@ -38,37 +38,16 @@ public sealed class DataAccessRoleDeleteCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); - command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); command.Options.Add(FabricOptionDefinitions.RoleName.AsRequired()); - command.Validators.Add(result => - { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); - var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - var item = result.GetValueOrDefault(FabricOptionDefinitions.Item.Name); - - if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) - { - result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); - } - - if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) - { - result.AddError("Item identifier is required. Provide --item or --item-id."); - } - }); } protected override DataAccessRoleDeleteOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.Item.Name); options.RoleName = parseResult.GetValueOrDefault(FabricOptionDefinitions.RoleName.Name); return options; } @@ -83,41 +62,20 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); try { - var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) - ? options.WorkspaceId - : options.Workspace; - - var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) - ? options.ItemId - : options.Item; - - await _oneLakeService.DeleteDataAccessRoleAsync(workspaceIdentifier!, itemIdentifier!, options.RoleName!, cancellationToken); + await _oneLakeService.DeleteDataAccessRoleAsync(options.WorkspaceId!, options.ItemId!, options.RoleName!, cancellationToken); var result = new DataAccessRoleDeleteCommandResult(options.RoleName!, "Data access role deleted successfully."); context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.DataAccessRoleDeleteCommandResult); } catch (Exception ex) { _logger.LogError(ex, "Error deleting data access role. Workspace: {Workspace}, Item: {Item}, Role: {Role}.", - options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item, options.RoleName); + options.WorkspaceId, options.ItemId, options.RoleName); HandleException(context, ex); } return context.Response; } - public sealed class DataAccessRoleDeleteCommandResult - { - public string RoleName { get; init; } = string.Empty; - public string Message { get; init; } = string.Empty; - - public DataAccessRoleDeleteCommandResult() - { - } - - public DataAccessRoleDeleteCommandResult(string roleName, string message) - { - RoleName = roleName; - Message = message; - } - } + public sealed record DataAccessRoleDeleteCommandResult(string RoleName, string Message); } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs index d5888a85fb..ee395994bc 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs @@ -40,37 +40,16 @@ public sealed class DataAccessRoleGetCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); - command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); command.Options.Add(FabricOptionDefinitions.RoleName.AsRequired()); - command.Validators.Add(result => - { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); - var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - var item = result.GetValueOrDefault(FabricOptionDefinitions.Item.Name); - - if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) - { - result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); - } - - if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) - { - result.AddError("Item identifier is required. Provide --item or --item-id."); - } - }); } protected override DataAccessRoleGetOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.Item.Name); options.RoleName = parseResult.GetValueOrDefault(FabricOptionDefinitions.RoleName.Name); return options; } @@ -85,24 +64,19 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); try { - var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) - ? options.WorkspaceId - : options.Workspace; - var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) - ? options.ItemId - : options.Item; - var result = await _oneLakeService.GetDataAccessRoleAsync(workspaceIdentifier!, itemIdentifier!, options.RoleName!, cancellationToken); + var result = await _oneLakeService.GetDataAccessRoleAsync(options.WorkspaceId!, options.ItemId!, options.RoleName!, cancellationToken); context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.DataAccessRole); } catch (Exception ex) { _logger.LogError(ex, "Error getting data access role. Workspace: {Workspace}, Item: {Item}, Role: {Role}.", - options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item, options.RoleName); + options.WorkspaceId, options.ItemId, options.RoleName); HandleException(context, ex); } return context.Response; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs index 86466a32fe..875803fccb 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs @@ -39,36 +39,17 @@ public sealed class DataAccessRoleListCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); - command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); - command.Validators.Add(result => - { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); - var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - var item = result.GetValueOrDefault(FabricOptionDefinitions.Item.Name); - - if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) - { - result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); - } - - if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) - { - result.AddError("Item identifier is required. Provide --item or --item-id."); - } - }); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ContinuationToken.AsOptional()); } protected override DataAccessRoleListOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.Item.Name); + options.ContinuationToken = parseResult.GetValueOrDefault(FabricOptionDefinitions.ContinuationTokenName); return options; } @@ -82,23 +63,17 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); try { - var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) - ? options.WorkspaceId - : options.Workspace; - var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) - ? options.ItemId - : options.Item; - - var result = await _oneLakeService.ListDataAccessRolesAsync(workspaceIdentifier!, itemIdentifier!, cancellationToken); + var result = await _oneLakeService.ListDataAccessRolesAsync(options.WorkspaceId!, options.ItemId!, options.ContinuationToken, cancellationToken); context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.DataAccessRoleListResponse); } catch (Exception ex) { - _logger.LogError(ex, "Error listing data access roles. Workspace: {Workspace}, Item: {Item}.", options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item); + _logger.LogError(ex, "Error listing data access roles. Workspace: {Workspace}, Item: {Item}.", options.WorkspaceId, options.ItemId); HandleException(context, ex); } return context.Response; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs index f65a4d5c7e..61970b1b2a 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs @@ -37,26 +37,14 @@ public sealed class DiagnosticsModifyCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); command.Options.Add(FabricOptionDefinitions.DiagnosticsConfig.AsRequired()); - command.Validators.Add(result => - { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); - - if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) - { - result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); - } - }); } protected override DiagnosticsModifyOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); options.DiagnosticsConfig = parseResult.GetValueOrDefault(FabricOptionDefinitions.DiagnosticsConfig.Name); return options; } @@ -71,26 +59,19 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); try { - var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) - ? options.WorkspaceId - : options.Workspace; - await _oneLakeService.ModifyDiagnosticsAsync(workspaceIdentifier!, options.DiagnosticsConfig!, cancellationToken); - context.Response.Results = ResponseResult.Create(new DiagnosticsModifyCommandResult("Diagnostics settings modified successfully."), OneLakeJsonContext.Default.DiagnosticsModifyCommandResult); + await _oneLakeService.ModifyDiagnosticsAsync(options.WorkspaceId!, options.DiagnosticsConfig!, cancellationToken); + context.Response.Results = ResponseResult.Create(new("Diagnostics settings modified successfully."), OneLakeJsonContext.Default.DiagnosticsModifyCommandResult); } catch (Exception ex) { - _logger.LogError(ex, "Error modifying OneLake diagnostics. Workspace: {Workspace}.", options.WorkspaceId ?? options.Workspace); + _logger.LogError(ex, "Error modifying OneLake diagnostics. Workspace: {Workspace}.", options.WorkspaceId); HandleException(context, ex); } return context.Response; } - public sealed class DiagnosticsModifyCommandResult - { - public string Message { get; init; } = string.Empty; - public DiagnosticsModifyCommandResult() { } - public DiagnosticsModifyCommandResult(string message) { Message = message; } - } + public sealed record DiagnosticsModifyCommandResult(string Message); } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs index d65ddb10a2..7726ac91e5 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs @@ -36,26 +36,14 @@ public sealed class ImmutabilityPolicyModifyCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); command.Options.Add(FabricOptionDefinitions.ImmutabilityPolicyConfig.AsRequired()); - command.Validators.Add(result => - { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); - - if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) - { - result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); - } - }); } protected override ImmutabilityPolicyModifyOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); options.ImmutabilityPolicyConfig = parseResult.GetValueOrDefault(FabricOptionDefinitions.ImmutabilityPolicyConfig.Name); return options; } @@ -70,26 +58,19 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); try { - var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) - ? options.WorkspaceId - : options.Workspace; - await _oneLakeService.ModifyImmutabilityPolicyAsync(workspaceIdentifier!, options.ImmutabilityPolicyConfig!, cancellationToken); - context.Response.Results = ResponseResult.Create(new ImmutabilityPolicyModifyCommandResult("Immutability policy modified successfully."), OneLakeJsonContext.Default.ImmutabilityPolicyModifyCommandResult); + await _oneLakeService.ModifyImmutabilityPolicyAsync(options.WorkspaceId!, options.ImmutabilityPolicyConfig!, cancellationToken); + context.Response.Results = ResponseResult.Create(new("Immutability policy modified successfully."), OneLakeJsonContext.Default.ImmutabilityPolicyModifyCommandResult); } catch (Exception ex) { - _logger.LogError(ex, "Error modifying OneLake immutability policy. Workspace: {Workspace}.", options.WorkspaceId ?? options.Workspace); + _logger.LogError(ex, "Error modifying OneLake immutability policy. Workspace: {Workspace}.", options.WorkspaceId); HandleException(context, ex); } return context.Response; } - public sealed class ImmutabilityPolicyModifyCommandResult - { - public string Message { get; init; } = string.Empty; - public ImmutabilityPolicyModifyCommandResult() { } - public ImmutabilityPolicyModifyCommandResult(string message) { Message = message; } - } + public sealed record ImmutabilityPolicyModifyCommandResult(string Message); } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs index 10e65d4c92..5ff783c928 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs @@ -17,8 +17,7 @@ namespace Fabric.Mcp.Tools.OneLake.Commands.Settings; Title = "Get OneLake Settings", Description = """ Get the OneLake settings for a workspace — diagnostics configuration and - immutability policy. Read-only; for changes use onelake_modify_diagnostics - or onelake_modify_immutability_policy. Requires OneLake.Read.All. + immutability policy. Requires OneLake.Read.All. """, Destructive = false, Idempotent = true, @@ -36,25 +35,13 @@ public sealed class SettingsGetCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); - command.Validators.Add(result => - { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); - - if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) - { - result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); - } - }); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); } protected override SettingsGetOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); return options; } @@ -68,19 +55,17 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); try { - var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) - ? options.WorkspaceId - : options.Workspace; - var result = await _oneLakeService.GetSettingsAsync(workspaceIdentifier!, cancellationToken); + var result = await _oneLakeService.GetSettingsAsync(options.WorkspaceId!, cancellationToken); context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeSettings); } catch (Exception ex) { - _logger.LogError(ex, "Error getting OneLake settings. Workspace: {Workspace}.", options.WorkspaceId ?? options.Workspace); + _logger.LogError(ex, "Error getting OneLake settings. Workspace: {Workspace}.", options.WorkspaceId); HandleException(context, ex); } return context.Response; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOrUpdateCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOrUpdateCommand.cs index 73144171f5..2a0ec5bfce 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOrUpdateCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOrUpdateCommand.cs @@ -19,7 +19,7 @@ namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; Create one or more shortcuts in a single call using the bulk create API (POST /shortcuts/bulkCreate). Pass a JSON object with a "createShortcutRequests" array — one entry for a single shortcut, many - entries for bulk. Use --shortcut-conflict-policy to control behaviour + entries for bulk. Use shortcut conflict policy to control behaviour when a shortcut with the same name and path already exists: Abort (default), CreateOrOverwrite, OverwriteOnly, or GenerateUniqueName. Requires OneLake.ReadWrite.All. @@ -40,38 +40,17 @@ public sealed class ShortcutCreateOrUpdateCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); - command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); command.Options.Add(FabricOptionDefinitions.ShortcutsDefinition.AsRequired()); command.Options.Add(FabricOptionDefinitions.ShortcutConflictPolicy.AsOptional()); - command.Validators.Add(result => - { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); - var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - var item = result.GetValueOrDefault(FabricOptionDefinitions.Item.Name); - - if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) - { - result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); - } - - if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) - { - result.AddError("Item identifier is required. Provide --item or --item-id."); - } - }); } protected override ShortcutCreateOrUpdateOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.Item.Name); options.ShortcutsDefinition = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutsDefinition.Name); options.ShortcutConflictPolicy = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutConflictPolicy.Name); return options; @@ -87,24 +66,19 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); try { - var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) - ? options.WorkspaceId - : options.Workspace; - var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) - ? options.ItemId - : options.Item; - var result = await _oneLakeService.CreateOrUpdateShortcutsAsync(workspaceIdentifier!, itemIdentifier!, options.ShortcutsDefinition!, options.ShortcutConflictPolicy, cancellationToken); + var result = await _oneLakeService.CreateOrUpdateShortcutsAsync(options.WorkspaceId!, options.ItemId!, options.ShortcutsDefinition!, options.ShortcutConflictPolicy, cancellationToken); context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.BulkCreateShortcutResponse); } catch (Exception ex) { _logger.LogError(ex, "Error creating/updating shortcuts. Workspace: {Workspace}, Item: {Item}.", - options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item); + options.WorkspaceId, options.ItemId); HandleException(context, ex); } return context.Response; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutDeleteCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutDeleteCommand.cs index 6a6abf291f..86dff0f210 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutDeleteCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutDeleteCommand.cs @@ -36,38 +36,17 @@ public sealed class ShortcutDeleteCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); - command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); command.Options.Add(FabricOptionDefinitions.ShortcutPath.AsRequired()); command.Options.Add(FabricOptionDefinitions.ShortcutName.AsRequired()); - command.Validators.Add(result => - { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); - var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - var item = result.GetValueOrDefault(FabricOptionDefinitions.Item.Name); - - if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) - { - result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); - } - - if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) - { - result.AddError("Item identifier is required. Provide --item or --item-id."); - } - }); } protected override ShortcutDeleteOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.Item.Name); options.ShortcutPath = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPath.Name); options.ShortcutName = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutName.Name); return options; @@ -83,43 +62,22 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); try { - var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) - ? options.WorkspaceId - : options.Workspace; - var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) - ? options.ItemId - : options.Item; - await _oneLakeService.DeleteShortcutAsync(workspaceIdentifier!, itemIdentifier!, options.ShortcutPath!, options.ShortcutName!, cancellationToken); + await _oneLakeService.DeleteShortcutAsync(options.WorkspaceId!, options.ItemId!, options.ShortcutPath!, options.ShortcutName!, cancellationToken); var result = new ShortcutDeleteCommandResult(options.ShortcutPath!, options.ShortcutName!, "Shortcut deleted successfully."); context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.ShortcutDeleteCommandResult); } catch (Exception ex) { _logger.LogError(ex, "Error deleting shortcut. Workspace: {Workspace}, Item: {Item}, Path: {Path}, Name: {Name}.", - options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item, options.ShortcutPath, options.ShortcutName); + options.WorkspaceId, options.ItemId, options.ShortcutPath, options.ShortcutName); HandleException(context, ex); } return context.Response; } - public sealed class ShortcutDeleteCommandResult - { - public string ShortcutPath { get; init; } = string.Empty; - public string ShortcutName { get; init; } = string.Empty; - public string Message { get; init; } = string.Empty; - - public ShortcutDeleteCommandResult() - { - } - - public ShortcutDeleteCommandResult(string shortcutPath, string shortcutName, string message) - { - ShortcutPath = shortcutPath; - ShortcutName = shortcutName; - Message = message; - } - } + public sealed record ShortcutDeleteCommandResult(string ShortcutPath, string ShortcutName, string Message); } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutGetCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutGetCommand.cs index 84d28d61c6..bf3355f3ee 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutGetCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutGetCommand.cs @@ -35,38 +35,17 @@ public sealed class ShortcutGetCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); - command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); command.Options.Add(FabricOptionDefinitions.ShortcutPath.AsRequired()); command.Options.Add(FabricOptionDefinitions.ShortcutName.AsRequired()); - command.Validators.Add(result => - { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); - var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - var item = result.GetValueOrDefault(FabricOptionDefinitions.Item.Name); - - if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) - { - result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); - } - - if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) - { - result.AddError("Item identifier is required. Provide --item or --item-id."); - } - }); } protected override ShortcutGetOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.Item.Name); options.ShortcutPath = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPath.Name); options.ShortcutName = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutName.Name); return options; @@ -82,24 +61,19 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); try { - var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) - ? options.WorkspaceId - : options.Workspace; - var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) - ? options.ItemId - : options.Item; - var result = await _oneLakeService.GetShortcutAsync(workspaceIdentifier!, itemIdentifier!, options.ShortcutPath!, options.ShortcutName!, cancellationToken); + var result = await _oneLakeService.GetShortcutAsync(options.WorkspaceId!, options.ItemId!, options.ShortcutPath!, options.ShortcutName!, cancellationToken); context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); } catch (Exception ex) { _logger.LogError(ex, "Error getting shortcut. Workspace: {Workspace}, Item: {Item}, Path: {Path}, Name: {Name}.", - options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item, options.ShortcutPath, options.ShortcutName); + options.WorkspaceId, options.ItemId, options.ShortcutPath, options.ShortcutName); HandleException(context, ex); } return context.Response; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs index 2afea6519c..22ac950c89 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs @@ -35,38 +35,19 @@ public sealed class ShortcutListCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); - command.Options.Add(FabricOptionDefinitions.ItemId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Item.AsOptional()); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); command.Options.Add(FabricOptionDefinitions.ParentPath.AsOptional()); - command.Validators.Add(result => - { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); - var itemId = result.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - var item = result.GetValueOrDefault(FabricOptionDefinitions.Item.Name); - - if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) - { - result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); - } - - if (string.IsNullOrWhiteSpace(item) && string.IsNullOrWhiteSpace(itemId)) - { - result.AddError("Item identifier is required. Provide --item or --item-id."); - } - }); + command.Options.Add(FabricOptionDefinitions.ContinuationToken.AsOptional()); } protected override ShortcutListOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - options.Item = parseResult.GetValueOrDefault(FabricOptionDefinitions.Item.Name); options.ParentPath = parseResult.GetValueOrDefault(FabricOptionDefinitions.ParentPath.Name); + options.ContinuationToken = parseResult.GetValueOrDefault(FabricOptionDefinitions.ContinuationTokenName); return options; } @@ -80,24 +61,18 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); try { - var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) - ? options.WorkspaceId - : options.Workspace; - var itemIdentifier = !string.IsNullOrWhiteSpace(options.ItemId) - ? options.ItemId - : options.Item; - - var result = await _oneLakeService.ListShortcutsAsync(workspaceIdentifier!, itemIdentifier!, options.ParentPath, cancellationToken); + var result = await _oneLakeService.ListShortcutsAsync(options.WorkspaceId!, options.ItemId!, options.ParentPath, options.ContinuationToken, cancellationToken); context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.ShortcutListResponse); } catch (Exception ex) { _logger.LogError(ex, "Error listing shortcuts. Workspace: {Workspace}, Item: {Item}.", - options.WorkspaceId ?? options.Workspace, options.ItemId ?? options.Item); + options.WorkspaceId, options.ItemId); HandleException(context, ex); } return context.Response; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutResetCacheCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutResetCacheCommand.cs index fdc677570b..f23f114ae4 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutResetCacheCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutResetCacheCommand.cs @@ -16,7 +16,7 @@ namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; Name = "reset_shortcut_cache", Title = "Reset OneLake Shortcut Cache", Description = """ - Drop cached shortcut reads for an item, forcing the next read to + Drop cached shortcut reads for a workspace, forcing the next read to re-resolve from the destination. Use sparingly — primarily for debugging stale-cache issues. Requires OneLake.ReadWrite.All. """, @@ -36,25 +36,13 @@ public sealed class ShortcutResetCacheCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); - command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); - command.Validators.Add(result => - { - var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); - - if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) - { - result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); - } - }); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); } protected override ShortcutResetCacheOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - options.Workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); return options; } @@ -68,35 +56,21 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); try { - var workspaceIdentifier = !string.IsNullOrWhiteSpace(options.WorkspaceId) - ? options.WorkspaceId - : options.Workspace; - await _oneLakeService.ResetShortcutCacheAsync(workspaceIdentifier!, cancellationToken); + await _oneLakeService.ResetShortcutCacheAsync(options.WorkspaceId!, cancellationToken); var result = new ShortcutResetCacheCommandResult("Shortcut cache reset successfully."); context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.ShortcutResetCacheCommandResult); } catch (Exception ex) { _logger.LogError(ex, "Error resetting shortcut cache. Workspace: {Workspace}.", - options.WorkspaceId ?? options.Workspace); + options.WorkspaceId); HandleException(context, ex); } return context.Response; } - public sealed class ShortcutResetCacheCommandResult - { - public string Message { get; init; } = string.Empty; - - public ShortcutResetCacheCommandResult() - { - } - - public ShortcutResetCacheCommandResult(string message) - { - Message = message; - } - } + public sealed record ShortcutResetCacheCommandResult(string Message); } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleCreateOrUpdateOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleCreateOrUpdateOptions.cs index 90941dd0c2..6beb75725a 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleCreateOrUpdateOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleCreateOrUpdateOptions.cs @@ -8,8 +8,7 @@ namespace Fabric.Mcp.Tools.OneLake.Options; public sealed class DataAccessRoleCreateOrUpdateOptions : GlobalOptions { public string? WorkspaceId { get; set; } - public string? Workspace { get; set; } public string? ItemId { get; set; } - public string? Item { get; set; } public string? RoleDefinition { get; set; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleDeleteOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleDeleteOptions.cs index 1f010b34f8..ad4a0e5805 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleDeleteOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleDeleteOptions.cs @@ -8,8 +8,7 @@ namespace Fabric.Mcp.Tools.OneLake.Options; public sealed class DataAccessRoleDeleteOptions : GlobalOptions { public string? WorkspaceId { get; set; } - public string? Workspace { get; set; } public string? ItemId { get; set; } - public string? Item { get; set; } public string? RoleName { get; set; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleGetOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleGetOptions.cs index 5cbb378fd4..1a983a5bd1 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleGetOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleGetOptions.cs @@ -8,8 +8,7 @@ namespace Fabric.Mcp.Tools.OneLake.Options; public sealed class DataAccessRoleGetOptions : GlobalOptions { public string? WorkspaceId { get; set; } - public string? Workspace { get; set; } public string? ItemId { get; set; } - public string? Item { get; set; } public string? RoleName { get; set; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleListOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleListOptions.cs index 354dd250e9..b0971180be 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleListOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleListOptions.cs @@ -8,7 +8,7 @@ namespace Fabric.Mcp.Tools.OneLake.Options; public sealed class DataAccessRoleListOptions : GlobalOptions { public string? WorkspaceId { get; set; } - public string? Workspace { get; set; } public string? ItemId { get; set; } - public string? Item { get; set; } + public string? ContinuationToken { get; set; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DiagnosticsModifyOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DiagnosticsModifyOptions.cs index d996930871..c3d9cf62e0 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DiagnosticsModifyOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DiagnosticsModifyOptions.cs @@ -8,6 +8,6 @@ namespace Fabric.Mcp.Tools.OneLake.Options; public sealed class DiagnosticsModifyOptions : GlobalOptions { public string? WorkspaceId { get; set; } - public string? Workspace { get; set; } public string? DiagnosticsConfig { get; set; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ImmutabilityPolicyModifyOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ImmutabilityPolicyModifyOptions.cs index c5e4cc0513..c9b03438ff 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ImmutabilityPolicyModifyOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ImmutabilityPolicyModifyOptions.cs @@ -8,6 +8,6 @@ namespace Fabric.Mcp.Tools.OneLake.Options; public sealed class ImmutabilityPolicyModifyOptions : GlobalOptions { public string? WorkspaceId { get; set; } - public string? Workspace { get; set; } public string? ImmutabilityPolicyConfig { get; set; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/SettingsGetOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/SettingsGetOptions.cs index 793940f548..d7434f053e 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/SettingsGetOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/SettingsGetOptions.cs @@ -8,5 +8,5 @@ namespace Fabric.Mcp.Tools.OneLake.Options; public sealed class SettingsGetOptions : GlobalOptions { public string? WorkspaceId { get; set; } - public string? Workspace { get; set; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOrUpdateOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOrUpdateOptions.cs index 12a684c4ef..632c971ec2 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOrUpdateOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOrUpdateOptions.cs @@ -8,9 +8,8 @@ namespace Fabric.Mcp.Tools.OneLake.Options; public sealed class ShortcutCreateOrUpdateOptions : GlobalOptions { public string? WorkspaceId { get; set; } - public string? Workspace { get; set; } public string? ItemId { get; set; } - public string? Item { get; set; } public string? ShortcutsDefinition { get; set; } public string? ShortcutConflictPolicy { get; set; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutDeleteOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutDeleteOptions.cs index 29c23bc7f7..e85afb81f5 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutDeleteOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutDeleteOptions.cs @@ -8,9 +8,8 @@ namespace Fabric.Mcp.Tools.OneLake.Options; public sealed class ShortcutDeleteOptions : GlobalOptions { public string? WorkspaceId { get; set; } - public string? Workspace { get; set; } public string? ItemId { get; set; } - public string? Item { get; set; } public string? ShortcutName { get; set; } public string? ShortcutPath { get; set; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutGetOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutGetOptions.cs index 4130f26f43..befe963f94 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutGetOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutGetOptions.cs @@ -8,9 +8,8 @@ namespace Fabric.Mcp.Tools.OneLake.Options; public sealed class ShortcutGetOptions : GlobalOptions { public string? WorkspaceId { get; set; } - public string? Workspace { get; set; } public string? ItemId { get; set; } - public string? Item { get; set; } public string? ShortcutName { get; set; } public string? ShortcutPath { get; set; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutListOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutListOptions.cs index 04c2b949be..2be48d5631 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutListOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutListOptions.cs @@ -8,8 +8,8 @@ namespace Fabric.Mcp.Tools.OneLake.Options; public sealed class ShortcutListOptions : GlobalOptions { public string? WorkspaceId { get; set; } - public string? Workspace { get; set; } public string? ItemId { get; set; } - public string? Item { get; set; } public string? ParentPath { get; set; } + public string? ContinuationToken { get; set; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutResetCacheOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutResetCacheOptions.cs index 763ab04d6b..aa2bd0a77d 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutResetCacheOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutResetCacheOptions.cs @@ -8,5 +8,5 @@ namespace Fabric.Mcp.Tools.OneLake.Options; public sealed class ShortcutResetCacheOptions : GlobalOptions { public string? WorkspaceId { get; set; } - public string? Workspace { get; set; } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs index a3d11c3da7..5dedeec982 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs @@ -48,13 +48,13 @@ public interface IOneLakeService Task GetTableAsync(string workspaceIdentifier, string itemIdentifier, string namespaceName, string tableName, CancellationToken cancellationToken = default); // Data Access Security Operations - Task ListDataAccessRolesAsync(string workspaceId, string itemId, CancellationToken cancellationToken = default); + Task ListDataAccessRolesAsync(string workspaceId, string itemId, string? continuationToken = null, CancellationToken cancellationToken = default); Task GetDataAccessRoleAsync(string workspaceId, string itemId, string roleName, CancellationToken cancellationToken = default); Task CreateOrUpdateDataAccessRoleAsync(string workspaceId, string itemId, string roleDefinitionJson, CancellationToken cancellationToken = default); Task DeleteDataAccessRoleAsync(string workspaceId, string itemId, string roleName, CancellationToken cancellationToken = default); // Shortcut Operations - Task ListShortcutsAsync(string workspaceId, string itemId, string? parentPath = null, CancellationToken cancellationToken = default); + Task ListShortcutsAsync(string workspaceId, string itemId, string? parentPath = null, string? continuationToken = null, CancellationToken cancellationToken = default); Task GetShortcutAsync(string workspaceId, string itemId, string shortcutPath, string shortcutName, CancellationToken cancellationToken = default); Task CreateOrUpdateShortcutsAsync(string workspaceId, string itemId, string shortcutsJson, string? shortcutConflictPolicy = null, CancellationToken cancellationToken = default); Task DeleteShortcutAsync(string workspaceId, string itemId, string shortcutPath, string shortcutName, CancellationToken cancellationToken = default); diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs index b650f51cc9..d1f28d9ad3 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs @@ -1915,9 +1915,14 @@ public void Dispose() } // Data Access Security Operations - public async Task ListDataAccessRolesAsync(string workspaceId, string itemId, CancellationToken cancellationToken = default) + public async Task ListDataAccessRolesAsync(string workspaceId, string itemId, string? continuationToken = null, CancellationToken cancellationToken = default) { var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/dataAccessRoles"; + if (!string.IsNullOrWhiteSpace(continuationToken)) + { + url += $"?continuationToken={Uri.EscapeDataString(continuationToken)}"; + } + var response = await SendFabricApiRequestAsync(HttpMethod.Get, url, cancellationToken: cancellationToken); return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.DataAccessRoleListResponse, cancellationToken) ?? new DataAccessRoleListResponse(); } @@ -1965,13 +1970,25 @@ public async Task DeleteDataAccessRoleAsync(string workspaceId, string itemId, s } // Shortcut Operations - public async Task ListShortcutsAsync(string workspaceId, string itemId, string? parentPath = null, CancellationToken cancellationToken = default) + public async Task ListShortcutsAsync(string workspaceId, string itemId, string? parentPath = null, string? continuationToken = null, CancellationToken cancellationToken = default) { var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/shortcuts"; + var queryParams = new List(); if (!string.IsNullOrWhiteSpace(parentPath)) { - url += $"?parentPath={Uri.EscapeDataString(parentPath)}"; + queryParams.Add($"parentPath={Uri.EscapeDataString(parentPath)}"); + } + + if (!string.IsNullOrWhiteSpace(continuationToken)) + { + queryParams.Add($"continuationToken={Uri.EscapeDataString(continuationToken)}"); } + + if (queryParams.Count > 0) + { + url += $"?{string.Join("&", queryParams)}"; + } + var response = await SendFabricApiRequestAsync(HttpMethod.Get, url, cancellationToken: cancellationToken); return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.ShortcutListResponse, cancellationToken) ?? new ShortcutListResponse(); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleCreateOrUpdateCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleCreateOrUpdateCommandTests.cs index 30c8a4687a..a87360ab28 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleCreateOrUpdateCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleCreateOrUpdateCommandTests.cs @@ -61,7 +61,6 @@ public void Metadata_HasCorrectProperties() [Theory] [InlineData("--workspace-id ws1 --item-id item1", true)] - [InlineData("--workspace ws1 --item item1", true)] [InlineData("--item-id item1", false)] // missing workspace [InlineData("--workspace-id ws1", false)] // missing item [InlineData("", false)] @@ -124,3 +123,4 @@ public async Task ExecuteAsync_HandlesServiceErrors() Assert.NotEqual(HttpStatusCode.OK, response.Status); } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleListCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleListCommandTests.cs index 58d4399b7f..4518bfe475 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleListCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Security/DataAccessRoleListCommandTests.cs @@ -28,6 +28,13 @@ public void GetCommand_ReturnsValidCommand() Assert.NotEmpty(CommandDefinition.Options); } + [Fact] + public void GetCommand_RegistersContinuationTokenOption() + { + var option = CommandDefinition.Options.FirstOrDefault(o => o.Name == "--continuation-token"); + Assert.NotNull(option); + } + [Fact] public void Constructor_ThrowsArgumentNullException_WhenLoggerIsNull() { diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/DiagnosticsModifyCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/DiagnosticsModifyCommandTests.cs index cffca59938..9ad9f16b85 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/DiagnosticsModifyCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/DiagnosticsModifyCommandTests.cs @@ -59,7 +59,6 @@ public void Metadata_HasCorrectProperties() [Theory] [InlineData("--workspace-id ws1 --diagnostics-config {\"status\":\"Disabled\"}", true)] - [InlineData("--workspace ws1 --diagnostics-config {\"status\":\"Disabled\"}", true)] [InlineData("--diagnostics-config {\"status\":\"Disabled\"}", false)] public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) { @@ -107,3 +106,4 @@ public async Task ExecuteAsync_HandlesServiceErrors() Assert.NotEqual(HttpStatusCode.OK, response.Status); } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs index 5791e2f018..4a1b72d28a 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs @@ -59,7 +59,6 @@ public void Metadata_HasCorrectProperties() [Theory] [InlineData("--workspace-id ws1 --immutability-policy {\"scope\":\"DiagnosticLogs\",\"retentionDays\":7}", true)] - [InlineData("--workspace ws1 --immutability-policy {\"scope\":\"DiagnosticLogs\",\"retentionDays\":7}", true)] [InlineData("--immutability-policy {\"scope\":\"DiagnosticLogs\",\"retentionDays\":7}", false)] public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) { @@ -107,3 +106,4 @@ public async Task ExecuteAsync_HandlesServiceErrors() Assert.NotEqual(HttpStatusCode.OK, response.Status); } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateOrUpdateCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateOrUpdateCommandTests.cs index ee4ec00711..958a3dcd20 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateOrUpdateCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateOrUpdateCommandTests.cs @@ -61,7 +61,6 @@ public void Metadata_HasCorrectProperties() [Theory] [InlineData("--workspace-id ws1 --item-id item1", true)] - [InlineData("--workspace ws1 --item item1", true)] [InlineData("--item-id item1", false)] // missing workspace [InlineData("--workspace-id ws1", false)] // missing item [InlineData("", false)] @@ -139,3 +138,4 @@ public async Task ExecuteAsync_HandlesServiceErrors() Assert.NotEqual(HttpStatusCode.OK, response.Status); } } + diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutListCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutListCommandTests.cs index 930642dbea..e6ad3d0ab9 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutListCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutListCommandTests.cs @@ -28,6 +28,13 @@ public void GetCommand_ReturnsValidCommand() Assert.NotEmpty(CommandDefinition.Options); } + [Fact] + public void GetCommand_RegistersContinuationTokenOption() + { + var option = CommandDefinition.Options.FirstOrDefault(o => o.Name == "--continuation-token"); + Assert.NotNull(option); + } + [Fact] public void Constructor_ThrowsArgumentNullException_WhenLoggerIsNull() { diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutResetCacheCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutResetCacheCommandTests.cs index 60325cf338..74cd20fea3 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutResetCacheCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutResetCacheCommandTests.cs @@ -59,7 +59,6 @@ public void Metadata_HasCorrectProperties() [Theory] [InlineData("--workspace-id ws1", true)] - [InlineData("--workspace ws1", true)] [InlineData("", false)] public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) { @@ -104,14 +103,11 @@ public async Task ExecuteAsync_HandlesServiceErrors() } [Fact] - public void BindOptions_UsesWorkspaceName_WhenWorkspaceIdNotProvided() + public void BindOptions_RequiresWorkspaceId() { - Service.ResetShortcutCacheAsync(Arg.Any(), Arg.Any()) - .Returns(Task.CompletedTask); - - // Validator should pass with --workspace (friendly name) - var parseResult = CommandDefinition.Parse("--workspace myWorkspace"); + var parseResult = CommandDefinition.Parse(string.Empty); var isValid = Command.Validate(parseResult.CommandResult); - Assert.True(isValid.IsValid); + Assert.False(isValid.IsValid); } } + From 5c23e696d70020c7e587fda15633be7814cfa382 Mon Sep 17 00:00:00 2001 From: Srinivas Thatipamula Date: Mon, 18 May 2026 14:42:34 -0700 Subject: [PATCH 06/15] Add JSON validation for settings methods and read API response in role creation - ModifyDiagnosticsAsync and ModifyImmutabilityPolicyAsync now validate JSON input early with JsonDocument.Parse, throwing a clear ArgumentException for malformed JSON. - CreateOrUpdateDataAccessRoleAsync now reads and deserializes the API response stream to preserve server-assigned fields (id, eTag, etc.), aligning with CreateOrUpdateShortcutsAsync behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Services/OneLakeService.cs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs index d1f28d9ad3..91726368c0 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs @@ -1958,8 +1958,8 @@ public async Task CreateOrUpdateDataAccessRoleAsync(string works // touching other roles on the item (unlike the bulk PUT approach). var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/dataAccessRoles?preview=true&dataAccessRoleConflictPolicy=Overwrite"; var requestBody = JsonSerializer.Serialize(roleDefinition, OneLakeJsonContext.Default.DataAccessRole); - await SendFabricApiRequestAsync(HttpMethod.Post, url, requestBody, cancellationToken: cancellationToken); - return roleDefinition; + var responseStream = await SendFabricApiRequestAsync(HttpMethod.Post, url, requestBody, cancellationToken: cancellationToken); + return await JsonSerializer.DeserializeAsync(responseStream, OneLakeJsonContext.Default.DataAccessRole, cancellationToken) ?? roleDefinition; } public async Task DeleteDataAccessRoleAsync(string workspaceId, string itemId, string roleName, CancellationToken cancellationToken = default) @@ -2053,12 +2053,30 @@ public async Task GetSettingsAsync(string workspaceId, Cancella public async Task ModifyDiagnosticsAsync(string workspaceId, string diagnosticsConfigJson, CancellationToken cancellationToken = default) { + try + { + using var doc = JsonDocument.Parse(diagnosticsConfigJson); + } + catch (JsonException ex) + { + throw new ArgumentException($"Invalid diagnostics configuration JSON: {ex.Message}", nameof(diagnosticsConfigJson), ex); + } + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/onelake/settings/modifyDiagnostics"; await SendFabricApiRequestAsync(HttpMethod.Post, url, diagnosticsConfigJson, cancellationToken: cancellationToken); } public async Task ModifyImmutabilityPolicyAsync(string workspaceId, string immutabilityPolicyJson, CancellationToken cancellationToken = default) { + try + { + using var doc = JsonDocument.Parse(immutabilityPolicyJson); + } + catch (JsonException ex) + { + throw new ArgumentException($"Invalid immutability policy JSON: {ex.Message}", nameof(immutabilityPolicyJson), ex); + } + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/onelake/settings/modifyImmutabilityPolicy"; await SendFabricApiRequestAsync(HttpMethod.Post, url, immutabilityPolicyJson, cancellationToken: cancellationToken); } From d5387f4b8dc3086814cdc1ae25944ecbf35e9266 Mon Sep 17 00:00:00 2001 From: Srinivas Thatipamula Date: Fri, 29 May 2026 16:42:50 -0700 Subject: [PATCH 07/15] Address bug bash feedback for OneLake security/settings tools - Bug 1: Add --local-path alias for --local-file-path option - Bug 2: Accept --workspace (GUID) in addition to --workspace-id for security and settings commands with GUID format validation - Bug 4: Enhance role-definition description with path-scoped example - Bug 5: Clarify sourcePath usage in data access role list description - Bug 6: Default tenantId from JWT tid claim when members omit it Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DataAccessRoleCreateOrUpdateCommand.cs | 22 +++++++- .../Security/DataAccessRoleDeleteCommand.cs | 22 +++++++- .../Security/DataAccessRoleGetCommand.cs | 22 +++++++- .../Security/DataAccessRoleListCommand.cs | 25 ++++++++- .../Settings/DiagnosticsModifyCommand.cs | 22 +++++++- .../ImmutabilityPolicyModifyCommand.cs | 22 +++++++- .../Commands/Settings/SettingsGetCommand.cs | 22 +++++++- .../src/Options/FabricOptionDefinitions.cs | 12 ++++- .../src/Services/OneLakeService.cs | 52 +++++++++++++++++++ 9 files changed, 205 insertions(+), 16 deletions(-) diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs index e41dc0a3bb..9ca5b7d0a6 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs @@ -40,15 +40,33 @@ public sealed class DataAccessRoleCreateOrUpdateCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); command.Options.Add(FabricOptionDefinitions.RoleDefinition.AsRequired()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + + var effectiveValue = !string.IsNullOrWhiteSpace(workspaceId) ? workspaceId : workspace; + if (!string.IsNullOrWhiteSpace(effectiveValue) && !Guid.TryParse(effectiveValue, out _)) + { + result.AddError("Workspace must be a valid GUID. Name-based resolution is not supported for this command."); + } + }); } protected override DataAccessRoleCreateOrUpdateOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + options.WorkspaceId = !string.IsNullOrWhiteSpace(workspaceId) ? workspaceId! : workspace ?? string.Empty; options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); options.RoleDefinition = parseResult.GetValueOrDefault(FabricOptionDefinitions.RoleDefinition.Name); return options; diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleDeleteCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleDeleteCommand.cs index 44d6fa1d43..c387c5462f 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleDeleteCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleDeleteCommand.cs @@ -38,15 +38,33 @@ public sealed class DataAccessRoleDeleteCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); command.Options.Add(FabricOptionDefinitions.RoleName.AsRequired()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + + var effectiveValue = !string.IsNullOrWhiteSpace(workspaceId) ? workspaceId : workspace; + if (!string.IsNullOrWhiteSpace(effectiveValue) && !Guid.TryParse(effectiveValue, out _)) + { + result.AddError("Workspace must be a valid GUID. Name-based resolution is not supported for this command."); + } + }); } protected override DataAccessRoleDeleteOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + options.WorkspaceId = !string.IsNullOrWhiteSpace(workspaceId) ? workspaceId! : workspace ?? string.Empty; options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); options.RoleName = parseResult.GetValueOrDefault(FabricOptionDefinitions.RoleName.Name); return options; diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs index ee395994bc..5a53e25972 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs @@ -40,15 +40,33 @@ public sealed class DataAccessRoleGetCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); command.Options.Add(FabricOptionDefinitions.RoleName.AsRequired()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + + var effectiveValue = !string.IsNullOrWhiteSpace(workspaceId) ? workspaceId : workspace; + if (!string.IsNullOrWhiteSpace(effectiveValue) && !Guid.TryParse(effectiveValue, out _)) + { + result.AddError("Workspace must be a valid GUID. Name-based resolution is not supported for this command."); + } + }); } protected override DataAccessRoleGetOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + options.WorkspaceId = !string.IsNullOrWhiteSpace(workspaceId) ? workspaceId! : workspace ?? string.Empty; options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); options.RoleName = parseResult.GetValueOrDefault(FabricOptionDefinitions.RoleName.Name); return options; diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs index 875803fccb..d3f6411dfc 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs @@ -22,6 +22,9 @@ List all data access roles defined on a single item (Lakehouse / Warehouse) — item. For looking up a specific role by name, fetch the list and pick by name; there is no server-side search. Caller must be a workspace Admin or Member on the item's workspace. Requires OneLake.Read.All. + Note: Built-in roles (e.g. DefaultReader) may include fabricItemMembers with + a 'sourcePath' field formatted as '/' — this is NOT a + OneLake file path; it identifies the workspace/item granting inherited access. """, Destructive = false, Idempotent = true, @@ -39,15 +42,33 @@ public sealed class DataAccessRoleListCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); command.Options.Add(FabricOptionDefinitions.ContinuationToken.AsOptional()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + + var effectiveValue = !string.IsNullOrWhiteSpace(workspaceId) ? workspaceId : workspace; + if (!string.IsNullOrWhiteSpace(effectiveValue) && !Guid.TryParse(effectiveValue, out _)) + { + result.AddError("Workspace must be a valid GUID. Name-based resolution is not supported for this command."); + } + }); } protected override DataAccessRoleListOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + options.WorkspaceId = !string.IsNullOrWhiteSpace(workspaceId) ? workspaceId! : workspace ?? string.Empty; options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); options.ContinuationToken = parseResult.GetValueOrDefault(FabricOptionDefinitions.ContinuationTokenName); return options; diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs index 61970b1b2a..566742a307 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs @@ -37,14 +37,32 @@ public sealed class DiagnosticsModifyCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); command.Options.Add(FabricOptionDefinitions.DiagnosticsConfig.AsRequired()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + + var effectiveValue = !string.IsNullOrWhiteSpace(workspaceId) ? workspaceId : workspace; + if (!string.IsNullOrWhiteSpace(effectiveValue) && !Guid.TryParse(effectiveValue, out _)) + { + result.AddError("Workspace must be a valid GUID. Name-based resolution is not supported for this command."); + } + }); } protected override DiagnosticsModifyOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + options.WorkspaceId = !string.IsNullOrWhiteSpace(workspaceId) ? workspaceId! : workspace ?? string.Empty; options.DiagnosticsConfig = parseResult.GetValueOrDefault(FabricOptionDefinitions.DiagnosticsConfig.Name); return options; } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs index 7726ac91e5..a2d4cbd623 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs @@ -36,14 +36,32 @@ public sealed class ImmutabilityPolicyModifyCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); command.Options.Add(FabricOptionDefinitions.ImmutabilityPolicyConfig.AsRequired()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + + var effectiveValue = !string.IsNullOrWhiteSpace(workspaceId) ? workspaceId : workspace; + if (!string.IsNullOrWhiteSpace(effectiveValue) && !Guid.TryParse(effectiveValue, out _)) + { + result.AddError("Workspace must be a valid GUID. Name-based resolution is not supported for this command."); + } + }); } protected override ImmutabilityPolicyModifyOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + options.WorkspaceId = !string.IsNullOrWhiteSpace(workspaceId) ? workspaceId! : workspace ?? string.Empty; options.ImmutabilityPolicyConfig = parseResult.GetValueOrDefault(FabricOptionDefinitions.ImmutabilityPolicyConfig.Name); return options; } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs index 5ff783c928..a5261eaf76 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs @@ -35,13 +35,31 @@ public sealed class SettingsGetCommand( protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); + command.Validators.Add(result => + { + var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = result.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + if (string.IsNullOrWhiteSpace(workspaceId) && string.IsNullOrWhiteSpace(workspace)) + { + result.AddError("Workspace identifier is required. Provide --workspace or --workspace-id."); + } + + var effectiveValue = !string.IsNullOrWhiteSpace(workspaceId) ? workspaceId : workspace; + if (!string.IsNullOrWhiteSpace(effectiveValue) && !Guid.TryParse(effectiveValue, out _)) + { + result.AddError("Workspace must be a valid GUID. Name-based resolution is not supported for this command."); + } + }); } protected override SettingsGetOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + var workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); + options.WorkspaceId = !string.IsNullOrWhiteSpace(workspaceId) ? workspaceId! : workspace ?? string.Empty; return options; } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs index f43c8047a9..87352bf820 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs @@ -98,7 +98,7 @@ public static class FabricOptionDefinitions }; public const string LocalFilePathName = "local-file-path"; - public static readonly Option LocalFilePath = new($"--{LocalFilePathName}") + public static readonly Option LocalFilePath = new($"--{LocalFilePathName}", "--local-path") { Description = "The path to a local file to upload.", Required = false @@ -190,7 +190,15 @@ public static class FabricOptionDefinitions public const string RoleDefinitionName = "role-definition"; public static readonly Option RoleDefinition = new($"--{RoleDefinitionName}") { - Description = "JSON definition of the data access role including members and decision rules.", + Description = """ + JSON definition of the data access role. Must include 'name', 'members' (with microsoftEntraMembers), + and 'decisionRules'. To scope access to a specific folder, include a Path attribute in decisionRules. + Example: {"name":"ImagesReadOnly","members":{"microsoftEntraMembers":[{"objectId":""}]}, + "decisionRules":[{"effect":"Permit","permission":[ + {"attributeName":"Action","attributeValueIncludedIn":["Read"]}, + {"attributeName":"Path","attributeValueIncludedIn":["Files/images/*"]}]}]}. + Omitting Path grants access to the entire item. + """, Required = true }; diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs index 91726368c0..5786559218 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs @@ -1953,6 +1953,21 @@ public async Task CreateOrUpdateDataAccessRoleAsync(string works throw new ArgumentException("Role definition must include a non-empty 'name' property.", nameof(roleDefinitionJson)); } + // Default tenantId on Entra members when omitted — saves callers from having to + // look up their own tenant ID for the most common single-tenant scenario. + if (roleDefinition.Members?.MicrosoftEntraMembers is { Count: > 0 } entraMembers + && entraMembers.Any(m => string.IsNullOrWhiteSpace(m.TenantId))) + { + var tenantId = await GetTenantIdFromTokenAsync(cancellationToken); + if (!string.IsNullOrWhiteSpace(tenantId)) + { + foreach (var member in entraMembers) + { + member.TenantId ??= tenantId; + } + } + } + // Use the single-role POST endpoint (preview API) with Overwrite conflict policy. // This creates the role if it doesn't exist, or replaces it if it does — without // touching other roles on the item (unlike the bulk PUT approach). @@ -2094,6 +2109,43 @@ private async Task SendFabricApiDeleteRequestAsync(string url, CancellationToken response.EnsureSuccessStatusCode(); } + private async Task GetTenantIdFromTokenAsync(CancellationToken cancellationToken) + { + try + { + var tokenContext = new TokenRequestContext(new[] { OneLakeEndpoints.GetFabricScope() }); + var token = await _credential.GetTokenAsync(tokenContext, cancellationToken); + + // JWT is three base64url segments separated by dots; the payload is segment[1]. + var parts = token.Token.Split('.'); + if (parts.Length < 2) + { + return null; + } + + var payload = parts[1]; + // Pad base64url to standard base64 + switch (payload.Length % 4) + { + case 2: payload += "=="; break; + case 3: payload += "="; break; + } + + var bytes = Convert.FromBase64String(payload.Replace('-', '+').Replace('_', '/')); + using var doc = JsonDocument.Parse(bytes); + if (doc.RootElement.TryGetProperty("tid", out var tidElement)) + { + return tidElement.GetString(); + } + } + catch + { + // Best-effort: if token parsing fails, caller should provide tenantId explicitly. + } + + return null; + } + private static string ExtractWarehouseQueryValue(string warehousePrefix) { const string WarehousePrefixRoot = "warehouse/"; From 86b2e98e79289351acd89ee957e2db5f2cbece4a Mon Sep 17 00:00:00 2001 From: Srinivas Thatipamula Date: Fri, 29 May 2026 16:51:26 -0700 Subject: [PATCH 08/15] Bug 3: Resolve email/UPN to Entra object ID via Graph API Add principal resolution in OneLakeService for CreateOrUpdateDataAccessRole. Non-GUID objectId values are automatically resolved: 1. GET /users/{email} (covers UPN and user email) 2. GET /groups?filter=mail eq '{email}' (covers mail-enabled groups/DLs) Fails with clear error listing unresolved principals. Updated command description to document email/UPN support and required Graph permissions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DataAccessRoleCreateOrUpdateCommand.cs | 8 +- .../src/Services/OneLakeService.cs | 103 ++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs index 9ca5b7d0a6..4ab65f3823 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs @@ -21,8 +21,12 @@ one item per call — does not affect other roles on the item or any roles on other items, so it is safe to call in a loop when multiple roles or multiple items need changing. There is no bulk variant: the underlying PUT-all API was intentionally not exposed because partial reads would - silently delete roles. Caller must be a workspace Admin or Member on the - item's workspace. Requires OneLake.ReadWrite.All. + silently delete roles. Members can be specified by Entra object ID (GUID), + email address, or UPN — non-GUID values are automatically resolved via + Microsoft Graph (tries /users then /groups by mail). Caller must be a + workspace Admin or Member on the item's workspace. Requires + OneLake.ReadWrite.All and User.Read.All + GroupMember.Read.All for + principal resolution. """, Destructive = false, Idempotent = true, diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs index 5786559218..58f6e1fe4a 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs @@ -1968,6 +1968,12 @@ public async Task CreateOrUpdateDataAccessRoleAsync(string works } } + // Resolve any email/UPN-based principals to Entra object IDs via Graph API. + if (roleDefinition.Members?.MicrosoftEntraMembers is { Count: > 0 } membersToResolve) + { + await ResolvePrincipalsAsync(membersToResolve, cancellationToken); + } + // Use the single-role POST endpoint (preview API) with Overwrite conflict policy. // This creates the role if it doesn't exist, or replaces it if it does — without // touching other roles on the item (unlike the bulk PUT approach). @@ -2146,6 +2152,103 @@ private async Task SendFabricApiDeleteRequestAsync(string url, CancellationToken return null; } + /// + /// Resolves non-GUID objectId values (email/UPN) to Entra object IDs via Microsoft Graph. + /// Resolution order: /users/{x} → /groups?$filter=mail eq '{x}'. + /// + private async Task ResolvePrincipalsAsync(List members, CancellationToken cancellationToken) + { + var membersToResolve = members.Where(m => !string.IsNullOrWhiteSpace(m.ObjectId) && !Guid.TryParse(m.ObjectId, out _)).ToList(); + if (membersToResolve.Count == 0) + { + return; + } + + const string graphScope = "https://graph.microsoft.com/.default"; + var tokenContext = new TokenRequestContext(new[] { graphScope }); + var token = await _credential.GetTokenAsync(tokenContext, cancellationToken); + + var errors = new List(); + + foreach (var member in membersToResolve) + { + var principal = member.ObjectId!.Trim(); + var resolved = await TryResolveUserAsync(principal, token.Token, cancellationToken) + ?? await TryResolveGroupByMailAsync(principal, token.Token, cancellationToken); + + if (resolved == null) + { + errors.Add($"No Entra principal matched '{principal}'. Ensure the email/UPN is correct and you have User.Read.All and GroupMember.Read.All permissions."); + continue; + } + + member.ObjectId = resolved.Value.ObjectId; + member.ObjectType ??= resolved.Value.ObjectType; + } + + if (errors.Count > 0) + { + throw new ArgumentException("Failed to resolve one or more principals:\n" + string.Join("\n", errors)); + } + } + + private async Task<(string ObjectId, string ObjectType)?> TryResolveUserAsync(string principalValue, string accessToken, CancellationToken cancellationToken) + { + try + { + var url = $"https://graph.microsoft.com/v1.0/users/{Uri.EscapeDataString(principalValue)}?$select=id"; + using var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + using var response = await _httpClient.SendAsync(request, cancellationToken); + if (response.StatusCode == HttpStatusCode.NotFound) + { + return null; + } + + response.EnsureSuccessStatusCode(); + using var doc = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync(cancellationToken), cancellationToken: cancellationToken); + var id = doc.RootElement.GetProperty("id").GetString(); + return id != null ? (id, "User") : null; + } + catch (HttpRequestException) + { + return null; + } + } + + private async Task<(string ObjectId, string ObjectType)?> TryResolveGroupByMailAsync(string mail, string accessToken, CancellationToken cancellationToken) + { + try + { + var filter = Uri.EscapeDataString($"mail eq '{mail}'"); + var url = $"https://graph.microsoft.com/v1.0/groups?$filter={filter}&$select=id,displayName&$top=1"; + using var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + using var response = await _httpClient.SendAsync(request, cancellationToken); + if (response.StatusCode == HttpStatusCode.NotFound) + { + return null; + } + + response.EnsureSuccessStatusCode(); + using var doc = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync(cancellationToken), cancellationToken: cancellationToken); + var values = doc.RootElement.GetProperty("value"); + if (values.GetArrayLength() == 0) + { + return null; + } + + var id = values[0].GetProperty("id").GetString(); + return id != null ? (id, "Group") : null; + } + catch (HttpRequestException) + { + return null; + } + } + private static string ExtractWarehouseQueryValue(string warehousePrefix) { const string WarehousePrefixRoot = "warehouse/"; From 1da440babb1e709923e52984a6a43ab34dab9c9c Mon Sep 17 00:00:00 2001 From: Srinivas Thatipamula Date: Sat, 30 May 2026 09:16:24 -0700 Subject: [PATCH 09/15] Fix whitespace formatting in switch statement Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Services/OneLakeService.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs index 58f6e1fe4a..9c98b1ba12 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs @@ -2133,8 +2133,12 @@ private async Task SendFabricApiDeleteRequestAsync(string url, CancellationToken // Pad base64url to standard base64 switch (payload.Length % 4) { - case 2: payload += "=="; break; - case 3: payload += "="; break; + case 2: + payload += "=="; + break; + case 3: + payload += "="; + break; } var bytes = Convert.FromBase64String(payload.Replace('-', '+').Replace('_', '/')); From 786975a8e8ad8823a80961414737264272825fab Mon Sep 17 00:00:00 2001 From: Srinivas Thatipamula Date: Fri, 5 Jun 2026 11:44:17 -0700 Subject: [PATCH 10/15] Update role-definition option description with email/UPN resolution docs Added detailed documentation for the --role-definition option explaining that objectId accepts emails/UPNs (auto-resolved via Graph) in addition to GUIDs, with examples for both formats. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Options/FabricOptionDefinitions.cs | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs index 87352bf820..9e11c8c1ef 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs @@ -191,13 +191,30 @@ public static class FabricOptionDefinitions public static readonly Option RoleDefinition = new($"--{RoleDefinitionName}") { Description = """ - JSON definition of the data access role. Must include 'name', 'members' (with microsoftEntraMembers), - and 'decisionRules'. To scope access to a specific folder, include a Path attribute in decisionRules. - Example: {"name":"ImagesReadOnly","members":{"microsoftEntraMembers":[{"objectId":""}]}, - "decisionRules":[{"effect":"Permit","permission":[ - {"attributeName":"Action","attributeValueIncludedIn":["Read"]}, - {"attributeName":"Path","attributeValueIncludedIn":["Files/images/*"]}]}]}. - Omitting Path grants access to the entire item. + JSON definition of the data access role. Must include 'name', 'members' + (with microsoftEntraMembers), and 'decisionRules'. + members.microsoftEntraMembers[].objectId accepts EITHER an Entra object ID + (GUID) OR an email address / UPN — non-GUID values are automatically + resolved to object IDs via Microsoft Graph (tries /users first, then + /groups by mail, so mail-enabled groups and DLs work too). Do NOT call + Graph yourself to convert emails to GUIDs first; pass the email or UPN + directly. tenantId may be omitted — it is filled in during resolution. + To scope access to a specific folder, include a Path attribute in + decisionRules. Omitting Path grants access to the entire item. + Example with emails (preferred when you know the address, not the GUID): + {"name":"ImagesReadOnly", + "members":{"microsoftEntraMembers":[ + {"objectId":"alice@contoso.com"}, + {"objectId":"data-readers@contoso.com"}]}, + "decisionRules":[{"effect":"Permit","permission":[ + {"attributeName":"Action","attributeValueIncludedIn":["Read"]}, + {"attributeName":"Path","attributeValueIncludedIn":["Files/images/*"]}]}]} + Example with GUIDs (use when you already have the object ID): + {"name":"ImagesReadOnly", + "members":{"microsoftEntraMembers":[ + {"objectId":"514402e2-4238-4672-b021-ff9000307b66"}]}, + "decisionRules":[{"effect":"Permit","permission":[ + {"attributeName":"Action","attributeValueIncludedIn":["Read"]}]}]} """, Required = true }; From e1bb1cf244387208868782ffe2c7cb096b05b11e Mon Sep 17 00:00:00 2001 From: Srinivas Thatipamula Date: Fri, 5 Jun 2026 13:16:31 -0700 Subject: [PATCH 11/15] Fix OneLake settings models to match Fabric REST API contract The DiagnosticsSettings, ImmutabilityPolicy, and OneLakeSettings models were incorrect (appeared based on Azure Monitor rather than the actual Fabric OneLake Settings API). Fixed to match: - DiagnosticsSettings: status (string) + destination (Lakehouse ref) - ImmutabilityPolicy: scope + retentionDays (was state + immutabilityPeriodSinceCreationInDays) - OneLakeSettings: added immutabilityPolicies (array), lifecycle - Added DiagnosticsDestination, ItemReferenceById, LifecycleSettings - Removed unused DiagnosticsCategory, DiagnosticsModifyRequest, ImmutabilityPolicyModifyRequest - Updated option descriptions with correct JSON format examples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Models/OneLakeJsonContext.cs | 9 ++- .../src/Models/SettingsModels.cs | 73 +++++++++++-------- .../src/Options/FabricOptionDefinitions.cs | 17 ++++- 3 files changed, 62 insertions(+), 37 deletions(-) diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs index a2208749bf..eb06e03421 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs @@ -107,10 +107,11 @@ namespace Fabric.Mcp.Tools.OneLake.Models; // Settings types [JsonSerializable(typeof(OneLakeSettings))] [JsonSerializable(typeof(DiagnosticsSettings))] -[JsonSerializable(typeof(DiagnosticsCategory))] -[JsonSerializable(typeof(ImmutabilityPolicySettings))] -[JsonSerializable(typeof(DiagnosticsModifyRequest))] -[JsonSerializable(typeof(ImmutabilityPolicyModifyRequest))] +[JsonSerializable(typeof(DiagnosticsDestination))] +[JsonSerializable(typeof(ItemReferenceById))] +[JsonSerializable(typeof(ImmutabilityPolicy))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(LifecycleSettings))] // Long running operation types [JsonSerializable(typeof(OperationState))] [JsonSerializable(typeof(OperationError))] diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Models/SettingsModels.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Models/SettingsModels.cs index e006b5c1c7..4b26abee56 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Models/SettingsModels.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Models/SettingsModels.cs @@ -6,70 +6,81 @@ namespace Fabric.Mcp.Tools.OneLake.Models; /// -/// OneLake settings for a workspace. +/// OneLake settings response for a workspace (GET /workspaces/{id}/onelake/settings). /// public class OneLakeSettings { [JsonPropertyName("diagnostics")] public DiagnosticsSettings? Diagnostics { get; set; } - [JsonPropertyName("immutabilityPolicy")] - public ImmutabilityPolicySettings? ImmutabilityPolicy { get; set; } + [JsonPropertyName("immutabilityPolicies")] + public List? ImmutabilityPolicies { get; set; } + + [JsonPropertyName("lifecycle")] + public LifecycleSettings? Lifecycle { get; set; } } /// -/// Diagnostic logging configuration for OneLake. +/// OneLake diagnostic settings object. +/// Used in both the GET response (nested under "diagnostics") and the +/// POST /modifyDiagnostics request body. /// public class DiagnosticsSettings { - [JsonPropertyName("enabled")] - public bool? Enabled { get; set; } - - [JsonPropertyName("logAnalyticsWorkspaceId")] - public string? LogAnalyticsWorkspaceId { get; set; } + [JsonPropertyName("status")] + public string? Status { get; set; } - [JsonPropertyName("categories")] - public List? Categories { get; set; } + [JsonPropertyName("destination")] + public DiagnosticsDestination? Destination { get; set; } } /// -/// A diagnostic logging category. +/// Lakehouse destination for OneLake diagnostic logs. /// -public class DiagnosticsCategory +public class DiagnosticsDestination { - [JsonPropertyName("category")] - public string? Category { get; set; } + [JsonPropertyName("type")] + public string? Type { get; set; } - [JsonPropertyName("enabled")] - public bool? Enabled { get; set; } + [JsonPropertyName("lakehouse")] + public ItemReferenceById? Lakehouse { get; set; } } /// -/// Immutability policy settings for OneLake. +/// An item reference by ID object. /// -public class ImmutabilityPolicySettings +public class ItemReferenceById { - [JsonPropertyName("state")] - public string? State { get; set; } + [JsonPropertyName("referenceType")] + public string? ReferenceType { get; set; } - [JsonPropertyName("immutabilityPeriodSinceCreationInDays")] - public int? ImmutabilityPeriodSinceCreationInDays { get; set; } + [JsonPropertyName("itemId")] + public string? ItemId { get; set; } + + [JsonPropertyName("workspaceId")] + public string? WorkspaceId { get; set; } } /// -/// Request body for modifying diagnostics. +/// Immutability policy object (GET response and POST /modifyImmutabilityPolicy request). /// -public class DiagnosticsModifyRequest +public class ImmutabilityPolicy { - [JsonPropertyName("diagnostics")] - public DiagnosticsSettings? Diagnostics { get; set; } + [JsonPropertyName("scope")] + public string? Scope { get; set; } + + [JsonPropertyName("retentionDays")] + public int? RetentionDays { get; set; } } /// -/// Request body for modifying immutability policy. +/// Lifecycle management settings for a workspace. /// -public class ImmutabilityPolicyModifyRequest +public class LifecycleSettings { - [JsonPropertyName("immutabilityPolicy")] - public ImmutabilityPolicySettings? ImmutabilityPolicy { get; set; } + [JsonPropertyName("defaultTier")] + public string? DefaultTier { get; set; } + + [JsonPropertyName("policy")] + public string? Policy { get; set; } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs index 9e11c8c1ef..613fda55d5 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs @@ -259,14 +259,27 @@ Example with GUIDs (use when you already have the object ID): public const string DiagnosticsConfigName = "diagnostics-config"; public static readonly Option DiagnosticsConfig = new($"--{DiagnosticsConfigName}") { - Description = "JSON configuration for OneLake diagnostic logging.", + Description = """ + JSON request body for modifying OneLake diagnostics. Must include 'status' + ("Enabled" or "Disabled"). When enabling, include 'destination' with a + Lakehouse reference. When disabling, destination may be omitted. + Example (enable): {"status":"Enabled","destination":{"type":"Lakehouse", + "lakehouse":{"referenceType":"ById","itemId":"", + "workspaceId":""}}} + Example (disable): {"status":"Disabled"} + """, Required = true }; public const string ImmutabilityPolicyConfigName = "immutability-policy"; public static readonly Option ImmutabilityPolicyConfig = new($"--{ImmutabilityPolicyConfigName}") { - Description = "JSON configuration for OneLake immutability policy.", + Description = """ + JSON request body for modifying OneLake immutability policy. Must include + 'scope' (currently only "DiagnosticLogs") and 'retentionDays' (minimum 1). + Retention days cannot be reduced below the current value. + Example: {"scope":"DiagnosticLogs","retentionDays":30} + """, Required = true }; } From 7c32b13271ca6450ba63c7e1e0b4a6351cabc921 Mon Sep 17 00:00:00 2001 From: Srinivas Thatipamula Date: Fri, 5 Jun 2026 15:14:38 -0700 Subject: [PATCH 12/15] Flat-options refactor for OneLake settings, immutability, and data access roles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename models to match Fabric API swagger (§5): DiagnosticsSettings → OneLakeDiagnosticSettings, DiagnosticsDestination → LakehouseDiagnosticDestination, LakehouseItemReference → ItemReferenceById - Replace --diagnostics-config JSON blob with flat options: --status, --destination-lakehouse-workspace-id, --destination-lakehouse-item-id (§4.1) - Replace --immutability-policy JSON blob with flat options: --scope, --retention-days (§4.2) - Flatten data access role command (§4.3): add --entra-members, --fabric-item-members, --permitted-paths, --permitted-actions; keep --role-definition as optional advanced escape hatch - Service layer now accepts typed models instead of raw JSON strings - Move settings tests into correct test project directory - All 279 tests pass Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DataAccessRoleCreateOrUpdateCommand.cs | 153 ++++++++++++++++-- .../Settings/DiagnosticsModifyCommand.cs | 59 ++++++- .../ImmutabilityPolicyModifyCommand.cs | 28 +++- .../src/Models/OneLakeJsonContext.cs | 4 +- .../src/Models/SettingsModels.cs | 40 +++-- .../DataAccessRoleCreateOrUpdateOptions.cs | 5 + .../src/Options/DiagnosticsModifyOptions.cs | 4 +- .../src/Options/FabricOptionDefinitions.cs | 78 ++++++--- .../ImmutabilityPolicyModifyOptions.cs | 3 +- .../src/Services/IOneLakeService.cs | 4 +- .../src/Services/OneLakeService.cs | 28 +--- .../Settings/DiagnosticsModifyCommandTests.cs | 51 ++++-- .../ImmutabilityPolicyModifyCommandTests.cs | 26 +-- 13 files changed, 367 insertions(+), 116 deletions(-) rename tools/Fabric.Mcp.Tools.OneLake/tests/{ => Fabric.Mcp.Tools.OneLake.Tests}/Commands/Settings/DiagnosticsModifyCommandTests.cs (50%) rename tools/Fabric.Mcp.Tools.OneLake/tests/{ => Fabric.Mcp.Tools.OneLake.Tests}/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs (73%) diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs index 4ab65f3823..d7f005e587 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs @@ -16,17 +16,15 @@ namespace Fabric.Mcp.Tools.OneLake.Commands.Security; Name = "create_or_update_data_access_role", Title = "Create or Update OneLake Data Access Role", Description = """ - Upsert a single data access role on a single item. Scoped to one role on - one item per call — does not affect other roles on the item or any roles - on other items, so it is safe to call in a loop when multiple roles or - multiple items need changing. There is no bulk variant: the underlying - PUT-all API was intentionally not exposed because partial reads would - silently delete roles. Members can be specified by Entra object ID (GUID), - email address, or UPN — non-GUID values are automatically resolved via - Microsoft Graph (tries /users then /groups by mail). Caller must be a - workspace Admin or Member on the item's workspace. Requires - OneLake.ReadWrite.All and User.Read.All + GroupMember.Read.All for - principal resolution. + Upsert a single data access role on a single item. Use flat options (--name, + --entra-members, --permitted-paths, --permitted-actions) for the common case + of granting Read access. For advanced scenarios (multiple decision rules, + column/row constraints), pass the full JSON via --role-definition instead. + When flat options are provided, --role-definition is ignored. + Members can be specified by Entra object ID (GUID), email address, or UPN — + non-GUID values are automatically resolved via Microsoft Graph. + Caller must be a workspace Admin or Member. Requires OneLake.ReadWrite.All and + User.Read.All + GroupMember.Read.All for principal resolution. """, Destructive = false, Idempotent = true, @@ -47,7 +45,12 @@ protected override void RegisterOptions(Command command) command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); - command.Options.Add(FabricOptionDefinitions.RoleDefinition.AsRequired()); + command.Options.Add(FabricOptionDefinitions.RoleName.AsOptional()); + command.Options.Add(FabricOptionDefinitions.EntraMembers.AsOptional()); + command.Options.Add(FabricOptionDefinitions.FabricItemMembers.AsOptional()); + command.Options.Add(FabricOptionDefinitions.PermittedPaths.AsOptional()); + command.Options.Add(FabricOptionDefinitions.PermittedActions.AsOptional()); + command.Options.Add(FabricOptionDefinitions.RoleDefinition.AsOptional()); command.Validators.Add(result => { var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); @@ -62,6 +65,48 @@ protected override void RegisterOptions(Command command) { result.AddError("Workspace must be a valid GUID. Name-based resolution is not supported for this command."); } + + var roleName = result.GetValueOrDefault(FabricOptionDefinitions.RoleName.Name); + var entraMembers = result.GetValueOrDefault(FabricOptionDefinitions.EntraMembers.Name); + var fabricItemMembers = result.GetValueOrDefault(FabricOptionDefinitions.FabricItemMembers.Name); + var roleDefinition = result.GetValueOrDefault(FabricOptionDefinitions.RoleDefinition.Name); + + var hasFlat = !string.IsNullOrWhiteSpace(roleName) || + !string.IsNullOrWhiteSpace(entraMembers) || + !string.IsNullOrWhiteSpace(fabricItemMembers); + + if (!hasFlat && string.IsNullOrWhiteSpace(roleDefinition)) + { + result.AddError("Provide either flat options (--role-name + --entra-members/--fabric-item-members) or --role-definition."); + } + + if (hasFlat) + { + if (string.IsNullOrWhiteSpace(roleName)) + result.AddError("--role-name is required when using flat options."); + + if (string.IsNullOrWhiteSpace(entraMembers) && string.IsNullOrWhiteSpace(fabricItemMembers)) + result.AddError("At least one of --entra-members or --fabric-item-members is required."); + + if (!string.IsNullOrWhiteSpace(entraMembers)) + { + foreach (var member in entraMembers.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + if (!Guid.TryParse(member, out _) && !member.Contains('@')) + result.AddError($"Invalid --entra-members value '{member}'. Must be a GUID, email, or UPN."); + } + } + + var permittedActions = result.GetValueOrDefault(FabricOptionDefinitions.PermittedActions.Name); + if (!string.IsNullOrWhiteSpace(permittedActions)) + { + foreach (var action in permittedActions.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + if (!string.Equals(action, "Read", StringComparison.OrdinalIgnoreCase)) + result.AddError($"Unsupported --permitted-actions value '{action}'. Only 'Read' is currently supported."); + } + } + } }); } @@ -72,6 +117,11 @@ protected override DataAccessRoleCreateOrUpdateOptions BindOptions(ParseResult p var workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); options.WorkspaceId = !string.IsNullOrWhiteSpace(workspaceId) ? workspaceId! : workspace ?? string.Empty; options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Name = parseResult.GetValueOrDefault(FabricOptionDefinitions.RoleName.Name); + options.EntraMembers = parseResult.GetValueOrDefault(FabricOptionDefinitions.EntraMembers.Name); + options.FabricItemMembers = parseResult.GetValueOrDefault(FabricOptionDefinitions.FabricItemMembers.Name); + options.PermittedPaths = parseResult.GetValueOrDefault(FabricOptionDefinitions.PermittedPaths.Name); + options.PermittedActions = parseResult.GetValueOrDefault(FabricOptionDefinitions.PermittedActions.Name); options.RoleDefinition = parseResult.GetValueOrDefault(FabricOptionDefinitions.RoleDefinition.Name); return options; } @@ -86,9 +136,19 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); try { + DataAccessRole result; + if (!string.IsNullOrWhiteSpace(options.Name)) + { + // Build role from flat options + var roleDefinitionJson = BuildRoleDefinitionJson(options); + result = await _oneLakeService.CreateOrUpdateDataAccessRoleAsync(options.WorkspaceId!, options.ItemId!, roleDefinitionJson, cancellationToken); + } + else + { + // Use raw JSON escape hatch + result = await _oneLakeService.CreateOrUpdateDataAccessRoleAsync(options.WorkspaceId!, options.ItemId!, options.RoleDefinition!, cancellationToken); + } - - var result = await _oneLakeService.CreateOrUpdateDataAccessRoleAsync(options.WorkspaceId!, options.ItemId!, options.RoleDefinition!, cancellationToken); context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.DataAccessRole); } catch (Exception ex) @@ -100,5 +160,70 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + private static string BuildRoleDefinitionJson(DataAccessRoleCreateOrUpdateOptions options) + { + var members = new DataAccessRoleMembers(); + + if (!string.IsNullOrWhiteSpace(options.EntraMembers)) + { + members.MicrosoftEntraMembers = options.EntraMembers + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(m => new MicrosoftEntraMember { ObjectId = m }) + .ToList(); + } + + if (!string.IsNullOrWhiteSpace(options.FabricItemMembers)) + { + members.FabricItemMembers = options.FabricItemMembers + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(m => + { + var parts = m.Split(':', 2); + return new FabricItemMember + { + SourcePath = parts[0], + ItemAccess = parts.Length > 1 ? [parts[1]] : ["Read"] + }; + }) + .ToList(); + } + + var actions = "Read"; + if (!string.IsNullOrWhiteSpace(options.PermittedActions)) + { + actions = options.PermittedActions; + } + + var permissions = new List + { + new() { AttributeName = "Action", AttributeValueIncludedIn = actions.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList() } + }; + + if (!string.IsNullOrWhiteSpace(options.PermittedPaths)) + { + permissions.Add(new DecisionRuleScope + { + AttributeName = "Path", + AttributeValueIncludedIn = options.PermittedPaths.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList() + }); + } + + var role = new DataAccessRole + { + Name = options.Name!, + Members = members, + DecisionRules = + [ + new DecisionRule + { + Effect = "Permit", + Permission = permissions + } + ] + }; + + return System.Text.Json.JsonSerializer.Serialize(role, OneLakeJsonContext.Default.DataAccessRole); + } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs index 566742a307..dbd4dfce10 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/DiagnosticsModifyCommand.cs @@ -16,10 +16,11 @@ namespace Fabric.Mcp.Tools.OneLake.Commands.Settings; Name = "modify_diagnostics", Title = "Modify OneLake Diagnostics", Description = """ - Modify the diagnostic logging configuration for OneLake at the workspace - scope. Replaces the existing diagnostics block; fetch with - onelake_get_settings first if you want to merge. Requires - OneLake.ReadWrite.All. + Enable or disable workspace-level OneLake diagnostic logging. When enabling, + specify the destination lakehouse where logs will be stored. When disabling, + destination options must be omitted. This is an LRO — the server may return + 202 Accepted. Requires OneLake.ReadWrite.All. Caller must be a workspace Admin + on the source workspace and Contributor+ on the destination workspace. """, Destructive = false, Idempotent = true, @@ -39,7 +40,9 @@ protected override void RegisterOptions(Command command) base.RegisterOptions(command); command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); - command.Options.Add(FabricOptionDefinitions.DiagnosticsConfig.AsRequired()); + command.Options.Add(FabricOptionDefinitions.DiagnosticsStatus.AsRequired()); + command.Options.Add(FabricOptionDefinitions.DestinationLakehouseWorkspaceId.AsOptional()); + command.Options.Add(FabricOptionDefinitions.DestinationLakehouseItemId.AsOptional()); command.Validators.Add(result => { var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); @@ -54,6 +57,34 @@ protected override void RegisterOptions(Command command) { result.AddError("Workspace must be a valid GUID. Name-based resolution is not supported for this command."); } + + var status = result.GetValueOrDefault(FabricOptionDefinitions.DiagnosticsStatus.Name); + if (!string.Equals(status, "Enabled", StringComparison.OrdinalIgnoreCase) && + !string.Equals(status, "Disabled", StringComparison.OrdinalIgnoreCase)) + { + result.AddError("--status must be 'Enabled' or 'Disabled'."); + } + + var destWorkspaceId = result.GetValueOrDefault(FabricOptionDefinitions.DestinationLakehouseWorkspaceId.Name); + var destItemId = result.GetValueOrDefault(FabricOptionDefinitions.DestinationLakehouseItemId.Name); + + if (string.Equals(status, "Enabled", StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrWhiteSpace(destWorkspaceId)) + result.AddError("--destination-lakehouse-workspace-id is required when --status is Enabled."); + else if (!Guid.TryParse(destWorkspaceId, out _)) + result.AddError("--destination-lakehouse-workspace-id must be a valid GUID."); + + if (string.IsNullOrWhiteSpace(destItemId)) + result.AddError("--destination-lakehouse-item-id is required when --status is Enabled."); + else if (!Guid.TryParse(destItemId, out _)) + result.AddError("--destination-lakehouse-item-id must be a valid GUID."); + } + else if (string.Equals(status, "Disabled", StringComparison.OrdinalIgnoreCase)) + { + if (!string.IsNullOrWhiteSpace(destWorkspaceId) || !string.IsNullOrWhiteSpace(destItemId)) + result.AddError("Destination options must be omitted when --status is Disabled."); + } }); } @@ -63,7 +94,9 @@ protected override DiagnosticsModifyOptions BindOptions(ParseResult parseResult) var workspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); var workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); options.WorkspaceId = !string.IsNullOrWhiteSpace(workspaceId) ? workspaceId! : workspace ?? string.Empty; - options.DiagnosticsConfig = parseResult.GetValueOrDefault(FabricOptionDefinitions.DiagnosticsConfig.Name); + options.Status = parseResult.GetValueOrDefault(FabricOptionDefinitions.DiagnosticsStatus.Name); + options.DestinationLakehouseWorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.DestinationLakehouseWorkspaceId.Name); + options.DestinationLakehouseItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.DestinationLakehouseItemId.Name); return options; } @@ -77,8 +110,20 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); try { + var settings = new OneLakeDiagnosticSettings { Status = options.Status }; + if (string.Equals(options.Status, "Enabled", StringComparison.OrdinalIgnoreCase)) + { + settings.Destination = new LakehouseDiagnosticDestination + { + Lakehouse = new ItemReferenceById + { + ItemId = options.DestinationLakehouseItemId, + WorkspaceId = options.DestinationLakehouseWorkspaceId + } + }; + } - await _oneLakeService.ModifyDiagnosticsAsync(options.WorkspaceId!, options.DiagnosticsConfig!, cancellationToken); + await _oneLakeService.ModifyDiagnosticsAsync(options.WorkspaceId!, settings, cancellationToken); context.Response.Results = ResponseResult.Create(new("Diagnostics settings modified successfully."), OneLakeJsonContext.Default.DiagnosticsModifyCommandResult); } catch (Exception ex) diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs index a2d4cbd623..cf5b30e34f 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/ImmutabilityPolicyModifyCommand.cs @@ -18,7 +18,8 @@ namespace Fabric.Mcp.Tools.OneLake.Commands.Settings; Description = """ Modify the workspace-level OneLake immutability policy. Once enabled, immutability cannot be disabled — confirm with the user before applying. - Requires OneLake.ReadWrite.All. + Retention days cannot be reduced below the current value. Requires + OneLake.ReadWrite.All. Caller must be a workspace Admin. """, Destructive = false, Idempotent = true, @@ -38,7 +39,8 @@ protected override void RegisterOptions(Command command) base.RegisterOptions(command); command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsOptional()); command.Options.Add(FabricOptionDefinitions.Workspace.AsOptional()); - command.Options.Add(FabricOptionDefinitions.ImmutabilityPolicyConfig.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ImmutabilityScope.AsRequired()); + command.Options.Add(FabricOptionDefinitions.RetentionDays.AsRequired()); command.Validators.Add(result => { var workspaceId = result.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); @@ -53,6 +55,18 @@ protected override void RegisterOptions(Command command) { result.AddError("Workspace must be a valid GUID. Name-based resolution is not supported for this command."); } + + var scope = result.GetValueOrDefault(FabricOptionDefinitions.ImmutabilityScope.Name); + if (!string.Equals(scope, "DiagnosticLogs", StringComparison.OrdinalIgnoreCase)) + { + result.AddError("--scope must be 'DiagnosticLogs'. No other scopes are currently supported."); + } + + var retentionDays = result.GetValueOrDefault(FabricOptionDefinitions.RetentionDays.Name); + if (retentionDays < 1) + { + result.AddError("--retention-days must be at least 1."); + } }); } @@ -62,7 +76,8 @@ protected override ImmutabilityPolicyModifyOptions BindOptions(ParseResult parse var workspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); var workspace = parseResult.GetValueOrDefault(FabricOptionDefinitions.Workspace.Name); options.WorkspaceId = !string.IsNullOrWhiteSpace(workspaceId) ? workspaceId! : workspace ?? string.Empty; - options.ImmutabilityPolicyConfig = parseResult.GetValueOrDefault(FabricOptionDefinitions.ImmutabilityPolicyConfig.Name); + options.Scope = parseResult.GetValueOrDefault(FabricOptionDefinitions.ImmutabilityScope.Name); + options.RetentionDays = parseResult.GetValueOrDefault(FabricOptionDefinitions.RetentionDays.Name); return options; } @@ -76,8 +91,13 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); try { + var policy = new ImmutabilityPolicy + { + Scope = options.Scope, + RetentionDays = options.RetentionDays + }; - await _oneLakeService.ModifyImmutabilityPolicyAsync(options.WorkspaceId!, options.ImmutabilityPolicyConfig!, cancellationToken); + await _oneLakeService.ModifyImmutabilityPolicyAsync(options.WorkspaceId!, policy, cancellationToken); context.Response.Results = ResponseResult.Create(new("Immutability policy modified successfully."), OneLakeJsonContext.Default.ImmutabilityPolicyModifyCommandResult); } catch (Exception ex) diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs index eb06e03421..9675ea0714 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs @@ -106,8 +106,8 @@ namespace Fabric.Mcp.Tools.OneLake.Models; [JsonSerializable(typeof(ImmutabilityPolicyModifyCommand.ImmutabilityPolicyModifyCommandResult))] // Settings types [JsonSerializable(typeof(OneLakeSettings))] -[JsonSerializable(typeof(DiagnosticsSettings))] -[JsonSerializable(typeof(DiagnosticsDestination))] +[JsonSerializable(typeof(OneLakeDiagnosticSettings))] +[JsonSerializable(typeof(LakehouseDiagnosticDestination))] [JsonSerializable(typeof(ItemReferenceById))] [JsonSerializable(typeof(ImmutabilityPolicy))] [JsonSerializable(typeof(List))] diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Models/SettingsModels.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Models/SettingsModels.cs index 4b26abee56..81c9efe5f3 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Models/SettingsModels.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Models/SettingsModels.cs @@ -5,14 +5,13 @@ namespace Fabric.Mcp.Tools.OneLake.Models; -/// -/// OneLake settings response for a workspace (GET /workspaces/{id}/onelake/settings). -/// +/// GET /workspaces/{id}/onelake/settings response (swagger: GetOneLakeSettingsResponse). public class OneLakeSettings { [JsonPropertyName("diagnostics")] - public DiagnosticsSettings? Diagnostics { get; set; } + public OneLakeDiagnosticSettings? Diagnostics { get; set; } + /// Swagger field is plural "immutabilityPolicies" and is an array. [JsonPropertyName("immutabilityPolicies")] public List? ImmutabilityPolicies { get; set; } @@ -21,38 +20,38 @@ public class OneLakeSettings } /// -/// OneLake diagnostic settings object. -/// Used in both the GET response (nested under "diagnostics") and the -/// POST /modifyDiagnostics request body. +/// Body of POST /onelake/settings/modifyDiagnostics AND the diagnostics block in GET. +/// Swagger: OneLakeDiagnosticSettings. /// -public class DiagnosticsSettings +public class OneLakeDiagnosticSettings { [JsonPropertyName("status")] public string? Status { get; set; } + /// Required when Status == Enabled; omitted when Disabled. [JsonPropertyName("destination")] - public DiagnosticsDestination? Destination { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public LakehouseDiagnosticDestination? Destination { get; set; } } /// -/// Lakehouse destination for OneLake diagnostic logs. +/// Swagger: LakehouseOneLakeDiagnosticSettingsDestination (discriminator type="Lakehouse"). +/// Single-variant today — promote to polymorphic when Fabric adds another destination type. /// -public class DiagnosticsDestination +public sealed class LakehouseDiagnosticDestination { [JsonPropertyName("type")] - public string? Type { get; set; } + public string Type { get; set; } = "Lakehouse"; [JsonPropertyName("lakehouse")] public ItemReferenceById? Lakehouse { get; set; } } -/// -/// An item reference by ID object. -/// -public class ItemReferenceById +/// Swagger: ItemReferenceById (discriminator referenceType="ById"). +public sealed class ItemReferenceById { [JsonPropertyName("referenceType")] - public string? ReferenceType { get; set; } + public string ReferenceType { get; set; } = "ById"; [JsonPropertyName("itemId")] public string? ItemId { get; set; } @@ -62,7 +61,8 @@ public class ItemReferenceById } /// -/// Immutability policy object (GET response and POST /modifyImmutabilityPolicy request). +/// Body of POST /onelake/settings/modifyImmutabilityPolicy AND items in GET response. +/// Swagger: ImmutabilityPolicyRequest / ImmutabilityPolicy. /// public class ImmutabilityPolicy { @@ -73,9 +73,7 @@ public class ImmutabilityPolicy public int? RetentionDays { get; set; } } -/// -/// Lifecycle management settings for a workspace. -/// +/// Lifecycle management settings for a workspace. public class LifecycleSettings { [JsonPropertyName("defaultTier")] diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleCreateOrUpdateOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleCreateOrUpdateOptions.cs index 6beb75725a..284c5e340c 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleCreateOrUpdateOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DataAccessRoleCreateOrUpdateOptions.cs @@ -10,5 +10,10 @@ public sealed class DataAccessRoleCreateOrUpdateOptions : GlobalOptions public string? WorkspaceId { get; set; } public string? ItemId { get; set; } public string? RoleDefinition { get; set; } + public string? Name { get; set; } + public string? EntraMembers { get; set; } + public string? FabricItemMembers { get; set; } + public string? PermittedPaths { get; set; } + public string? PermittedActions { get; set; } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DiagnosticsModifyOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DiagnosticsModifyOptions.cs index c3d9cf62e0..b306e6383a 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/DiagnosticsModifyOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/DiagnosticsModifyOptions.cs @@ -8,6 +8,8 @@ namespace Fabric.Mcp.Tools.OneLake.Options; public sealed class DiagnosticsModifyOptions : GlobalOptions { public string? WorkspaceId { get; set; } - public string? DiagnosticsConfig { get; set; } + public string? Status { get; set; } + public string? DestinationLakehouseWorkspaceId { get; set; } + public string? DestinationLakehouseItemId { get; set; } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs index 613fda55d5..6a106d1e51 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs @@ -216,7 +216,35 @@ Example with GUIDs (use when you already have the object ID): "decisionRules":[{"effect":"Permit","permission":[ {"attributeName":"Action","attributeValueIncludedIn":["Read"]}]}]} """, - Required = true + Required = false + }; + + public const string EntraMembersName = "entra-members"; + public static readonly Option EntraMembers = new($"--{EntraMembersName}") + { + Description = "Comma-separated Entra member identifiers (object IDs, emails, or UPNs). Non-GUID values are resolved via Microsoft Graph.", + Required = false + }; + + public const string FabricItemMembersName = "fabric-item-members"; + public static readonly Option FabricItemMembers = new($"--{FabricItemMembersName}") + { + Description = "Comma-separated Fabric item member references in format 'itemId:permission' (e.g. 'dfbe1234-...:Read').", + Required = false + }; + + public const string PermittedPathsName = "permitted-paths"; + public static readonly Option PermittedPaths = new($"--{PermittedPathsName}") + { + Description = "Comma-separated paths to grant access to (e.g. 'Files/images/*,Tables/sales'). Omit to grant access to the entire item.", + Required = false + }; + + public const string PermittedActionsName = "permitted-actions"; + public static readonly Option PermittedActions = new($"--{PermittedActionsName}") + { + Description = "Comma-separated actions to permit. Currently only 'Read' is supported. Defaults to 'Read' if omitted.", + Required = false }; // Shortcut options @@ -256,30 +284,40 @@ Example with GUIDs (use when you already have the object ID): }; // Settings options - public const string DiagnosticsConfigName = "diagnostics-config"; - public static readonly Option DiagnosticsConfig = new($"--{DiagnosticsConfigName}") + // Diagnostics flat options + public const string DiagnosticsStatusName = "status"; + public static readonly Option DiagnosticsStatus = new($"--{DiagnosticsStatusName}") { - Description = """ - JSON request body for modifying OneLake diagnostics. Must include 'status' - ("Enabled" or "Disabled"). When enabling, include 'destination' with a - Lakehouse reference. When disabling, destination may be omitted. - Example (enable): {"status":"Enabled","destination":{"type":"Lakehouse", - "lakehouse":{"referenceType":"ById","itemId":"", - "workspaceId":""}}} - Example (disable): {"status":"Disabled"} - """, + Description = "The status of diagnostics: Enabled or Disabled.", Required = true }; - public const string ImmutabilityPolicyConfigName = "immutability-policy"; - public static readonly Option ImmutabilityPolicyConfig = new($"--{ImmutabilityPolicyConfigName}") + public const string DestinationLakehouseWorkspaceIdName = "destination-lakehouse-workspace-id"; + public static readonly Option DestinationLakehouseWorkspaceId = new($"--{DestinationLakehouseWorkspaceIdName}") { - Description = """ - JSON request body for modifying OneLake immutability policy. Must include - 'scope' (currently only "DiagnosticLogs") and 'retentionDays' (minimum 1). - Retention days cannot be reduced below the current value. - Example: {"scope":"DiagnosticLogs","retentionDays":30} - """, + Description = "The workspace ID (GUID) of the destination lakehouse for diagnostic logs. Required when --status is Enabled.", + Required = false + }; + + public const string DestinationLakehouseItemIdName = "destination-lakehouse-item-id"; + public static readonly Option DestinationLakehouseItemId = new($"--{DestinationLakehouseItemIdName}") + { + Description = "The item ID (GUID) of the destination lakehouse for diagnostic logs. Required when --status is Enabled.", + Required = false + }; + + // Immutability policy flat options + public const string ImmutabilityScopeName = "scope"; + public static readonly Option ImmutabilityScope = new($"--{ImmutabilityScopeName}") + { + Description = "The scope of the immutability policy. Currently only 'DiagnosticLogs' is supported.", + Required = true + }; + + public const string RetentionDaysName = "retention-days"; + public static readonly Option RetentionDays = new($"--{RetentionDaysName}") + { + Description = "Number of days to retain diagnostic logs (minimum 1). Cannot be reduced below the current value.", Required = true }; } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ImmutabilityPolicyModifyOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ImmutabilityPolicyModifyOptions.cs index c9b03438ff..709ba45bed 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ImmutabilityPolicyModifyOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ImmutabilityPolicyModifyOptions.cs @@ -8,6 +8,7 @@ namespace Fabric.Mcp.Tools.OneLake.Options; public sealed class ImmutabilityPolicyModifyOptions : GlobalOptions { public string? WorkspaceId { get; set; } - public string? ImmutabilityPolicyConfig { get; set; } + public string? Scope { get; set; } + public int? RetentionDays { get; set; } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs index 5dedeec982..263c76c0a7 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs @@ -62,6 +62,6 @@ public interface IOneLakeService // Settings Operations Task GetSettingsAsync(string workspaceId, CancellationToken cancellationToken = default); - Task ModifyDiagnosticsAsync(string workspaceId, string diagnosticsConfigJson, CancellationToken cancellationToken = default); - Task ModifyImmutabilityPolicyAsync(string workspaceId, string immutabilityPolicyJson, CancellationToken cancellationToken = default); + Task ModifyDiagnosticsAsync(string workspaceId, OneLakeDiagnosticSettings settings, CancellationToken cancellationToken = default); + Task ModifyImmutabilityPolicyAsync(string workspaceId, ImmutabilityPolicy policy, CancellationToken cancellationToken = default); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs index 9c98b1ba12..1b7b5c16b4 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs @@ -2072,34 +2072,18 @@ public async Task GetSettingsAsync(string workspaceId, Cancella return await JsonSerializer.DeserializeAsync(response, OneLakeJsonContext.Default.OneLakeSettings, cancellationToken) ?? new OneLakeSettings(); } - public async Task ModifyDiagnosticsAsync(string workspaceId, string diagnosticsConfigJson, CancellationToken cancellationToken = default) + public async Task ModifyDiagnosticsAsync(string workspaceId, OneLakeDiagnosticSettings settings, CancellationToken cancellationToken = default) { - try - { - using var doc = JsonDocument.Parse(diagnosticsConfigJson); - } - catch (JsonException ex) - { - throw new ArgumentException($"Invalid diagnostics configuration JSON: {ex.Message}", nameof(diagnosticsConfigJson), ex); - } - var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/onelake/settings/modifyDiagnostics"; - await SendFabricApiRequestAsync(HttpMethod.Post, url, diagnosticsConfigJson, cancellationToken: cancellationToken); + var jsonContent = JsonSerializer.Serialize(settings, OneLakeJsonContext.Default.OneLakeDiagnosticSettings); + await SendFabricApiRequestAsync(HttpMethod.Post, url, jsonContent, cancellationToken: cancellationToken); } - public async Task ModifyImmutabilityPolicyAsync(string workspaceId, string immutabilityPolicyJson, CancellationToken cancellationToken = default) + public async Task ModifyImmutabilityPolicyAsync(string workspaceId, ImmutabilityPolicy policy, CancellationToken cancellationToken = default) { - try - { - using var doc = JsonDocument.Parse(immutabilityPolicyJson); - } - catch (JsonException ex) - { - throw new ArgumentException($"Invalid immutability policy JSON: {ex.Message}", nameof(immutabilityPolicyJson), ex); - } - var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/onelake/settings/modifyImmutabilityPolicy"; - await SendFabricApiRequestAsync(HttpMethod.Post, url, immutabilityPolicyJson, cancellationToken: cancellationToken); + var jsonContent = JsonSerializer.Serialize(policy, OneLakeJsonContext.Default.ImmutabilityPolicy); + await SendFabricApiRequestAsync(HttpMethod.Post, url, jsonContent, cancellationToken: cancellationToken); } private async Task SendFabricApiDeleteRequestAsync(string url, CancellationToken cancellationToken) diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/DiagnosticsModifyCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Settings/DiagnosticsModifyCommandTests.cs similarity index 50% rename from tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/DiagnosticsModifyCommandTests.cs rename to tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Settings/DiagnosticsModifyCommandTests.cs index 9ad9f16b85..6f977a9559 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/DiagnosticsModifyCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Settings/DiagnosticsModifyCommandTests.cs @@ -18,7 +18,7 @@ public void Constructor_InitializesCommandCorrectly() { Assert.Equal("modify_diagnostics", Command.Name); Assert.Equal("Modify OneLake Diagnostics", Command.Title); - Assert.Contains("Modify the diagnostic logging configuration", Command.Description); + Assert.Contains("Enable or disable workspace-level OneLake diagnostic logging", Command.Description); Assert.False(Command.Metadata.ReadOnly); Assert.False(Command.Metadata.Destructive); Assert.True(Command.Metadata.Idempotent); @@ -58,13 +58,16 @@ public void Metadata_HasCorrectProperties() } [Theory] - [InlineData("--workspace-id ws1 --diagnostics-config {\"status\":\"Disabled\"}", true)] - [InlineData("--diagnostics-config {\"status\":\"Disabled\"}", false)] + [InlineData("--workspace-id 85173301-af01-49c9-b667-03edc44517da --status Disabled", true)] + [InlineData("--workspace-id 85173301-af01-49c9-b667-03edc44517da --status Enabled --destination-lakehouse-workspace-id 85173301-af01-49c9-b667-03edc44517da --destination-lakehouse-item-id eceb53c6-6227-41f1-a649-62ebe7ee9eb1", true)] + [InlineData("--status Disabled", false)] // missing workspace + [InlineData("--workspace-id 85173301-af01-49c9-b667-03edc44517da --status Enabled", false)] // missing destination when enabled + [InlineData("--workspace-id 85173301-af01-49c9-b667-03edc44517da --status Disabled --destination-lakehouse-workspace-id 85173301-af01-49c9-b667-03edc44517da --destination-lakehouse-item-id eceb53c6-6227-41f1-a649-62ebe7ee9eb1", false)] // destination provided when disabled public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) { if (shouldSucceed) { - Service.ModifyDiagnosticsAsync(Arg.Any(), Arg.Any(), Arg.Any()) + Service.ModifyDiagnosticsAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.CompletedTask); } @@ -78,29 +81,53 @@ public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldS } [Fact] - public async Task ExecuteAsync_SuccessfulModification_ReturnsSuccessMessage() + public async Task ExecuteAsync_DisablesDiagnostics_ReturnsSuccessMessage() { - Service.ModifyDiagnosticsAsync("ws1", Arg.Any(), Arg.Any()) + Service.ModifyDiagnosticsAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.CompletedTask); var response = await ExecuteCommandAsync( - "--workspace-id", "ws1", - "--diagnostics-config", "{\"status\":\"Disabled\"}"); + "--workspace-id", "85173301-af01-49c9-b667-03edc44517da", + "--status", "Disabled"); var result = ValidateAndDeserializeResponse(response, OneLakeJsonContext.Default.DiagnosticsModifyCommandResult); Assert.Contains("successfully", result.Message, StringComparison.OrdinalIgnoreCase); - await Service.Received(1).ModifyDiagnosticsAsync("ws1", Arg.Any(), Arg.Any()); + await Service.Received(1).ModifyDiagnosticsAsync("85173301-af01-49c9-b667-03edc44517da", Arg.Is(s => s.Status == "Disabled" && s.Destination == null), Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_EnablesDiagnostics_BuildsCorrectModel() + { + Service.ModifyDiagnosticsAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + + var response = await ExecuteCommandAsync( + "--workspace-id", "85173301-af01-49c9-b667-03edc44517da", + "--status", "Enabled", + "--destination-lakehouse-workspace-id", "85173301-af01-49c9-b667-03edc44517da", + "--destination-lakehouse-item-id", "eceb53c6-6227-41f1-a649-62ebe7ee9eb1"); + + var result = ValidateAndDeserializeResponse(response, OneLakeJsonContext.Default.DiagnosticsModifyCommandResult); + Assert.Contains("successfully", result.Message, StringComparison.OrdinalIgnoreCase); + await Service.Received(1).ModifyDiagnosticsAsync("85173301-af01-49c9-b667-03edc44517da", + Arg.Is(s => + s.Status == "Enabled" && + s.Destination != null && + s.Destination.Lakehouse != null && + s.Destination.Lakehouse.WorkspaceId == "85173301-af01-49c9-b667-03edc44517da" && + s.Destination.Lakehouse.ItemId == "eceb53c6-6227-41f1-a649-62ebe7ee9eb1"), + Arg.Any()); } [Fact] public async Task ExecuteAsync_HandlesServiceErrors() { - Service.ModifyDiagnosticsAsync(Arg.Any(), Arg.Any(), Arg.Any()) + Service.ModifyDiagnosticsAsync(Arg.Any(), Arg.Any(), Arg.Any()) .ThrowsAsync(new HttpRequestException("Forbidden")); var response = await ExecuteCommandAsync( - "--workspace-id", "ws1", - "--diagnostics-config", "{\"status\":\"Disabled\"}"); + "--workspace-id", "85173301-af01-49c9-b667-03edc44517da", + "--status", "Disabled"); Assert.NotNull(response); Assert.NotEqual(HttpStatusCode.OK, response.Status); diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs similarity index 73% rename from tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs rename to tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs index 4a1b72d28a..58cc467b6a 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Settings/ImmutabilityPolicyModifyCommandTests.cs @@ -58,13 +58,15 @@ public void Metadata_HasCorrectProperties() } [Theory] - [InlineData("--workspace-id ws1 --immutability-policy {\"scope\":\"DiagnosticLogs\",\"retentionDays\":7}", true)] - [InlineData("--immutability-policy {\"scope\":\"DiagnosticLogs\",\"retentionDays\":7}", false)] + [InlineData("--workspace-id 85173301-af01-49c9-b667-03edc44517da --scope DiagnosticLogs --retention-days 7", true)] + [InlineData("--scope DiagnosticLogs --retention-days 7", false)] // missing workspace + [InlineData("--workspace-id 85173301-af01-49c9-b667-03edc44517da --scope DiagnosticLogs --retention-days 0", false)] // retention < 1 + [InlineData("--workspace-id 85173301-af01-49c9-b667-03edc44517da --scope Invalid --retention-days 7", false)] // bad scope public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) { if (shouldSucceed) { - Service.ModifyImmutabilityPolicyAsync(Arg.Any(), Arg.Any(), Arg.Any()) + Service.ModifyImmutabilityPolicyAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.CompletedTask); } @@ -80,27 +82,31 @@ public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldS [Fact] public async Task ExecuteAsync_SuccessfulModification_ReturnsSuccessMessage() { - Service.ModifyImmutabilityPolicyAsync("ws1", Arg.Any(), Arg.Any()) + Service.ModifyImmutabilityPolicyAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.CompletedTask); var response = await ExecuteCommandAsync( - "--workspace-id", "ws1", - "--immutability-policy", "{\"scope\":\"DiagnosticLogs\",\"retentionDays\":7}"); + "--workspace-id", "85173301-af01-49c9-b667-03edc44517da", + "--scope", "DiagnosticLogs", + "--retention-days", "7"); var result = ValidateAndDeserializeResponse(response, OneLakeJsonContext.Default.ImmutabilityPolicyModifyCommandResult); Assert.Contains("successfully", result.Message, StringComparison.OrdinalIgnoreCase); - await Service.Received(1).ModifyImmutabilityPolicyAsync("ws1", Arg.Any(), Arg.Any()); + await Service.Received(1).ModifyImmutabilityPolicyAsync("85173301-af01-49c9-b667-03edc44517da", + Arg.Is(p => p.Scope == "DiagnosticLogs" && p.RetentionDays == 7), + Arg.Any()); } [Fact] public async Task ExecuteAsync_HandlesServiceErrors() { - Service.ModifyImmutabilityPolicyAsync(Arg.Any(), Arg.Any(), Arg.Any()) + Service.ModifyImmutabilityPolicyAsync(Arg.Any(), Arg.Any(), Arg.Any()) .ThrowsAsync(new HttpRequestException("Forbidden")); var response = await ExecuteCommandAsync( - "--workspace-id", "ws1", - "--immutability-policy", "{\"scope\":\"DiagnosticLogs\",\"retentionDays\":7}"); + "--workspace-id", "85173301-af01-49c9-b667-03edc44517da", + "--scope", "DiagnosticLogs", + "--retention-days", "7"); Assert.NotNull(response); Assert.NotEqual(HttpStatusCode.OK, response.Status); From ddecb1860580b330c7803c2c9aae8d037ad5737a Mon Sep 17 00:00:00 2001 From: Srinivas Thatipamula Date: Sat, 6 Jun 2026 10:35:20 -0700 Subject: [PATCH 13/15] =?UTF-8?q?feat:=20Add=209=20per-target=20shortcut?= =?UTF-8?q?=20commands=20(=C2=A74.4)=20+=20changelog/README=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split bulk shortcut creation into 9 per-target-type commands with flat typed options for better LLM ergonomics: - create_shortcut_onelake - create_shortcut_adls_gen2 - create_shortcut_amazon_s3 - create_shortcut_azure_blob - create_shortcut_gcs - create_shortcut_s3_compatible - create_shortcut_dataverse - create_shortcut_onedrive_sharepoint - create_shortcut_external_data_share Also includes: - New CreateShortcutAsync service method (single-shortcut POST) - Target-specific option definitions in FabricOptionDefinitions - Updated Fabric Server and OneLake toolset READMEs - Changelog entry for all flat-options work (§4.1-4.4) - 281 tests passing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- servers/Fabric.Mcp.Server/README.md | 15 +- .../changelog-entries/1780766915548.yaml | 7 + tools/Fabric.Mcp.Tools.OneLake/README.md | 84 ++++- .../Shortcut/ShortcutCreateAdlsGen2Command.cs | 98 ++++++ .../Shortcut/ShortcutCreateAmazonS3Command.cs | 97 ++++++ .../ShortcutCreateAzureBlobCommand.cs | 98 ++++++ .../ShortcutCreateDataverseCommand.cs | 100 ++++++ .../ShortcutCreateExternalDataShareCommand.cs | 91 ++++++ .../Shortcut/ShortcutCreateGcsCommand.cs | 98 ++++++ ...ShortcutCreateOneDriveSharePointCommand.cs | 101 ++++++ .../Shortcut/ShortcutCreateOneLakeCommand.cs | 100 ++++++ .../ShortcutCreateS3CompatibleCommand.cs | 100 ++++++ .../src/FabricOneLakeSetup.cs | 18 ++ .../src/Models/ShortcutModels.cs | 3 + .../src/Options/FabricOptionDefinitions.cs | 78 +++++ .../src/Options/ShortcutCreateOptions.cs | 26 ++ .../src/Services/IOneLakeService.cs | 1 + .../src/Services/OneLakeService.cs | 15 + .../ShortcutCreateCommandVariantsTests.cs | 303 ++++++++++++++++++ .../FabricOneLakeSetupTests.cs | 16 + .../Services/OneLakeServiceShortcutTests.cs | 103 ++++++ 21 files changed, 1540 insertions(+), 12 deletions(-) create mode 100644 servers/Fabric.Mcp.Server/changelog-entries/1780766915548.yaml create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAdlsGen2Command.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAmazonS3Command.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAzureBlobCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateDataverseCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateExternalDataShareCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateGcsCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneDriveSharePointCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneLakeCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateS3CompatibleCommand.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOptions.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateCommandVariantsTests.cs create mode 100644 tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Services/OneLakeServiceShortcutTests.cs diff --git a/servers/Fabric.Mcp.Server/README.md b/servers/Fabric.Mcp.Server/README.md index ee52908925..58fa49cf69 100644 --- a/servers/Fabric.Mcp.Server/README.md +++ b/servers/Fabric.Mcp.Server/README.md @@ -270,7 +270,16 @@ The Fabric MCP Server exposes tools organized into three categories: |-----------|-------------| | `onelake_list_shortcuts` | Lists shortcuts defined within an item, recursing through subfolders. | | `onelake_get_shortcut` | Gets the properties of a single shortcut. | -| `onelake_create_or_update_shortcuts` | Creates or updates one or more shortcuts in a single call. | +| `onelake_create_or_update_shortcuts` | Creates or updates one or more shortcuts in a single call (bulk JSON). | +| `onelake_create_shortcut_onelake` | Creates a shortcut pointing to another OneLake location. | +| `onelake_create_shortcut_adls_gen2` | Creates a shortcut pointing to Azure Data Lake Storage Gen2. | +| `onelake_create_shortcut_amazon_s3` | Creates a shortcut pointing to Amazon S3. | +| `onelake_create_shortcut_azure_blob` | Creates a shortcut pointing to Azure Blob Storage. | +| `onelake_create_shortcut_gcs` | Creates a shortcut pointing to Google Cloud Storage. | +| `onelake_create_shortcut_s3_compatible` | Creates a shortcut pointing to S3-compatible storage. | +| `onelake_create_shortcut_dataverse` | Creates a shortcut pointing to a Dataverse environment. | +| `onelake_create_shortcut_onedrive_sharepoint` | Creates a shortcut pointing to OneDrive/SharePoint Online. | +| `onelake_create_shortcut_external_data_share` | Creates a shortcut pointing to an external data share. | | `onelake_delete_shortcut` | Deletes a single shortcut from an item (preserves destination data). | | `onelake_reset_shortcut_cache` | Drops cached shortcut reads, forcing re-resolution from destination. | @@ -279,8 +288,8 @@ The Fabric MCP Server exposes tools organized into three categories: | Tool Name | Description | |-----------|-------------| | `onelake_get_settings` | Gets OneLake settings for a workspace (diagnostics + immutability policy). | -| `onelake_modify_diagnostics` | Modifies diagnostic logging configuration at workspace scope. | -| `onelake_modify_immutability_policy` | Modifies the workspace-level OneLake immutability policy. | +| `onelake_modify_diagnostics` | Modifies diagnostic logging configuration (status, destination lakehouse) at workspace scope. | +| `onelake_modify_immutability_policy` | Modifies the workspace-level OneLake immutability policy (scope, retention days). | ### Core Fabric Operations diff --git a/servers/Fabric.Mcp.Server/changelog-entries/1780766915548.yaml b/servers/Fabric.Mcp.Server/changelog-entries/1780766915548.yaml new file mode 100644 index 0000000000..d2d3e1d80a --- /dev/null +++ b/servers/Fabric.Mcp.Server/changelog-entries/1780766915548.yaml @@ -0,0 +1,7 @@ +changes: + - section: "Features Added" + description: "Added 9 per-target shortcut creation tools (OneLake, ADLS Gen2, Amazon S3, Azure Blob, GCS, S3-compatible, Dataverse, OneDrive/SharePoint, External Data Share) with flat typed options for better LLM ergonomics" + - section: "Features Added" + description: "Flattened JSON-string options into discrete typed parameters for diagnostics, immutability policy, and data access role commands" + - section: "Bugs Fixed" + description: "Fixed OneLake diagnostics and immutability settings models to match the Fabric REST API contract" diff --git a/tools/Fabric.Mcp.Tools.OneLake/README.md b/tools/Fabric.Mcp.Tools.OneLake/README.md index 9395b8b4d6..640e2020c3 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/README.md +++ b/tools/Fabric.Mcp.Tools.OneLake/README.md @@ -13,10 +13,10 @@ OneLake is Microsoft Fabric's built-in data lake that provides unified storage f - Read and modify workspace-level OneLake settings (diagnostics, immutability) **Features:** -- 31 comprehensive OneLake commands with full MCP integration +- 40 comprehensive OneLake commands with full MCP integration - Complete coverage for OneLake table APIs: configuration, namespace discovery, and table metadata - Data access security management: list, get, create/update, and delete roles -- Shortcut management: list, get, create/update, delete, and cache reset +- Shortcut management: list, get, create/update (bulk + 9 per-target), delete, and cache reset - Workspace-level settings: diagnostics and immutability policy configuration - Friendly-name support for workspaces and items across all commands - Robust error handling and authentication @@ -714,9 +714,9 @@ dotnet run -- onelake shortcut get --workspace-id "47242da5-ff3b-46fb-a94f-97790 - `--shortcut-name`: Name of the shortcut - `--shortcut-path`: Path of the shortcut within the item -#### Create or Update Shortcuts +#### Create or Update Shortcuts (Bulk) -Creates one or more shortcuts in a single call. Pass `--create-or-overwrite` to upsert (default fails on conflict). +Creates one or more shortcuts in a single call using a JSON blob. Pass `--create-or-overwrite` to upsert (default fails on conflict). ```bash dotnet run -- onelake shortcut create-or-update --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --shortcuts '[{"name":"ExternalData","path":"Tables/ExternalData","target":{"adlsGen2":{"location":"https://storageaccount.dfs.core.windows.net","subpath":"/container/path"}}}]' @@ -728,6 +728,69 @@ dotnet run -- onelake shortcut create-or-update --workspace-id "47242da5-ff3b-46 - `--shortcuts`: JSON array of shortcut definitions - `--create-or-overwrite`: (Optional) If set, overwrites existing shortcuts +#### Create Shortcut (Per-Target — Recommended for AI agents) + +Nine per-target tools provide flat, typed options instead of requiring a JSON blob. Use the tool matching your target type: + +| Tool | Target Type | +|------|-------------| +| `create_shortcut_onelake` | Another OneLake location | +| `create_shortcut_adls_gen2` | Azure Data Lake Storage Gen2 | +| `create_shortcut_amazon_s3` | Amazon S3 | +| `create_shortcut_azure_blob` | Azure Blob Storage | +| `create_shortcut_gcs` | Google Cloud Storage | +| `create_shortcut_s3_compatible` | S3-compatible storage | +| `create_shortcut_dataverse` | Dataverse environment | +| `create_shortcut_onedrive_sharepoint` | OneDrive / SharePoint Online | +| `create_shortcut_external_data_share` | External data share | + +**Common parameters (all per-target tools):** +- `--workspace-id`: Workspace ID (GUID) +- `--item-id`: Item ID (GUID) +- `--shortcut-path`: Path within the item where the shortcut lives +- `--shortcut-name`: Display name for the shortcut +- `--shortcut-conflict-policy`: (Optional) Abort, CreateOrOverwrite, OverwriteOnly, GenerateUniqueName + +**OneLake target additional parameters:** +- `--target-workspace-id`: Target workspace ID +- `--target-item-id`: Target item ID +- `--target-path`: (Optional) Path within the target item +- `--target-connection-id`: (Optional) Connection ID + +**ADLS Gen2 / Amazon S3 / Azure Blob / GCS target parameters:** +- `--target-location`: Storage URL +- `--target-subpath`: (Optional) Subpath within the location +- `--target-connection-id`: Connection ID for authentication + +**S3-compatible target additional parameters:** +- `--target-bucket`: Bucket name (in addition to location, subpath, connection-id) + +**Dataverse target parameters:** +- `--target-environment-domain`: Dataverse environment URI +- `--target-connection-id`: Connection ID +- `--target-deltalake-folder`: Delta Lake folder path +- `--target-table-name`: (Optional) Table name + +**OneDrive/SharePoint target parameters:** +- `--target-location`: Storage URL +- `--target-subpath`: (Optional) Subpath +- `--target-connection-id`: Connection ID +- `--target-update-fabric-item-sensitivity`: (Optional) Update sensitivity label from source + +**External Data Share target parameters:** +- `--target-connection-id`: Connection ID + +**Example — Create an ADLS Gen2 shortcut:** +```bash +dotnet run -- onelake shortcut create-adls-gen2 \ + --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" \ + --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" \ + --shortcut-path "Tables" --shortcut-name "ExternalData" \ + --target-location "https://myaccount.dfs.core.windows.net/container" \ + --target-subpath "/data/tables" \ + --target-connection-id "a1b2c3d4-5678-9012-3456-789012345678" +``` + #### Delete Shortcut Deletes a single shortcut from an item. The destination data is preserved — only the shortcut reference is removed. @@ -770,27 +833,30 @@ dotnet run -- onelake settings get --workspace-id "47242da5-ff3b-46fb-a94f-97790 #### Modify Diagnostics -Modifies the diagnostic logging configuration for OneLake at the workspace scope. Replaces the existing diagnostics block; fetch with `get settings` first if you want to merge. +Modifies the diagnostic logging configuration for OneLake at the workspace scope using flat options. ```bash -dotnet run -- onelake settings modify-diagnostics --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --diagnostics-config '{"logAnalyticsWorkspaceId":"","level":"Verbose"}' +dotnet run -- onelake settings modify-diagnostics --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --status "Enabled" --destination-lakehouse-workspace-id "ws-guid" --destination-lakehouse-item-id "item-guid" ``` **Parameters:** - `--workspace-id`: Workspace ID (GUID) -- `--diagnostics-config`: JSON configuration for diagnostic settings +- `--status`: Diagnostic status (Enabled or Disabled) +- `--destination-lakehouse-workspace-id`: (Optional) Workspace ID of the destination lakehouse +- `--destination-lakehouse-item-id`: (Optional) Item ID of the destination lakehouse #### Modify Immutability Policy Modifies the workspace-level OneLake immutability policy. **Warning:** Once enabled, immutability cannot be disabled — confirm with the user before applying. ```bash -dotnet run -- onelake settings modify-immutability --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --immutability-policy '{"state":"Enabled"}' +dotnet run -- onelake settings modify-immutability --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --scope "Workspace" --retention-days 30 ``` **Parameters:** - `--workspace-id`: Workspace ID (GUID) -- `--immutability-policy`: JSON immutability policy configuration +- `--scope`: Immutability scope (e.g. Workspace) +- `--retention-days`: (Optional) Retention period in days ## Quick Reference - fabmcp.exe Commands diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAdlsGen2Command.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAdlsGen2Command.cs new file mode 100644 index 0000000000..0633dcdfc3 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAdlsGen2Command.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; + +[CommandMetadata( + Id = "a1b2c3d4-2001-4000-8000-000000000011", + Name = "create_shortcut_adls_gen2", + Title = "Create OneLake Shortcut (ADLS Gen2 Target)", + Description = """ + Create a shortcut pointing to an Azure Data Lake Storage Gen2 location. + Requires a connection ID for authentication and a target URL. Requires + OneLake.ReadWrite.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = false, + Secret = false)] +public sealed class ShortcutCreateAdlsGen2Command( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutPath.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutName.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutConflictPolicy.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetLocation.AsRequired()); + command.Options.Add(FabricOptionDefinitions.TargetSubpath.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetConnectionId.AsRequired()); + } + + protected override ShortcutCreateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Path = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPath.Name); + options.Name = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutName.Name); + options.ConflictPolicy = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutConflictPolicy.Name); + options.TargetLocation = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetLocation.Name); + options.TargetSubpath = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetSubpath.Name); + options.TargetConnectionId = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetConnectionId.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + return context.Response; + + var options = BindOptions(parseResult); + try + { + var shortcut = new OneLakeShortcut + { + Path = options.Path, + Name = options.Name, + Target = new ShortcutTarget + { + Type = "AdlsGen2", + AdlsGen2 = new AdlsGen2ShortcutTarget + { + Location = options.TargetLocation, + Subpath = options.TargetSubpath, + ConnectionId = options.TargetConnectionId + } + } + }; + + var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId!, options.ItemId!, shortcut, options.ConflictPolicy, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating ADLS Gen2 shortcut. Workspace: {Workspace}, Item: {Item}.", options.WorkspaceId, options.ItemId); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAmazonS3Command.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAmazonS3Command.cs new file mode 100644 index 0000000000..50d13059b5 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAmazonS3Command.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; + +[CommandMetadata( + Id = "a1b2c3d4-2001-4000-8000-000000000012", + Name = "create_shortcut_amazon_s3", + Title = "Create OneLake Shortcut (Amazon S3 Target)", + Description = """ + Create a shortcut pointing to an Amazon S3 location. Requires a connection + ID for authentication and a target URL. Requires OneLake.ReadWrite.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = false, + Secret = false)] +public sealed class ShortcutCreateAmazonS3Command( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutPath.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutName.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutConflictPolicy.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetLocation.AsRequired()); + command.Options.Add(FabricOptionDefinitions.TargetSubpath.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetConnectionId.AsRequired()); + } + + protected override ShortcutCreateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Path = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPath.Name); + options.Name = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutName.Name); + options.ConflictPolicy = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutConflictPolicy.Name); + options.TargetLocation = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetLocation.Name); + options.TargetSubpath = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetSubpath.Name); + options.TargetConnectionId = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetConnectionId.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + return context.Response; + + var options = BindOptions(parseResult); + try + { + var shortcut = new OneLakeShortcut + { + Path = options.Path, + Name = options.Name, + Target = new ShortcutTarget + { + Type = "AmazonS3", + AmazonS3 = new AmazonS3ShortcutTarget + { + Location = options.TargetLocation, + Subpath = options.TargetSubpath, + ConnectionId = options.TargetConnectionId + } + } + }; + + var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId!, options.ItemId!, shortcut, options.ConflictPolicy, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating Amazon S3 shortcut. Workspace: {Workspace}, Item: {Item}.", options.WorkspaceId, options.ItemId); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAzureBlobCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAzureBlobCommand.cs new file mode 100644 index 0000000000..83666581a5 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAzureBlobCommand.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; + +[CommandMetadata( + Id = "a1b2c3d4-2001-4000-8000-000000000013", + Name = "create_shortcut_azure_blob", + Title = "Create OneLake Shortcut (Azure Blob Storage Target)", + Description = """ + Create a shortcut pointing to an Azure Blob Storage location. Requires a + connection ID for authentication and a target URL. Requires + OneLake.ReadWrite.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = false, + Secret = false)] +public sealed class ShortcutCreateAzureBlobCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutPath.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutName.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutConflictPolicy.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetLocation.AsRequired()); + command.Options.Add(FabricOptionDefinitions.TargetSubpath.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetConnectionId.AsRequired()); + } + + protected override ShortcutCreateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Path = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPath.Name); + options.Name = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutName.Name); + options.ConflictPolicy = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutConflictPolicy.Name); + options.TargetLocation = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetLocation.Name); + options.TargetSubpath = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetSubpath.Name); + options.TargetConnectionId = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetConnectionId.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + return context.Response; + + var options = BindOptions(parseResult); + try + { + var shortcut = new OneLakeShortcut + { + Path = options.Path, + Name = options.Name, + Target = new ShortcutTarget + { + Type = "AzureBlobStorage", + AzureBlobStorage = new AzureBlobStorageShortcutTarget + { + Location = options.TargetLocation, + Subpath = options.TargetSubpath, + ConnectionId = options.TargetConnectionId + } + } + }; + + var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId!, options.ItemId!, shortcut, options.ConflictPolicy, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating Azure Blob shortcut. Workspace: {Workspace}, Item: {Item}.", options.WorkspaceId, options.ItemId); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateDataverseCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateDataverseCommand.cs new file mode 100644 index 0000000000..e42dc86abd --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateDataverseCommand.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; + +[CommandMetadata( + Id = "a1b2c3d4-2001-4000-8000-000000000016", + Name = "create_shortcut_dataverse", + Title = "Create OneLake Shortcut (Dataverse Target)", + Description = """ + Create a shortcut pointing to a Dataverse environment. Requires the + environment domain, connection ID, and Delta Lake folder. Requires + OneLake.ReadWrite.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = false, + Secret = false)] +public sealed class ShortcutCreateDataverseCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutPath.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutName.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutConflictPolicy.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetEnvironmentDomain.AsRequired()); + command.Options.Add(FabricOptionDefinitions.TargetConnectionId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.TargetDeltaLakeFolder.AsRequired()); + command.Options.Add(FabricOptionDefinitions.TargetTableName.AsOptional()); + } + + protected override ShortcutCreateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Path = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPath.Name); + options.Name = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutName.Name); + options.ConflictPolicy = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutConflictPolicy.Name); + options.TargetEnvironmentDomain = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetEnvironmentDomain.Name); + options.TargetConnectionId = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetConnectionId.Name); + options.TargetDeltaLakeFolder = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetDeltaLakeFolder.Name); + options.TargetTableName = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetTableNameOptionName); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + return context.Response; + + var options = BindOptions(parseResult); + try + { + var shortcut = new OneLakeShortcut + { + Path = options.Path, + Name = options.Name, + Target = new ShortcutTarget + { + Type = "Dataverse", + Dataverse = new DataverseShortcutTarget + { + EnvironmentDomain = options.TargetEnvironmentDomain, + ConnectionId = options.TargetConnectionId, + DeltaLakeFolder = options.TargetDeltaLakeFolder + } + } + }; + + var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId!, options.ItemId!, shortcut, options.ConflictPolicy, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating Dataverse shortcut. Workspace: {Workspace}, Item: {Item}.", options.WorkspaceId, options.ItemId); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateExternalDataShareCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateExternalDataShareCommand.cs new file mode 100644 index 0000000000..e44b510b8e --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateExternalDataShareCommand.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; + +[CommandMetadata( + Id = "a1b2c3d4-2001-4000-8000-000000000018", + Name = "create_shortcut_external_data_share", + Title = "Create OneLake Shortcut (External Data Share Target)", + Description = """ + Create a shortcut pointing to an external data share. Only requires a + connection ID. Requires OneLake.ReadWrite.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = false, + Secret = false)] +public sealed class ShortcutCreateExternalDataShareCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutPath.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutName.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutConflictPolicy.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetConnectionId.AsRequired()); + } + + protected override ShortcutCreateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Path = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPath.Name); + options.Name = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutName.Name); + options.ConflictPolicy = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutConflictPolicy.Name); + options.TargetConnectionId = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetConnectionId.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + return context.Response; + + var options = BindOptions(parseResult); + try + { + var shortcut = new OneLakeShortcut + { + Path = options.Path, + Name = options.Name, + Target = new ShortcutTarget + { + Type = "ExternalDataShare", + ExternalDataShare = new ExternalDataShareShortcutTarget + { + ConnectionId = options.TargetConnectionId + } + } + }; + + var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId!, options.ItemId!, shortcut, options.ConflictPolicy, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating external data share shortcut. Workspace: {Workspace}, Item: {Item}.", options.WorkspaceId, options.ItemId); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateGcsCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateGcsCommand.cs new file mode 100644 index 0000000000..d1bb6a619f --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateGcsCommand.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; + +[CommandMetadata( + Id = "a1b2c3d4-2001-4000-8000-000000000014", + Name = "create_shortcut_gcs", + Title = "Create OneLake Shortcut (Google Cloud Storage Target)", + Description = """ + Create a shortcut pointing to a Google Cloud Storage location. Requires a + connection ID for authentication and a target URL. Requires + OneLake.ReadWrite.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = false, + Secret = false)] +public sealed class ShortcutCreateGcsCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutPath.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutName.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutConflictPolicy.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetLocation.AsRequired()); + command.Options.Add(FabricOptionDefinitions.TargetSubpath.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetConnectionId.AsRequired()); + } + + protected override ShortcutCreateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Path = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPath.Name); + options.Name = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutName.Name); + options.ConflictPolicy = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutConflictPolicy.Name); + options.TargetLocation = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetLocation.Name); + options.TargetSubpath = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetSubpath.Name); + options.TargetConnectionId = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetConnectionId.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + return context.Response; + + var options = BindOptions(parseResult); + try + { + var shortcut = new OneLakeShortcut + { + Path = options.Path, + Name = options.Name, + Target = new ShortcutTarget + { + Type = "GoogleCloudStorage", + GoogleCloudStorage = new GoogleCloudStorageShortcutTarget + { + Location = options.TargetLocation, + Subpath = options.TargetSubpath, + ConnectionId = options.TargetConnectionId + } + } + }; + + var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId!, options.ItemId!, shortcut, options.ConflictPolicy, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating GCS shortcut. Workspace: {Workspace}, Item: {Item}.", options.WorkspaceId, options.ItemId); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneDriveSharePointCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneDriveSharePointCommand.cs new file mode 100644 index 0000000000..f597dd2383 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneDriveSharePointCommand.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; + +[CommandMetadata( + Id = "a1b2c3d4-2001-4000-8000-000000000017", + Name = "create_shortcut_onedrive_sharepoint", + Title = "Create OneLake Shortcut (OneDrive/SharePoint Target)", + Description = """ + Create a shortcut pointing to a OneDrive or SharePoint Online location. + Requires a connection ID and target URL. Optionally updates the Fabric + item sensitivity label from the source. Requires OneLake.ReadWrite.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = false, + Secret = false)] +public sealed class ShortcutCreateOneDriveSharePointCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutPath.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutName.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutConflictPolicy.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetLocation.AsRequired()); + command.Options.Add(FabricOptionDefinitions.TargetSubpath.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetConnectionId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.TargetUpdateFabricItemSensitivity.AsOptional()); + } + + protected override ShortcutCreateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Path = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPath.Name); + options.Name = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutName.Name); + options.ConflictPolicy = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutConflictPolicy.Name); + options.TargetLocation = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetLocation.Name); + options.TargetSubpath = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetSubpath.Name); + options.TargetConnectionId = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetConnectionId.Name); + options.TargetUpdateFabricItemSensitivity = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetUpdateFabricItemSensitivityName); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + return context.Response; + + var options = BindOptions(parseResult); + try + { + var shortcut = new OneLakeShortcut + { + Path = options.Path, + Name = options.Name, + Target = new ShortcutTarget + { + Type = "OneDriveSharePoint", + OneDriveSharePoint = new OneDriveSharePointShortcutTarget + { + Location = options.TargetLocation, + Subpath = options.TargetSubpath, + ConnectionId = options.TargetConnectionId, + UpdateFabricItemSensitivity = options.TargetUpdateFabricItemSensitivity ? true : null + } + } + }; + + var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId!, options.ItemId!, shortcut, options.ConflictPolicy, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating OneDrive/SharePoint shortcut. Workspace: {Workspace}, Item: {Item}.", options.WorkspaceId, options.ItemId); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneLakeCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneLakeCommand.cs new file mode 100644 index 0000000000..8426cb3154 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneLakeCommand.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; + +[CommandMetadata( + Id = "a1b2c3d4-2001-4000-8000-000000000010", + Name = "create_shortcut_onelake", + Title = "Create OneLake Shortcut (OneLake Target)", + Description = """ + Create a shortcut pointing to another OneLake location. Specify the target + workspace, item, and optional path within the target item. Requires + OneLake.ReadWrite.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = false, + Secret = false)] +public sealed class ShortcutCreateOneLakeCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutPath.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutName.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutConflictPolicy.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetWorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.TargetItemId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.TargetPath.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetConnectionId.AsOptional()); + } + + protected override ShortcutCreateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Path = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPath.Name); + options.Name = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutName.Name); + options.ConflictPolicy = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutConflictPolicy.Name); + options.TargetWorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetWorkspaceId.Name); + options.TargetItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetItemId.Name); + options.TargetPath = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetPath.Name); + options.TargetConnectionId = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetConnectionId.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + return context.Response; + + var options = BindOptions(parseResult); + try + { + var shortcut = new OneLakeShortcut + { + Path = options.Path, + Name = options.Name, + Target = new ShortcutTarget + { + Type = "OneLake", + OneLake = new OneLakeShortcutTarget + { + WorkspaceId = options.TargetWorkspaceId, + ItemId = options.TargetItemId, + Path = options.TargetPath + } + } + }; + + var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId!, options.ItemId!, shortcut, options.ConflictPolicy, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating OneLake shortcut. Workspace: {Workspace}, Item: {Item}.", options.WorkspaceId, options.ItemId); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateS3CompatibleCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateS3CompatibleCommand.cs new file mode 100644 index 0000000000..272fc5f791 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateS3CompatibleCommand.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Options; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Option; + +namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; + +[CommandMetadata( + Id = "a1b2c3d4-2001-4000-8000-000000000015", + Name = "create_shortcut_s3_compatible", + Title = "Create OneLake Shortcut (S3 Compatible Target)", + Description = """ + Create a shortcut pointing to an S3-compatible storage location. Requires + a connection ID, target URL, and bucket name. Requires OneLake.ReadWrite.All. + """, + Destructive = false, + Idempotent = true, + LocalRequired = false, + OpenWorld = false, + ReadOnly = false, + Secret = false)] +public sealed class ShortcutCreateS3CompatibleCommand( + ILogger logger, + IOneLakeService oneLakeService) : GlobalCommand() +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutPath.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutName.AsRequired()); + command.Options.Add(FabricOptionDefinitions.ShortcutConflictPolicy.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetLocation.AsRequired()); + command.Options.Add(FabricOptionDefinitions.TargetSubpath.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetConnectionId.AsRequired()); + command.Options.Add(FabricOptionDefinitions.TargetBucket.AsRequired()); + } + + protected override ShortcutCreateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); + options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); + options.Path = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPath.Name); + options.Name = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutName.Name); + options.ConflictPolicy = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutConflictPolicy.Name); + options.TargetLocation = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetLocation.Name); + options.TargetSubpath = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetSubpath.Name); + options.TargetConnectionId = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetConnectionId.Name); + options.TargetBucket = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetBucket.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + return context.Response; + + var options = BindOptions(parseResult); + try + { + var shortcut = new OneLakeShortcut + { + Path = options.Path, + Name = options.Name, + Target = new ShortcutTarget + { + Type = "S3Compatible", + S3Compatible = new S3CompatibleShortcutTarget + { + Location = options.TargetLocation, + Subpath = options.TargetSubpath, + ConnectionId = options.TargetConnectionId, + Bucket = options.TargetBucket + } + } + }; + + var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId!, options.ItemId!, shortcut, options.ConflictPolicy, cancellationToken); + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating S3-compatible shortcut. Workspace: {Workspace}, Item: {Item}.", options.WorkspaceId, options.ItemId); + HandleException(context, ex); + } + + return context.Response; + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/FabricOneLakeSetup.cs b/tools/Fabric.Mcp.Tools.OneLake/src/FabricOneLakeSetup.cs index d2bb1d856f..f1e474318a 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/FabricOneLakeSetup.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/FabricOneLakeSetup.cs @@ -67,6 +67,15 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); // Register settings commands services.AddSingleton(); @@ -116,6 +125,15 @@ Microsoft Fabric OneLake Operations - Manage and interact with OneLake data lake fabricOneLake.AddCommand(serviceProvider); fabricOneLake.AddCommand(serviceProvider); fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); + fabricOneLake.AddCommand(serviceProvider); // Register settings commands fabricOneLake.AddCommand(serviceProvider); diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Models/ShortcutModels.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Models/ShortcutModels.cs index f9edf02ff2..0e9692dee1 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Models/ShortcutModels.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Models/ShortcutModels.cs @@ -144,6 +144,9 @@ public class S3CompatibleShortcutTarget [JsonPropertyName("connectionId")] public string? ConnectionId { get; set; } + + [JsonPropertyName("bucket")] + public string? Bucket { get; set; } } /// diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs index 6a106d1e51..0fb9bb4b13 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs @@ -320,4 +320,82 @@ Example with GUIDs (use when you already have the object ID): Description = "Number of days to retain diagnostic logs (minimum 1). Cannot be reduced below the current value.", Required = true }; + + // Shortcut target options + public const string TargetWorkspaceIdName = "target-workspace-id"; + public static readonly Option TargetWorkspaceId = new($"--{TargetWorkspaceIdName}") + { + Description = "The workspace ID (GUID) of the target OneLake item.", + Required = true + }; + + public const string TargetItemIdName = "target-item-id"; + public static readonly Option TargetItemId = new($"--{TargetItemIdName}") + { + Description = "The item ID (GUID) of the target OneLake item.", + Required = true + }; + + public const string TargetPathName = "target-path"; + public static readonly Option TargetPath = new($"--{TargetPathName}") + { + Description = "The path within the target item (e.g. 'Files/data').", + Required = false + }; + + public const string TargetLocationName = "target-location"; + public static readonly Option TargetLocation = new($"--{TargetLocationName}") + { + Description = "The target storage URL (e.g. 'https://myaccount.dfs.core.windows.net/container').", + Required = true + }; + + public const string TargetSubpathName = "target-subpath"; + public static readonly Option TargetSubpath = new($"--{TargetSubpathName}") + { + Description = "The subpath within the target storage location.", + Required = false + }; + + public const string TargetConnectionIdName = "target-connection-id"; + public static readonly Option TargetConnectionId = new($"--{TargetConnectionIdName}") + { + Description = "The connection ID (GUID) for authenticating to the target.", + Required = true + }; + + public const string TargetBucketName = "target-bucket"; + public static readonly Option TargetBucket = new($"--{TargetBucketName}") + { + Description = "The bucket name for S3-compatible targets.", + Required = true + }; + + public const string TargetEnvironmentDomainName = "target-environment-domain"; + public static readonly Option TargetEnvironmentDomain = new($"--{TargetEnvironmentDomainName}") + { + Description = "The Dataverse environment domain URI (e.g. 'https://orgname.crm.dynamics.com').", + Required = true + }; + + public const string TargetDeltaLakeFolderName = "target-deltalake-folder"; + public static readonly Option TargetDeltaLakeFolder = new($"--{TargetDeltaLakeFolderName}") + { + Description = "The Delta Lake folder path in Dataverse.", + Required = true + }; + + public const string TargetTableNameOptionName = "target-table-name"; + public static readonly Option TargetTableName = new($"--{TargetTableNameOptionName}") + { + Description = "The Dataverse table name.", + Required = false + }; + + public const string TargetUpdateFabricItemSensitivityName = "target-update-fabric-item-sensitivity"; + public static readonly Option TargetUpdateFabricItemSensitivity = new($"--{TargetUpdateFabricItemSensitivityName}") + { + Description = "Whether to update Fabric item sensitivity from OneDrive/SharePoint. Default: false.", + Required = false + }; } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOptions.cs new file mode 100644 index 0000000000..e595ad5268 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOptions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Mcp.Core.Options; + +namespace Fabric.Mcp.Tools.OneLake.Options; + +public sealed class ShortcutCreateOptions : GlobalOptions +{ + public string? WorkspaceId { get; set; } + public string? ItemId { get; set; } + public string? Path { get; set; } + public string? Name { get; set; } + public string? ConflictPolicy { get; set; } + public string? TargetWorkspaceId { get; set; } + public string? TargetItemId { get; set; } + public string? TargetPath { get; set; } + public string? TargetLocation { get; set; } + public string? TargetSubpath { get; set; } + public string? TargetConnectionId { get; set; } + public string? TargetBucket { get; set; } + public string? TargetEnvironmentDomain { get; set; } + public string? TargetDeltaLakeFolder { get; set; } + public string? TargetTableName { get; set; } + public bool TargetUpdateFabricItemSensitivity { get; set; } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs index 263c76c0a7..d2f3edb12a 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Services/IOneLakeService.cs @@ -57,6 +57,7 @@ public interface IOneLakeService Task ListShortcutsAsync(string workspaceId, string itemId, string? parentPath = null, string? continuationToken = null, CancellationToken cancellationToken = default); Task GetShortcutAsync(string workspaceId, string itemId, string shortcutPath, string shortcutName, CancellationToken cancellationToken = default); Task CreateOrUpdateShortcutsAsync(string workspaceId, string itemId, string shortcutsJson, string? shortcutConflictPolicy = null, CancellationToken cancellationToken = default); + Task CreateShortcutAsync(string workspaceId, string itemId, OneLakeShortcut shortcut, string? shortcutConflictPolicy = null, CancellationToken cancellationToken = default); Task DeleteShortcutAsync(string workspaceId, string itemId, string shortcutPath, string shortcutName, CancellationToken cancellationToken = default); Task ResetShortcutCacheAsync(string workspaceId, CancellationToken cancellationToken = default); diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs index 1b7b5c16b4..89e646a120 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Services/OneLakeService.cs @@ -2050,6 +2050,21 @@ public async Task CreateOrUpdateShortcutsAsync(strin ?? new BulkCreateShortcutResponse(); } + public async Task CreateShortcutAsync(string workspaceId, string itemId, OneLakeShortcut shortcut, string? shortcutConflictPolicy = null, CancellationToken cancellationToken = default) + { + var url = $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{workspaceId}/items/{itemId}/shortcuts"; + if (!string.IsNullOrWhiteSpace(shortcutConflictPolicy)) + url += $"?shortcutConflictPolicy={Uri.EscapeDataString(shortcutConflictPolicy)}"; + + var body = JsonSerializer.Serialize(shortcut, OneLakeJsonContext.Default.OneLakeShortcut); + var response = await SendFabricApiRequestAsync(HttpMethod.Post, url, body, cancellationToken: cancellationToken); + using var reader = new StreamReader(response); + var responseBody = await reader.ReadToEndAsync(cancellationToken); + if (string.IsNullOrWhiteSpace(responseBody)) + return shortcut; + return JsonSerializer.Deserialize(responseBody, OneLakeJsonContext.Default.OneLakeShortcut) ?? shortcut; + } + public async Task DeleteShortcutAsync(string workspaceId, string itemId, string shortcutPath, string shortcutName, CancellationToken cancellationToken = default) { var encodedPath = Uri.EscapeDataString(shortcutPath); diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateCommandVariantsTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateCommandVariantsTests.cs new file mode 100644 index 0000000000..327b302933 --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateCommandVariantsTests.cs @@ -0,0 +1,303 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Fabric.Mcp.Tools.OneLake.Commands.Shortcut; +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using NSubstitute; + +namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Shortcut; + +public class ShortcutCreateCommandVariantsTests +{ + [Fact] + public async Task ExecuteAsync_OneLakeCommand_MapsTargetValues() + { + var service = CreateShortcutService(); + var command = new ShortcutCreateOneLakeCommand(Substitute.For>(), service); + + var response = await ExecuteAsync(command, + "--workspace-id", "ws1", + "--item-id", "item1", + "--shortcut-path", "Files/landing", + "--shortcut-name", "shortcut1", + "--target-workspace-id", "target-ws", + "--target-item-id", "target-item", + "--target-path", "Files/data"); + + Assert.Equal(HttpStatusCode.OK, response.Status); + await service.Received(1).CreateShortcutAsync( + "ws1", + "item1", + Arg.Is(shortcut => + shortcut.Path == "Files/landing" && + shortcut.Name == "shortcut1" && + shortcut.Target!.Type == "OneLake" && + shortcut.Target.OneLake!.WorkspaceId == "target-ws" && + shortcut.Target.OneLake.ItemId == "target-item" && + shortcut.Target.OneLake.Path == "Files/data"), + null, + Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_AdlsGen2Command_MapsTargetValues() + { + var service = CreateShortcutService(); + var command = new ShortcutCreateAdlsGen2Command(Substitute.For>(), service); + + var response = await ExecuteAsync(command, + "--workspace-id", "ws1", + "--item-id", "item1", + "--shortcut-path", "Files/landing", + "--shortcut-name", "shortcut1", + "--target-location", "https://account.dfs.core.windows.net/container", + "--target-subpath", "/folder", + "--target-connection-id", "connection-1"); + + Assert.Equal(HttpStatusCode.OK, response.Status); + await service.Received(1).CreateShortcutAsync( + "ws1", + "item1", + Arg.Is(shortcut => + shortcut.Target!.Type == "AdlsGen2" && + shortcut.Target.AdlsGen2!.Location == "https://account.dfs.core.windows.net/container" && + shortcut.Target.AdlsGen2.Subpath == "/folder" && + shortcut.Target.AdlsGen2.ConnectionId == "connection-1"), + null, + Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_AmazonS3Command_MapsTargetValues() + { + var service = CreateShortcutService(); + var command = new ShortcutCreateAmazonS3Command(Substitute.For>(), service); + + var response = await ExecuteAsync(command, + "--workspace-id", "ws1", + "--item-id", "item1", + "--shortcut-path", "Files/landing", + "--shortcut-name", "shortcut1", + "--target-location", "https://bucket.s3.us-west-2.amazonaws.com", + "--target-subpath", "/folder", + "--target-connection-id", "connection-1"); + + Assert.Equal(HttpStatusCode.OK, response.Status); + await service.Received(1).CreateShortcutAsync( + "ws1", + "item1", + Arg.Is(shortcut => + shortcut.Target!.Type == "AmazonS3" && + shortcut.Target.AmazonS3!.Location == "https://bucket.s3.us-west-2.amazonaws.com" && + shortcut.Target.AmazonS3.Subpath == "/folder" && + shortcut.Target.AmazonS3.ConnectionId == "connection-1"), + null, + Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_AzureBlobCommand_MapsTargetValues() + { + var service = CreateShortcutService(); + var command = new ShortcutCreateAzureBlobCommand(Substitute.For>(), service); + + var response = await ExecuteAsync(command, + "--workspace-id", "ws1", + "--item-id", "item1", + "--shortcut-path", "Files/landing", + "--shortcut-name", "shortcut1", + "--target-location", "https://account.blob.core.windows.net/container", + "--target-subpath", "/folder", + "--target-connection-id", "connection-1"); + + Assert.Equal(HttpStatusCode.OK, response.Status); + await service.Received(1).CreateShortcutAsync( + "ws1", + "item1", + Arg.Is(shortcut => + shortcut.Target!.Type == "AzureBlobStorage" && + shortcut.Target.AzureBlobStorage!.Location == "https://account.blob.core.windows.net/container" && + shortcut.Target.AzureBlobStorage.Subpath == "/folder" && + shortcut.Target.AzureBlobStorage.ConnectionId == "connection-1"), + null, + Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_GcsCommand_MapsTargetValues() + { + var service = CreateShortcutService(); + var command = new ShortcutCreateGcsCommand(Substitute.For>(), service); + + var response = await ExecuteAsync(command, + "--workspace-id", "ws1", + "--item-id", "item1", + "--shortcut-path", "Files/landing", + "--shortcut-name", "shortcut1", + "--target-location", "https://bucket.storage.googleapis.com", + "--target-subpath", "/folder", + "--target-connection-id", "connection-1"); + + Assert.Equal(HttpStatusCode.OK, response.Status); + await service.Received(1).CreateShortcutAsync( + "ws1", + "item1", + Arg.Is(shortcut => + shortcut.Target!.Type == "GoogleCloudStorage" && + shortcut.Target.GoogleCloudStorage!.Location == "https://bucket.storage.googleapis.com" && + shortcut.Target.GoogleCloudStorage.Subpath == "/folder" && + shortcut.Target.GoogleCloudStorage.ConnectionId == "connection-1"), + null, + Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_S3CompatibleCommand_MapsBucketValue() + { + var service = CreateShortcutService(); + var command = new ShortcutCreateS3CompatibleCommand(Substitute.For>(), service); + + var response = await ExecuteAsync(command, + "--workspace-id", "ws1", + "--item-id", "item1", + "--shortcut-path", "Files/landing", + "--shortcut-name", "shortcut1", + "--target-location", "https://s3endpoint.contoso.com", + "--target-subpath", "/folder", + "--target-connection-id", "connection-1", + "--target-bucket", "bucket-1"); + + Assert.Equal(HttpStatusCode.OK, response.Status); + await service.Received(1).CreateShortcutAsync( + "ws1", + "item1", + Arg.Is(shortcut => + shortcut.Target!.Type == "S3Compatible" && + shortcut.Target.S3Compatible!.Location == "https://s3endpoint.contoso.com" && + shortcut.Target.S3Compatible.Subpath == "/folder" && + shortcut.Target.S3Compatible.ConnectionId == "connection-1" && + shortcut.Target.S3Compatible.Bucket == "bucket-1"), + null, + Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_DataverseCommand_MapsTargetValues() + { + var service = CreateShortcutService(); + var command = new ShortcutCreateDataverseCommand(Substitute.For>(), service); + + var response = await ExecuteAsync(command, + "--workspace-id", "ws1", + "--item-id", "item1", + "--shortcut-path", "Files/landing", + "--shortcut-name", "shortcut1", + "--target-environment-domain", "https://org.crm.dynamics.com", + "--target-connection-id", "connection-1", + "--target-deltalake-folder", "Tables/account", + "--target-table-name", "account"); + + Assert.Equal(HttpStatusCode.OK, response.Status); + await service.Received(1).CreateShortcutAsync( + "ws1", + "item1", + Arg.Is(shortcut => + shortcut.Target!.Type == "Dataverse" && + shortcut.Target.Dataverse!.EnvironmentDomain == "https://org.crm.dynamics.com" && + shortcut.Target.Dataverse.ConnectionId == "connection-1" && + shortcut.Target.Dataverse.DeltaLakeFolder == "Tables/account"), + null, + Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_OneDriveSharePointCommand_MapsSensitivityFlag() + { + var service = CreateShortcutService(); + var command = new ShortcutCreateOneDriveSharePointCommand(Substitute.For>(), service); + + var response = await ExecuteAsync(command, + "--workspace-id", "ws1", + "--item-id", "item1", + "--shortcut-path", "Files/landing", + "--shortcut-name", "shortcut1", + "--target-location", "https://contoso.sharepoint.com/sites/site", + "--target-subpath", "/Documents", + "--target-connection-id", "connection-1", + "--target-update-fabric-item-sensitivity", "true"); + + Assert.Equal(HttpStatusCode.OK, response.Status); + await service.Received(1).CreateShortcutAsync( + "ws1", + "item1", + Arg.Is(shortcut => + shortcut.Target!.Type == "OneDriveSharePoint" && + shortcut.Target.OneDriveSharePoint!.Location == "https://contoso.sharepoint.com/sites/site" && + shortcut.Target.OneDriveSharePoint.Subpath == "/Documents" && + shortcut.Target.OneDriveSharePoint.ConnectionId == "connection-1" && + shortcut.Target.OneDriveSharePoint.UpdateFabricItemSensitivity == true), + null, + Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_ExternalDataShareCommand_MapsTargetValues() + { + var service = CreateShortcutService(); + var command = new ShortcutCreateExternalDataShareCommand(Substitute.For>(), service); + + var response = await ExecuteAsync(command, + "--workspace-id", "ws1", + "--item-id", "item1", + "--shortcut-path", "Files/landing", + "--shortcut-name", "shortcut1", + "--target-connection-id", "connection-1"); + + Assert.Equal(HttpStatusCode.OK, response.Status); + await service.Received(1).CreateShortcutAsync( + "ws1", + "item1", + Arg.Is(shortcut => + shortcut.Target!.Type == "ExternalDataShare" && + shortcut.Target.ExternalDataShare!.ConnectionId == "connection-1"), + null, + Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_MissingRequiredArguments_ReturnsBadRequest() + { + var command = new ShortcutCreateOneLakeCommand( + Substitute.For>(), + CreateShortcutService()); + + var response = await ExecuteAsync(command, + "--item-id", "item1", + "--shortcut-path", "Files/landing", + "--shortcut-name", "shortcut1", + "--target-workspace-id", "target-ws", + "--target-item-id", "target-item"); + + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + + private static IOneLakeService CreateShortcutService() + { + var service = Substitute.For(); + service.CreateShortcutAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(callInfo => callInfo.ArgAt(2)); + return service; + } + + private static async Task ExecuteAsync(IBaseCommand command, params string[] args) + { + using var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var context = new CommandContext(serviceProvider); + return await command.ExecuteAsync(context, command.GetCommand().Parse(args), CancellationToken.None); + } +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/FabricOneLakeSetupTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/FabricOneLakeSetupTests.cs index 80ae08526e..ed48b46196 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/FabricOneLakeSetupTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/FabricOneLakeSetupTests.cs @@ -61,5 +61,21 @@ public void RegisterCommands_RegistersAllOneLakeCommands() Assert.True(rootGroup.Commands.ContainsKey("get_table"), "Should have get_table command"); Assert.True(rootGroup.Commands.ContainsKey("list_table_namespaces"), "Should have list_table_namespaces command"); Assert.True(rootGroup.Commands.ContainsKey("get_table_namespace"), "Should have get_table_namespace command"); + + // Shortcut commands + Assert.True(rootGroup.Commands.ContainsKey("list_shortcuts"), "Should have list_shortcuts command"); + Assert.True(rootGroup.Commands.ContainsKey("get_shortcut"), "Should have get_shortcut command"); + Assert.True(rootGroup.Commands.ContainsKey("create_or_update_shortcuts"), "Should have create_or_update_shortcuts command"); + Assert.True(rootGroup.Commands.ContainsKey("delete_shortcut"), "Should have delete_shortcut command"); + Assert.True(rootGroup.Commands.ContainsKey("reset_shortcut_cache"), "Should have reset_shortcut_cache command"); + Assert.True(rootGroup.Commands.ContainsKey("create_shortcut_onelake"), "Should have create_shortcut_onelake command"); + Assert.True(rootGroup.Commands.ContainsKey("create_shortcut_adls_gen2"), "Should have create_shortcut_adls_gen2 command"); + Assert.True(rootGroup.Commands.ContainsKey("create_shortcut_amazon_s3"), "Should have create_shortcut_amazon_s3 command"); + Assert.True(rootGroup.Commands.ContainsKey("create_shortcut_azure_blob"), "Should have create_shortcut_azure_blob command"); + Assert.True(rootGroup.Commands.ContainsKey("create_shortcut_gcs"), "Should have create_shortcut_gcs command"); + Assert.True(rootGroup.Commands.ContainsKey("create_shortcut_s3_compatible"), "Should have create_shortcut_s3_compatible command"); + Assert.True(rootGroup.Commands.ContainsKey("create_shortcut_dataverse"), "Should have create_shortcut_dataverse command"); + Assert.True(rootGroup.Commands.ContainsKey("create_shortcut_onedrive_sharepoint"), "Should have create_shortcut_onedrive_sharepoint command"); + Assert.True(rootGroup.Commands.ContainsKey("create_shortcut_external_data_share"), "Should have create_shortcut_external_data_share command"); } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Services/OneLakeServiceShortcutTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Services/OneLakeServiceShortcutTests.cs new file mode 100644 index 0000000000..cf8a7d111d --- /dev/null +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Services/OneLakeServiceShortcutTests.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma warning disable xUnit1051 + +using System.Net; +using System.Text; +using System.Text.Json; +using Fabric.Mcp.Tools.OneLake.Models; +using Fabric.Mcp.Tools.OneLake.Services; +using Fabric.Mcp.Tools.OneLake.Tests.TestSupport; + +namespace Fabric.Mcp.Tools.OneLake.Tests.Services; + +public class OneLakeServiceShortcutTests +{ + private const string WorkspaceId = "ws-shortcut-test"; + private const string ItemId = "item-shortcut-test"; + + [Fact] + public async Task CreateShortcutAsync_WithConflictPolicy_SendsExpectedRequestAndReturnsResponse() + { + HttpRequestMessage? capturedRequest = null; + string? capturedBody = null; + var expectedShortcut = new OneLakeShortcut + { + Path = "Files/landing", + Name = "shortcut1", + Target = new ShortcutTarget + { + Type = "OneLake", + OneLake = new OneLakeShortcutTarget + { + WorkspaceId = "target-ws", + ItemId = "target-item", + Path = "Files/data" + } + } + }; + + var handler = new CapturingHttpMessageHandler(request => + { + capturedRequest = request; + capturedBody = request.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); + var responseJson = JsonSerializer.Serialize(expectedShortcut, OneLakeJsonContext.Default.OneLakeShortcut); + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responseJson, Encoding.UTF8, "application/json") + }; + }); + + using var httpClient = new HttpClient(handler); + var service = new OneLakeService(httpClient, new FakeTokenCredential()); + + var result = await service.CreateShortcutAsync(WorkspaceId, ItemId, expectedShortcut, "CreateOrOverwrite"); + + Assert.NotNull(capturedRequest); + Assert.Equal(HttpMethod.Post, capturedRequest!.Method); + Assert.Equal( + $"{OneLakeEndpoints.GetFabricApiBaseUrl()}/workspaces/{WorkspaceId}/items/{ItemId}/shortcuts?shortcutConflictPolicy=CreateOrOverwrite", + capturedRequest.RequestUri!.ToString()); + + Assert.NotNull(capturedBody); + var sentShortcut = JsonSerializer.Deserialize(capturedBody, OneLakeJsonContext.Default.OneLakeShortcut); + Assert.NotNull(sentShortcut); + Assert.Equal(expectedShortcut.Name, sentShortcut!.Name); + Assert.Equal(expectedShortcut.Target!.Type, sentShortcut.Target!.Type); + Assert.Equal(expectedShortcut.Target.OneLake!.WorkspaceId, sentShortcut.Target.OneLake!.WorkspaceId); + + Assert.Equal(expectedShortcut.Name, result.Name); + Assert.Equal(expectedShortcut.Target.OneLake.ItemId, result.Target!.OneLake!.ItemId); + } + + [Fact] + public async Task CreateShortcutAsync_WithEmptyResponse_ReturnsOriginalShortcut() + { + var shortcut = new OneLakeShortcut + { + Path = "Files/landing", + Name = "shortcut2", + Target = new ShortcutTarget + { + Type = "ExternalDataShare", + ExternalDataShare = new ExternalDataShareShortcutTarget + { + ConnectionId = "connection-1" + } + } + }; + + var handler = new CapturingHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(string.Empty, Encoding.UTF8, "application/json") + }); + + using var httpClient = new HttpClient(handler); + var service = new OneLakeService(httpClient, new FakeTokenCredential()); + + var result = await service.CreateShortcutAsync(WorkspaceId, ItemId, shortcut); + + Assert.Same(shortcut, result); + } +} From 67117041f45cdcd8529fac7a88c8506a824cbc9b Mon Sep 17 00:00:00 2001 From: Srinivas Thatipamula Date: Sun, 14 Jun 2026 12:26:03 -0700 Subject: [PATCH 14/15] Fix shortcut bugs from flat-options spec review (B13-B19, CL-1) - Delete ShortcutCreateExternalDataShareCommand (B13: endpoint doesn't exist) - Delete ShortcutCreateOrUpdateCommand bulk JSON tool (CL-1: dropped from v1) - Make --target-subpath required in AdlsGen2 command (B14) - Make --target-path required in OneLake command (B15) - Wire --target-connection-id through OneLakeShortcutTarget model (B16) - Omit target.type on create requests via JsonIgnore WhenWritingNull (B17) - Constrain --conflict-policy with AcceptOnlyFromAmong enum values (B18) - Add --include-managed flag to list shortcuts, filter DW-managed by default (B19) - Update READMEs for Fabric.Mcp.Server and OneLake toolset Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- servers/Fabric.Mcp.Server/README.md | 4 +- tools/Fabric.Mcp.Tools.OneLake/README.md | 31 +--- .../Shortcut/ShortcutCreateAdlsGen2Command.cs | 3 +- .../Shortcut/ShortcutCreateAmazonS3Command.cs | 1 - .../ShortcutCreateAzureBlobCommand.cs | 1 - .../ShortcutCreateDataverseCommand.cs | 1 - .../ShortcutCreateExternalDataShareCommand.cs | 91 ----------- .../Shortcut/ShortcutCreateGcsCommand.cs | 1 - ...ShortcutCreateOneDriveSharePointCommand.cs | 1 - .../Shortcut/ShortcutCreateOneLakeCommand.cs | 6 +- .../Shortcut/ShortcutCreateOrUpdateCommand.cs | 84 ----------- .../ShortcutCreateS3CompatibleCommand.cs | 1 - .../Commands/Shortcut/ShortcutListCommand.cs | 25 ++++ .../src/FabricOneLakeSetup.cs | 4 - .../src/Models/ShortcutModels.cs | 8 + .../src/Options/FabricOptionDefinitions.cs | 27 ++-- .../Options/ShortcutCreateOrUpdateOptions.cs | 15 -- .../src/Options/ShortcutListOptions.cs | 1 + .../ShortcutCreateCommandVariantsTests.cs | 32 ---- .../ShortcutCreateOrUpdateCommandTests.cs | 141 ------------------ .../FabricOneLakeSetupTests.cs | 2 - 21 files changed, 63 insertions(+), 417 deletions(-) delete mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateExternalDataShareCommand.cs delete mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOrUpdateCommand.cs delete mode 100644 tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOrUpdateOptions.cs delete mode 100644 tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateOrUpdateCommandTests.cs diff --git a/servers/Fabric.Mcp.Server/README.md b/servers/Fabric.Mcp.Server/README.md index 58fa49cf69..c51d6cc578 100644 --- a/servers/Fabric.Mcp.Server/README.md +++ b/servers/Fabric.Mcp.Server/README.md @@ -268,9 +268,8 @@ The Fabric MCP Server exposes tools organized into three categories: | Tool Name | Description | |-----------|-------------| -| `onelake_list_shortcuts` | Lists shortcuts defined within an item, recursing through subfolders. | +| `onelake_list_shortcuts` | Lists shortcuts defined within an item. Hides DW-managed shortcuts by default (`--include-managed` to show). | | `onelake_get_shortcut` | Gets the properties of a single shortcut. | -| `onelake_create_or_update_shortcuts` | Creates or updates one or more shortcuts in a single call (bulk JSON). | | `onelake_create_shortcut_onelake` | Creates a shortcut pointing to another OneLake location. | | `onelake_create_shortcut_adls_gen2` | Creates a shortcut pointing to Azure Data Lake Storage Gen2. | | `onelake_create_shortcut_amazon_s3` | Creates a shortcut pointing to Amazon S3. | @@ -279,7 +278,6 @@ The Fabric MCP Server exposes tools organized into three categories: | `onelake_create_shortcut_s3_compatible` | Creates a shortcut pointing to S3-compatible storage. | | `onelake_create_shortcut_dataverse` | Creates a shortcut pointing to a Dataverse environment. | | `onelake_create_shortcut_onedrive_sharepoint` | Creates a shortcut pointing to OneDrive/SharePoint Online. | -| `onelake_create_shortcut_external_data_share` | Creates a shortcut pointing to an external data share. | | `onelake_delete_shortcut` | Deletes a single shortcut from an item (preserves destination data). | | `onelake_reset_shortcut_cache` | Drops cached shortcut reads, forcing re-resolution from destination. | diff --git a/tools/Fabric.Mcp.Tools.OneLake/README.md b/tools/Fabric.Mcp.Tools.OneLake/README.md index 640e2020c3..7fa8e2293e 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/README.md +++ b/tools/Fabric.Mcp.Tools.OneLake/README.md @@ -9,14 +9,14 @@ OneLake is Microsoft Fabric's built-in data lake that provides unified storage f - Manage OneLake folders and files - Browse items, tables and namespaces - Configure OneLake data access security (role-based) -- Create, list and manage shortcuts (including bulk + cache reset) +- Create, list and manage shortcuts (per-target typed tools + cache reset) - Read and modify workspace-level OneLake settings (diagnostics, immutability) **Features:** - 40 comprehensive OneLake commands with full MCP integration - Complete coverage for OneLake table APIs: configuration, namespace discovery, and table metadata - Data access security management: list, get, create/update, and delete roles -- Shortcut management: list, get, create/update (bulk + 9 per-target), delete, and cache reset +- Shortcut management: list, get, create (8 per-target typed tools), delete, and cache reset - Workspace-level settings: diagnostics and immutability policy configuration - Friendly-name support for workspaces and items across all commands - Robust error handling and authentication @@ -688,7 +688,7 @@ Shortcuts are references to data stored in external or internal locations (ADLS #### List Shortcuts -Lists shortcuts defined within an item, recursing through subfolders. Supports pagination via `--continuation-token`. +Lists shortcuts defined within an item, recursing through subfolders. Supports pagination via `--continuation-token`. By default, DW-managed shortcuts (which can number in the hundreds of thousands per Warehouse) are hidden — use `--include-managed` to show them. ```bash dotnet run -- onelake shortcut list --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" @@ -699,6 +699,7 @@ dotnet run -- onelake shortcut list --workspace-id "47242da5-ff3b-46fb-a94f-9779 - `--item-id`: Item ID (GUID) - `--parent-path`: (Optional) Parent path to scope the listing - `--continuation-token`: (Optional) Token for retrieving the next page of results +- `--include-managed`: (Optional) Include DW-managed shortcuts in results (default: false) #### Get Shortcut @@ -714,23 +715,9 @@ dotnet run -- onelake shortcut get --workspace-id "47242da5-ff3b-46fb-a94f-97790 - `--shortcut-name`: Name of the shortcut - `--shortcut-path`: Path of the shortcut within the item -#### Create or Update Shortcuts (Bulk) - -Creates one or more shortcuts in a single call using a JSON blob. Pass `--create-or-overwrite` to upsert (default fails on conflict). - -```bash -dotnet run -- onelake shortcut create-or-update --workspace-id "47242da5-ff3b-46fb-a94f-977909b773d5" --item-id "0e67ed13-2bb6-49be-9c87-a1105a4ea342" --shortcuts '[{"name":"ExternalData","path":"Tables/ExternalData","target":{"adlsGen2":{"location":"https://storageaccount.dfs.core.windows.net","subpath":"/container/path"}}}]' -``` - -**Parameters:** -- `--workspace-id`: Workspace ID (GUID) -- `--item-id`: Item ID (GUID) -- `--shortcuts`: JSON array of shortcut definitions -- `--create-or-overwrite`: (Optional) If set, overwrites existing shortcuts - #### Create Shortcut (Per-Target — Recommended for AI agents) -Nine per-target tools provide flat, typed options instead of requiring a JSON blob. Use the tool matching your target type: +Eight per-target tools provide flat, typed options instead of requiring a JSON blob. Use the tool matching your target type: | Tool | Target Type | |------|-------------| @@ -742,7 +729,6 @@ Nine per-target tools provide flat, typed options instead of requiring a JSON bl | `create_shortcut_s3_compatible` | S3-compatible storage | | `create_shortcut_dataverse` | Dataverse environment | | `create_shortcut_onedrive_sharepoint` | OneDrive / SharePoint Online | -| `create_shortcut_external_data_share` | External data share | **Common parameters (all per-target tools):** - `--workspace-id`: Workspace ID (GUID) @@ -754,12 +740,12 @@ Nine per-target tools provide flat, typed options instead of requiring a JSON bl **OneLake target additional parameters:** - `--target-workspace-id`: Target workspace ID - `--target-item-id`: Target item ID -- `--target-path`: (Optional) Path within the target item +- `--target-path`: Path within the target item (required) - `--target-connection-id`: (Optional) Connection ID **ADLS Gen2 / Amazon S3 / Azure Blob / GCS target parameters:** - `--target-location`: Storage URL -- `--target-subpath`: (Optional) Subpath within the location +- `--target-subpath`: Subpath within the location (required for ADLS Gen2) - `--target-connection-id`: Connection ID for authentication **S3-compatible target additional parameters:** @@ -777,9 +763,6 @@ Nine per-target tools provide flat, typed options instead of requiring a JSON bl - `--target-connection-id`: Connection ID - `--target-update-fabric-item-sensitivity`: (Optional) Update sensitivity label from source -**External Data Share target parameters:** -- `--target-connection-id`: Connection ID - **Example — Create an ADLS Gen2 shortcut:** ```bash dotnet run -- onelake shortcut create-adls-gen2 \ diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAdlsGen2Command.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAdlsGen2Command.cs index 0633dcdfc3..5c16379f4a 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAdlsGen2Command.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAdlsGen2Command.cs @@ -42,7 +42,7 @@ protected override void RegisterOptions(Command command) command.Options.Add(FabricOptionDefinitions.ShortcutName.AsRequired()); command.Options.Add(FabricOptionDefinitions.ShortcutConflictPolicy.AsOptional()); command.Options.Add(FabricOptionDefinitions.TargetLocation.AsRequired()); - command.Options.Add(FabricOptionDefinitions.TargetSubpath.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetSubpath.AsRequired()); command.Options.Add(FabricOptionDefinitions.TargetConnectionId.AsRequired()); } @@ -74,7 +74,6 @@ public override async Task ExecuteAsync(CommandContext context, Name = options.Name, Target = new ShortcutTarget { - Type = "AdlsGen2", AdlsGen2 = new AdlsGen2ShortcutTarget { Location = options.TargetLocation, diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAmazonS3Command.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAmazonS3Command.cs index 50d13059b5..aaaab61087 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAmazonS3Command.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAmazonS3Command.cs @@ -73,7 +73,6 @@ public override async Task ExecuteAsync(CommandContext context, Name = options.Name, Target = new ShortcutTarget { - Type = "AmazonS3", AmazonS3 = new AmazonS3ShortcutTarget { Location = options.TargetLocation, diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAzureBlobCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAzureBlobCommand.cs index 83666581a5..f920b684d1 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAzureBlobCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAzureBlobCommand.cs @@ -74,7 +74,6 @@ public override async Task ExecuteAsync(CommandContext context, Name = options.Name, Target = new ShortcutTarget { - Type = "AzureBlobStorage", AzureBlobStorage = new AzureBlobStorageShortcutTarget { Location = options.TargetLocation, diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateDataverseCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateDataverseCommand.cs index e42dc86abd..7eb14ba4aa 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateDataverseCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateDataverseCommand.cs @@ -76,7 +76,6 @@ public override async Task ExecuteAsync(CommandContext context, Name = options.Name, Target = new ShortcutTarget { - Type = "Dataverse", Dataverse = new DataverseShortcutTarget { EnvironmentDomain = options.TargetEnvironmentDomain, diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateExternalDataShareCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateExternalDataShareCommand.cs deleted file mode 100644 index e44b510b8e..0000000000 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateExternalDataShareCommand.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Fabric.Mcp.Tools.OneLake.Models; -using Fabric.Mcp.Tools.OneLake.Options; -using Fabric.Mcp.Tools.OneLake.Services; -using Microsoft.Extensions.Logging; -using Microsoft.Mcp.Core.Commands; -using Microsoft.Mcp.Core.Extensions; -using Microsoft.Mcp.Core.Models.Option; - -namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; - -[CommandMetadata( - Id = "a1b2c3d4-2001-4000-8000-000000000018", - Name = "create_shortcut_external_data_share", - Title = "Create OneLake Shortcut (External Data Share Target)", - Description = """ - Create a shortcut pointing to an external data share. Only requires a - connection ID. Requires OneLake.ReadWrite.All. - """, - Destructive = false, - Idempotent = true, - LocalRequired = false, - OpenWorld = false, - ReadOnly = false, - Secret = false)] -public sealed class ShortcutCreateExternalDataShareCommand( - ILogger logger, - IOneLakeService oneLakeService) : GlobalCommand() -{ - private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); - - protected override void RegisterOptions(Command command) - { - base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); - command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); - command.Options.Add(FabricOptionDefinitions.ShortcutPath.AsRequired()); - command.Options.Add(FabricOptionDefinitions.ShortcutName.AsRequired()); - command.Options.Add(FabricOptionDefinitions.ShortcutConflictPolicy.AsOptional()); - command.Options.Add(FabricOptionDefinitions.TargetConnectionId.AsRequired()); - } - - protected override ShortcutCreateOptions BindOptions(ParseResult parseResult) - { - var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - options.Path = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutPath.Name); - options.Name = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutName.Name); - options.ConflictPolicy = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutConflictPolicy.Name); - options.TargetConnectionId = parseResult.GetValueOrDefault(FabricOptionDefinitions.TargetConnectionId.Name); - return options; - } - - public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) - { - if (!Validate(parseResult.CommandResult, context.Response).IsValid) - return context.Response; - - var options = BindOptions(parseResult); - try - { - var shortcut = new OneLakeShortcut - { - Path = options.Path, - Name = options.Name, - Target = new ShortcutTarget - { - Type = "ExternalDataShare", - ExternalDataShare = new ExternalDataShareShortcutTarget - { - ConnectionId = options.TargetConnectionId - } - } - }; - - var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId!, options.ItemId!, shortcut, options.ConflictPolicy, cancellationToken); - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating external data share shortcut. Workspace: {Workspace}, Item: {Item}.", options.WorkspaceId, options.ItemId); - HandleException(context, ex); - } - - return context.Response; - } -} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateGcsCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateGcsCommand.cs index d1bb6a619f..699ce7d33b 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateGcsCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateGcsCommand.cs @@ -74,7 +74,6 @@ public override async Task ExecuteAsync(CommandContext context, Name = options.Name, Target = new ShortcutTarget { - Type = "GoogleCloudStorage", GoogleCloudStorage = new GoogleCloudStorageShortcutTarget { Location = options.TargetLocation, diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneDriveSharePointCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneDriveSharePointCommand.cs index f597dd2383..3f5fa5f2a9 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneDriveSharePointCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneDriveSharePointCommand.cs @@ -76,7 +76,6 @@ public override async Task ExecuteAsync(CommandContext context, Name = options.Name, Target = new ShortcutTarget { - Type = "OneDriveSharePoint", OneDriveSharePoint = new OneDriveSharePointShortcutTarget { Location = options.TargetLocation, diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneLakeCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneLakeCommand.cs index 8426cb3154..77009f7edf 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneLakeCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneLakeCommand.cs @@ -43,7 +43,7 @@ protected override void RegisterOptions(Command command) command.Options.Add(FabricOptionDefinitions.ShortcutConflictPolicy.AsOptional()); command.Options.Add(FabricOptionDefinitions.TargetWorkspaceId.AsRequired()); command.Options.Add(FabricOptionDefinitions.TargetItemId.AsRequired()); - command.Options.Add(FabricOptionDefinitions.TargetPath.AsOptional()); + command.Options.Add(FabricOptionDefinitions.TargetPath.AsRequired()); command.Options.Add(FabricOptionDefinitions.TargetConnectionId.AsOptional()); } @@ -76,12 +76,12 @@ public override async Task ExecuteAsync(CommandContext context, Name = options.Name, Target = new ShortcutTarget { - Type = "OneLake", OneLake = new OneLakeShortcutTarget { WorkspaceId = options.TargetWorkspaceId, ItemId = options.TargetItemId, - Path = options.TargetPath + Path = options.TargetPath, + ConnectionId = options.TargetConnectionId } } }; diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOrUpdateCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOrUpdateCommand.cs deleted file mode 100644 index 2a0ec5bfce..0000000000 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOrUpdateCommand.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Fabric.Mcp.Tools.OneLake.Models; -using Fabric.Mcp.Tools.OneLake.Options; -using Fabric.Mcp.Tools.OneLake.Services; -using Microsoft.Extensions.Logging; -using Microsoft.Mcp.Core.Commands; -using Microsoft.Mcp.Core.Extensions; -using Microsoft.Mcp.Core.Models.Option; - -namespace Fabric.Mcp.Tools.OneLake.Commands.Shortcut; - -[CommandMetadata( - Id = "a1b2c3d4-2001-4000-8000-000000000003", - Name = "create_or_update_shortcuts", - Title = "Create or Update OneLake Shortcuts", - Description = """ - Create one or more shortcuts in a single call using the bulk create API - (POST /shortcuts/bulkCreate). Pass a JSON object with a - "createShortcutRequests" array — one entry for a single shortcut, many - entries for bulk. Use shortcut conflict policy to control behaviour - when a shortcut with the same name and path already exists: Abort - (default), CreateOrOverwrite, OverwriteOnly, or GenerateUniqueName. - Requires OneLake.ReadWrite.All. - """, - Destructive = false, - Idempotent = false, - LocalRequired = false, - OpenWorld = false, - ReadOnly = false, - Secret = false)] -public sealed class ShortcutCreateOrUpdateCommand( - ILogger logger, - IOneLakeService oneLakeService) : GlobalCommand() -{ - private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); - - protected override void RegisterOptions(Command command) - { - base.RegisterOptions(command); - command.Options.Add(FabricOptionDefinitions.WorkspaceId.AsRequired()); - command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); - command.Options.Add(FabricOptionDefinitions.ShortcutsDefinition.AsRequired()); - command.Options.Add(FabricOptionDefinitions.ShortcutConflictPolicy.AsOptional()); - } - - protected override ShortcutCreateOrUpdateOptions BindOptions(ParseResult parseResult) - { - var options = base.BindOptions(parseResult); - options.WorkspaceId = parseResult.GetValueOrDefault(FabricOptionDefinitions.WorkspaceId.Name); - options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); - options.ShortcutsDefinition = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutsDefinition.Name); - options.ShortcutConflictPolicy = parseResult.GetValueOrDefault(FabricOptionDefinitions.ShortcutConflictPolicy.Name); - return options; - } - - public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) - { - if (!Validate(parseResult.CommandResult, context.Response).IsValid) - { - return context.Response; - } - - var options = BindOptions(parseResult); - try - { - - - var result = await _oneLakeService.CreateOrUpdateShortcutsAsync(options.WorkspaceId!, options.ItemId!, options.ShortcutsDefinition!, options.ShortcutConflictPolicy, cancellationToken); - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.BulkCreateShortcutResponse); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating/updating shortcuts. Workspace: {Workspace}, Item: {Item}.", - options.WorkspaceId, options.ItemId); - HandleException(context, ex); - } - - return context.Response; - } -} - diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateS3CompatibleCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateS3CompatibleCommand.cs index 272fc5f791..d8f8895fc8 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateS3CompatibleCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateS3CompatibleCommand.cs @@ -75,7 +75,6 @@ public override async Task ExecuteAsync(CommandContext context, Name = options.Name, Target = new ShortcutTarget { - Type = "S3Compatible", S3Compatible = new S3CompatibleShortcutTarget { Location = options.TargetLocation, diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs index 22ac950c89..ec6fb5dada 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs @@ -39,6 +39,7 @@ protected override void RegisterOptions(Command command) command.Options.Add(FabricOptionDefinitions.ItemId.AsRequired()); command.Options.Add(FabricOptionDefinitions.ParentPath.AsOptional()); command.Options.Add(FabricOptionDefinitions.ContinuationToken.AsOptional()); + command.Options.Add(FabricOptionDefinitions.IncludeManaged.AsOptional()); } protected override ShortcutListOptions BindOptions(ParseResult parseResult) @@ -48,6 +49,7 @@ protected override ShortcutListOptions BindOptions(ParseResult parseResult) options.ItemId = parseResult.GetValueOrDefault(FabricOptionDefinitions.ItemId.Name); options.ParentPath = parseResult.GetValueOrDefault(FabricOptionDefinitions.ParentPath.Name); options.ContinuationToken = parseResult.GetValueOrDefault(FabricOptionDefinitions.ContinuationTokenName); + options.IncludeManaged = parseResult.GetValueOrDefault(FabricOptionDefinitions.IncludeManagedName); return options; } @@ -63,6 +65,14 @@ public override async Task ExecuteAsync(CommandContext context, { var result = await _oneLakeService.ListShortcutsAsync(options.WorkspaceId!, options.ItemId!, options.ParentPath, options.ContinuationToken, cancellationToken); + + if (!options.IncludeManaged && result.Value is not null) + { + result.Value = result.Value + .Where(s => !IsManagedShortcut(s)) + .ToList(); + } + context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.ShortcutListResponse); } catch (Exception ex) @@ -74,5 +84,20 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + /// + /// DW-managed shortcuts are created internally by Warehouse/SQL endpoints and can number + /// in the hundreds of thousands, drowning user-visible shortcuts. They typically reside + /// under well-known managed paths (e.g. "Tables/dbo.*" with OneLake-internal targets). + /// + private static bool IsManagedShortcut(OneLakeShortcut shortcut) + { + // Managed shortcuts are internal OneLake-to-OneLake references under DW table paths. + // Heuristic: shortcuts whose path starts with "Tables/" and target is OneLake are DW-managed. + if (shortcut.Path is null || shortcut.Target?.OneLake is null) + return false; + + return shortcut.Path.StartsWith("Tables/", StringComparison.OrdinalIgnoreCase); + } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/FabricOneLakeSetup.cs b/tools/Fabric.Mcp.Tools.OneLake/src/FabricOneLakeSetup.cs index f1e474318a..efd388ff17 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/FabricOneLakeSetup.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/FabricOneLakeSetup.cs @@ -64,7 +64,6 @@ public void ConfigureServices(IServiceCollection services) // Register shortcut commands services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -75,7 +74,6 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); // Register settings commands services.AddSingleton(); @@ -122,7 +120,6 @@ Microsoft Fabric OneLake Operations - Manage and interact with OneLake data lake // Register shortcut commands fabricOneLake.AddCommand(serviceProvider); fabricOneLake.AddCommand(serviceProvider); - fabricOneLake.AddCommand(serviceProvider); fabricOneLake.AddCommand(serviceProvider); fabricOneLake.AddCommand(serviceProvider); fabricOneLake.AddCommand(serviceProvider); @@ -133,7 +130,6 @@ Microsoft Fabric OneLake Operations - Manage and interact with OneLake data lake fabricOneLake.AddCommand(serviceProvider); fabricOneLake.AddCommand(serviceProvider); fabricOneLake.AddCommand(serviceProvider); - fabricOneLake.AddCommand(serviceProvider); // Register settings commands fabricOneLake.AddCommand(serviceProvider); diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Models/ShortcutModels.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Models/ShortcutModels.cs index 0e9692dee1..b563210de8 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Models/ShortcutModels.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Models/ShortcutModels.cs @@ -25,7 +25,11 @@ public class OneLakeShortcut /// public class ShortcutTarget { + /// + /// Discriminator returned by GET responses. Must NOT be sent on create/update requests. + /// [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Type { get; set; } [JsonPropertyName("oneLake")] @@ -69,6 +73,10 @@ public class OneLakeShortcutTarget [JsonPropertyName("path")] public string? Path { get; set; } + + [JsonPropertyName("connectionId")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ConnectionId { get; set; } } /// diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs index 0fb9bb4b13..d17ecc0728 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/FabricOptionDefinitions.cs @@ -270,18 +270,18 @@ Example with GUIDs (use when you already have the object ID): }; public const string ShortcutConflictPolicyName = "shortcut-conflict-policy"; - public static readonly Option ShortcutConflictPolicy = new($"--{ShortcutConflictPolicyName}") - { - Description = "Action when a shortcut with the same name and path already exists. One of: Abort (default), CreateOrOverwrite, OverwriteOnly, GenerateUniqueName.", - Required = false - }; + public static readonly Option ShortcutConflictPolicy = CreateShortcutConflictPolicyOption(); - public const string ShortcutsDefinitionName = "shortcuts"; - public static readonly Option ShortcutsDefinition = new($"--{ShortcutsDefinitionName}") + private static Option CreateShortcutConflictPolicyOption() { - Description = "JSON array of shortcut definitions to create or update.", - Required = true - }; + var option = new Option($"--{ShortcutConflictPolicyName}") + { + Description = "Action when a shortcut with the same name and path already exists. Default: Abort.", + Required = false + }; + option.AcceptOnlyFromAmong("Abort", "CreateOrOverwrite", "OverwriteOnly", "GenerateUniqueName"); + return option; + } // Settings options // Diagnostics flat options @@ -398,4 +398,11 @@ Example with GUIDs (use when you already have the object ID): Description = "Whether to update Fabric item sensitivity from OneDrive/SharePoint. Default: false.", Required = false }; + + public const string IncludeManagedName = "include-managed"; + public static readonly Option IncludeManaged = new($"--{IncludeManagedName}") + { + Description = "Include DW-managed shortcuts in the results. Default: false (managed shortcuts are hidden to avoid overwhelming output).", + Required = false + }; } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOrUpdateOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOrUpdateOptions.cs deleted file mode 100644 index 632c971ec2..0000000000 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutCreateOrUpdateOptions.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.Mcp.Core.Options; - -namespace Fabric.Mcp.Tools.OneLake.Options; - -public sealed class ShortcutCreateOrUpdateOptions : GlobalOptions -{ - public string? WorkspaceId { get; set; } - public string? ItemId { get; set; } - public string? ShortcutsDefinition { get; set; } - public string? ShortcutConflictPolicy { get; set; } -} - diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutListOptions.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutListOptions.cs index 2be48d5631..53e3f4f9c1 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutListOptions.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Options/ShortcutListOptions.cs @@ -11,5 +11,6 @@ public sealed class ShortcutListOptions : GlobalOptions public string? ItemId { get; set; } public string? ParentPath { get; set; } public string? ContinuationToken { get; set; } + public bool IncludeManaged { get; set; } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateCommandVariantsTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateCommandVariantsTests.cs index 327b302933..2280f22f8d 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateCommandVariantsTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateCommandVariantsTests.cs @@ -36,7 +36,6 @@ await service.Received(1).CreateShortcutAsync( Arg.Is(shortcut => shortcut.Path == "Files/landing" && shortcut.Name == "shortcut1" && - shortcut.Target!.Type == "OneLake" && shortcut.Target.OneLake!.WorkspaceId == "target-ws" && shortcut.Target.OneLake.ItemId == "target-item" && shortcut.Target.OneLake.Path == "Files/data"), @@ -64,7 +63,6 @@ await service.Received(1).CreateShortcutAsync( "ws1", "item1", Arg.Is(shortcut => - shortcut.Target!.Type == "AdlsGen2" && shortcut.Target.AdlsGen2!.Location == "https://account.dfs.core.windows.net/container" && shortcut.Target.AdlsGen2.Subpath == "/folder" && shortcut.Target.AdlsGen2.ConnectionId == "connection-1"), @@ -92,7 +90,6 @@ await service.Received(1).CreateShortcutAsync( "ws1", "item1", Arg.Is(shortcut => - shortcut.Target!.Type == "AmazonS3" && shortcut.Target.AmazonS3!.Location == "https://bucket.s3.us-west-2.amazonaws.com" && shortcut.Target.AmazonS3.Subpath == "/folder" && shortcut.Target.AmazonS3.ConnectionId == "connection-1"), @@ -120,7 +117,6 @@ await service.Received(1).CreateShortcutAsync( "ws1", "item1", Arg.Is(shortcut => - shortcut.Target!.Type == "AzureBlobStorage" && shortcut.Target.AzureBlobStorage!.Location == "https://account.blob.core.windows.net/container" && shortcut.Target.AzureBlobStorage.Subpath == "/folder" && shortcut.Target.AzureBlobStorage.ConnectionId == "connection-1"), @@ -148,7 +144,6 @@ await service.Received(1).CreateShortcutAsync( "ws1", "item1", Arg.Is(shortcut => - shortcut.Target!.Type == "GoogleCloudStorage" && shortcut.Target.GoogleCloudStorage!.Location == "https://bucket.storage.googleapis.com" && shortcut.Target.GoogleCloudStorage.Subpath == "/folder" && shortcut.Target.GoogleCloudStorage.ConnectionId == "connection-1"), @@ -177,7 +172,6 @@ await service.Received(1).CreateShortcutAsync( "ws1", "item1", Arg.Is(shortcut => - shortcut.Target!.Type == "S3Compatible" && shortcut.Target.S3Compatible!.Location == "https://s3endpoint.contoso.com" && shortcut.Target.S3Compatible.Subpath == "/folder" && shortcut.Target.S3Compatible.ConnectionId == "connection-1" && @@ -207,7 +201,6 @@ await service.Received(1).CreateShortcutAsync( "ws1", "item1", Arg.Is(shortcut => - shortcut.Target!.Type == "Dataverse" && shortcut.Target.Dataverse!.EnvironmentDomain == "https://org.crm.dynamics.com" && shortcut.Target.Dataverse.ConnectionId == "connection-1" && shortcut.Target.Dataverse.DeltaLakeFolder == "Tables/account"), @@ -236,7 +229,6 @@ await service.Received(1).CreateShortcutAsync( "ws1", "item1", Arg.Is(shortcut => - shortcut.Target!.Type == "OneDriveSharePoint" && shortcut.Target.OneDriveSharePoint!.Location == "https://contoso.sharepoint.com/sites/site" && shortcut.Target.OneDriveSharePoint.Subpath == "/Documents" && shortcut.Target.OneDriveSharePoint.ConnectionId == "connection-1" && @@ -245,30 +237,6 @@ await service.Received(1).CreateShortcutAsync( Arg.Any()); } - [Fact] - public async Task ExecuteAsync_ExternalDataShareCommand_MapsTargetValues() - { - var service = CreateShortcutService(); - var command = new ShortcutCreateExternalDataShareCommand(Substitute.For>(), service); - - var response = await ExecuteAsync(command, - "--workspace-id", "ws1", - "--item-id", "item1", - "--shortcut-path", "Files/landing", - "--shortcut-name", "shortcut1", - "--target-connection-id", "connection-1"); - - Assert.Equal(HttpStatusCode.OK, response.Status); - await service.Received(1).CreateShortcutAsync( - "ws1", - "item1", - Arg.Is(shortcut => - shortcut.Target!.Type == "ExternalDataShare" && - shortcut.Target.ExternalDataShare!.ConnectionId == "connection-1"), - null, - Arg.Any()); - } - [Fact] public async Task ExecuteAsync_MissingRequiredArguments_ReturnsBadRequest() { diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateOrUpdateCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateOrUpdateCommandTests.cs deleted file mode 100644 index 958a3dcd20..0000000000 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Commands/Shortcut/ShortcutCreateOrUpdateCommandTests.cs +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using Fabric.Mcp.Tools.OneLake.Commands.Shortcut; -using Fabric.Mcp.Tools.OneLake.Models; -using Fabric.Mcp.Tools.OneLake.Services; -using Microsoft.Mcp.Tests.Client; -using NSubstitute; -using NSubstitute.ExceptionExtensions; - -namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Shortcut; - -public class ShortcutCreateOrUpdateCommandTests : CommandUnitTestsBase -{ - private const string ValidJson = "{\"createShortcutRequests\":[{\"path\":\"Files/folder\",\"name\":\"sc1\",\"target\":{\"oneLake\":{\"workspaceId\":\"ws2\",\"itemId\":\"item2\",\"path\":\"Tables\"}}}]}"; - - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - Assert.Equal("create_or_update_shortcuts", Command.Name); - Assert.Equal("Create or Update OneLake Shortcuts", Command.Title); - Assert.Contains("bulk create", Command.Description, StringComparison.OrdinalIgnoreCase); - Assert.False(Command.Metadata.ReadOnly); - Assert.False(Command.Metadata.Destructive); - Assert.False(Command.Metadata.Idempotent); - } - - [Fact] - public void GetCommand_ReturnsValidCommand() - { - Assert.Equal("create_or_update_shortcuts", CommandDefinition.Name); - Assert.NotNull(CommandDefinition.Description); - Assert.NotEmpty(CommandDefinition.Options); - } - - [Fact] - public void Constructor_ThrowsArgumentNullException_WhenLoggerIsNull() - { - Assert.Throws(() => new ShortcutCreateOrUpdateCommand(null!, Service)); - } - - [Fact] - public void Constructor_ThrowsArgumentNullException_WhenOneLakeServiceIsNull() - { - Assert.Throws(() => new ShortcutCreateOrUpdateCommand(Logger, null!)); - } - - [Fact] - public void Metadata_HasCorrectProperties() - { - var metadata = Command.Metadata; - - Assert.False(metadata.Destructive); - Assert.False(metadata.Idempotent); - Assert.False(metadata.LocalRequired); - Assert.False(metadata.OpenWorld); - Assert.False(metadata.ReadOnly); - Assert.False(metadata.Secret); - } - - [Theory] - [InlineData("--workspace-id ws1 --item-id item1", true)] - [InlineData("--item-id item1", false)] // missing workspace - [InlineData("--workspace-id ws1", false)] // missing item - [InlineData("", false)] - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - if (shouldSucceed) - { - Service.CreateOrUpdateShortcutsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(new BulkCreateShortcutResponse()); - } - - var fullArgs = string.IsNullOrWhiteSpace(args) - ? $"--shortcuts {ValidJson}" - : $"{args} --shortcuts {ValidJson}"; - - var response = await ExecuteCommandAsync(fullArgs); - - Assert.NotNull(response); - if (shouldSucceed) - Assert.Equal(HttpStatusCode.OK, response.Status); - else - Assert.Equal(HttpStatusCode.BadRequest, response.Status); - } - - [Fact] - public async Task ExecuteAsync_SingleShortcut_CallsBulkCreateOnce() - { - var expected = new BulkCreateShortcutResponse - { - Value = [new CreateShortcutResponse { Status = "Succeeded" }] - }; - - Service.CreateOrUpdateShortcutsAsync("ws1", "item1", Arg.Any(), null, Arg.Any()) - .Returns(expected); - - var response = await ExecuteCommandAsync( - "--workspace-id", "ws1", - "--item-id", "item1", - "--shortcuts", ValidJson); - - var result = ValidateAndDeserializeResponse(response, OneLakeJsonContext.Default.BulkCreateShortcutResponse); - Assert.Single(result.Value!); - Assert.Equal("Succeeded", result.Value![0].Status); - await Service.Received(1).CreateOrUpdateShortcutsAsync("ws1", "item1", Arg.Any(), null, Arg.Any()); - } - - [Fact] - public async Task ExecuteAsync_WithConflictPolicy_PassesPolicyToService() - { - Service.CreateOrUpdateShortcutsAsync("ws1", "item1", Arg.Any(), "CreateOrOverwrite", Arg.Any()) - .Returns(new BulkCreateShortcutResponse { Value = [] }); - - var response = await ExecuteCommandAsync( - "--workspace-id", "ws1", - "--item-id", "item1", - "--shortcuts", ValidJson, - "--shortcut-conflict-policy", "CreateOrOverwrite"); - - Assert.Equal(HttpStatusCode.OK, response.Status); - await Service.Received(1).CreateOrUpdateShortcutsAsync("ws1", "item1", Arg.Any(), "CreateOrOverwrite", Arg.Any()); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - Service.CreateOrUpdateShortcutsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .ThrowsAsync(new HttpRequestException("Conflict")); - - var response = await ExecuteCommandAsync( - "--workspace-id", "ws1", - "--item-id", "item1", - "--shortcuts", ValidJson); - - Assert.NotNull(response); - Assert.NotEqual(HttpStatusCode.OK, response.Status); - } -} - diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/FabricOneLakeSetupTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/FabricOneLakeSetupTests.cs index ed48b46196..59bbbbd5f3 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/FabricOneLakeSetupTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/FabricOneLakeSetupTests.cs @@ -65,7 +65,6 @@ public void RegisterCommands_RegistersAllOneLakeCommands() // Shortcut commands Assert.True(rootGroup.Commands.ContainsKey("list_shortcuts"), "Should have list_shortcuts command"); Assert.True(rootGroup.Commands.ContainsKey("get_shortcut"), "Should have get_shortcut command"); - Assert.True(rootGroup.Commands.ContainsKey("create_or_update_shortcuts"), "Should have create_or_update_shortcuts command"); Assert.True(rootGroup.Commands.ContainsKey("delete_shortcut"), "Should have delete_shortcut command"); Assert.True(rootGroup.Commands.ContainsKey("reset_shortcut_cache"), "Should have reset_shortcut_cache command"); Assert.True(rootGroup.Commands.ContainsKey("create_shortcut_onelake"), "Should have create_shortcut_onelake command"); @@ -76,6 +75,5 @@ public void RegisterCommands_RegistersAllOneLakeCommands() Assert.True(rootGroup.Commands.ContainsKey("create_shortcut_s3_compatible"), "Should have create_shortcut_s3_compatible command"); Assert.True(rootGroup.Commands.ContainsKey("create_shortcut_dataverse"), "Should have create_shortcut_dataverse command"); Assert.True(rootGroup.Commands.ContainsKey("create_shortcut_onedrive_sharepoint"), "Should have create_shortcut_onedrive_sharepoint command"); - Assert.True(rootGroup.Commands.ContainsKey("create_shortcut_external_data_share"), "Should have create_shortcut_external_data_share command"); } } From 321bb3331a057d24d25f43b68344d7da63d41a74 Mon Sep 17 00:00:00 2001 From: Srinivas Thatipamula Date: Tue, 16 Jun 2026 07:35:34 -0700 Subject: [PATCH 15/15] Add missing namespace to BlobGetCommand and fix test using Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Commands/File/BlobGetCommand.cs | 2 ++ .../Commands/BlobGetCommandTests.cs | 1 + 2 files changed, 3 insertions(+) diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/File/BlobGetCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/File/BlobGetCommand.cs index 1d0a333cfe..994eee7381 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/File/BlobGetCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/File/BlobGetCommand.cs @@ -14,6 +14,8 @@ using Microsoft.Mcp.Core.Models.Option; using Microsoft.Mcp.Core.Options; +namespace Fabric.Mcp.Tools.OneLake.Commands.File; + [CommandMetadata( Id = "75d6cb4c-4e81-4e69-a4ec-eca53a7dacd9", Name = "download_file", diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/BlobGetCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/BlobGetCommandTests.cs index 9935fb1ff7..b5c8920e10 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/BlobGetCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/BlobGetCommandTests.cs @@ -3,6 +3,7 @@ using System.Net; using System.Text; +using Fabric.Mcp.Tools.OneLake.Commands.File; using Fabric.Mcp.Tools.OneLake.Models; using Fabric.Mcp.Tools.OneLake.Services; using Microsoft.Extensions.DependencyInjection;