diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 9e03ed4f18..395ee5b1e3 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -241,6 +241,12 @@
# ServiceLabel: %tools-Deploy
# ServiceOwners: @qianwens @xfz11 @wchigit
+# PRLabel: %tools-DeviceProvisioning
+/tools/Azure.Mcp.Tools.DeviceProvisioning/ @microsoft/azure-mcp
+
+# ServiceLabel: %tools-DeviceProvisioning
+# ServiceOwners: @microsoft/azure-mcp
+
# PRLabel: %tools-DeviceRegistry
/tools/Azure.Mcp.Tools.DeviceRegistry/ @microsoft/azure-mcp
diff --git a/.vscode/cspell.json b/.vscode/cspell.json
index d14be171d1..ddf39b26ae 100644
--- a/.vscode/cspell.json
+++ b/.vscode/cspell.json
@@ -304,6 +304,7 @@
"deploymentid",
"deregistering",
"devbox",
+ "deviceprovisioning",
"deviceregistry",
"dfff",
"disasterrecovery",
diff --git a/Microsoft.Mcp.slnx b/Microsoft.Mcp.slnx
index b45b6a3212..c1b06b555f 100644
--- a/Microsoft.Mcp.slnx
+++ b/Microsoft.Mcp.slnx
@@ -260,6 +260,13 @@
+
+
+
+
+
+
+
diff --git a/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx b/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx
index 25f381e051..05c2e22783 100644
--- a/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx
+++ b/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx
@@ -187,6 +187,13 @@
+
+
+
+
+
+
+
diff --git a/servers/Azure.Mcp.Server/README.md b/servers/Azure.Mcp.Server/README.md
index a7720a29f5..94b75af836 100644
--- a/servers/Azure.Mcp.Server/README.md
+++ b/servers/Azure.Mcp.Server/README.md
@@ -1186,6 +1186,12 @@ For customer-hosted ADME instances, pass the resource application ID or App ID U
* "Generate insights about my Azure environment to help me plan a new data analytics platform"
* "What insights can you derive about my subscription to help me plan a containerized microservices workload on AKS?"
+### 🔧 Azure Device Provisioning Service
+
+* "Show me Device Provisioning Service 'my-dps' in resource group 'my-resource-group' of my subscription 'my-subscription'"
+* "Get the ID scope, allocation policy, endpoints, and linked IoT Hubs for DPS instance 'my-dps'"
+* "Show the configuration of DPS instance 'my-dps' without returning connection strings or keys"
+
### 🌐 Azure IoT Hub
* "Show me IoT Hub 'my-iot-hub' in resource group 'my-resource-group' of my subscription 'my-subscription'"
@@ -1438,6 +1444,7 @@ The Azure MCP Server provides tools for interacting with **45+ Azure service are
- 🧮 **Azure Data Explorer** - Analytics queries and KQL
- 🐬 **Azure Database for MySQL** - MySQL database management
- 🐘 **Azure Database for PostgreSQL** - PostgreSQL database management
+- 🔧 **Azure Device Provisioning Service** - Device Provisioning Service resource configuration and linked IoT Hub metadata
- 🏭 **Azure Device Registry** - Device Registry namespace management
- 📊 **Azure Event Grid** - Event routing and management
- ⚡ **Azure Event Hubs** - Namespace, event hub, and consumer group management
diff --git a/servers/Azure.Mcp.Server/changelog-entries/khalil-device-provisioning-service-get.yaml b/servers/Azure.Mcp.Server/changelog-entries/khalil-device-provisioning-service-get.yaml
new file mode 100644
index 0000000000..e0173fcf46
--- /dev/null
+++ b/servers/Azure.Mcp.Server/changelog-entries/khalil-device-provisioning-service-get.yaml
@@ -0,0 +1,3 @@
+changes:
+ - section: "Features Added"
+ description: "Added a read-only Azure Device Provisioning Service tool for retrieving service configuration and linked IoT Hub metadata without exposing connection strings or keys."
diff --git a/servers/Azure.Mcp.Server/docs/azmcp-commands.md b/servers/Azure.Mcp.Server/docs/azmcp-commands.md
index 16c4853727..05eb1512ab 100644
--- a/servers/Azure.Mcp.Server/docs/azmcp-commands.md
+++ b/servers/Azure.Mcp.Server/docs/azmcp-commands.md
@@ -3161,6 +3161,17 @@ azmcp insights get --scope tenant \
--nocache
```
+### Azure Device Provisioning Service Operations
+
+```bash
+# Get Azure Device Provisioning Service details by service name in a specific resource group.
+# The response excludes linked-hub connection strings, authorization policies, and keys.
+# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp deviceprovisioning service get --subscription \
+ --resource-group \
+ --service
+```
+
### Azure IoT Hub Operations
```bash
diff --git a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
index 67e6d52670..72123d6909 100644
--- a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
+++ b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
@@ -757,6 +757,14 @@ The `Interaction` column describes whether a prompt can invoke its tool immediat
| insights_get | Generate insights about my Azure environment to help me plan a new data analytics platform | none |
| insights_get | What insights can you derive about my subscription to help me plan a containerized microservices workload on AKS? | none |
+## Azure Device Provisioning Service
+
+| Tool Name | Test Prompt | Interaction |
+|:----------|:------------|:------------|
+| deviceprovisioning_service_get | Get details for Device Provisioning Service in resource group | none |
+| deviceprovisioning_service_get | Show the configuration of DPS instance in subscription | none |
+| deviceprovisioning_service_get | Retrieve the ID scope, allocation policy, endpoints, and linked IoT Hubs for DPS instance | none |
+
## Azure IoT Hub
| Tool Name | Test Prompt | Interaction |
diff --git a/servers/Azure.Mcp.Server/src/Program.cs b/servers/Azure.Mcp.Server/src/Program.cs
index 3a5edb6e52..3d2526269c 100644
--- a/servers/Azure.Mcp.Server/src/Program.cs
+++ b/servers/Azure.Mcp.Server/src/Program.cs
@@ -194,6 +194,7 @@ internal static IAreaSetup[] RegisterAreas()
new Azure.Mcp.Tools.AzureTerraform.AzureTerraformSetup(),
new Azure.Mcp.Tools.AzureTerraformBestPractices.AzureTerraformBestPracticesSetup(),
new Azure.Mcp.Tools.Deploy.DeploySetup(),
+ new Azure.Mcp.Tools.DeviceProvisioning.DeviceProvisioningSetup(),
new Azure.Mcp.Tools.DeviceRegistry.DeviceRegistrySetup(),
new Azure.Mcp.Tools.EventGrid.EventGridSetup(),
new Azure.Mcp.Tools.Acr.AcrSetup(),
diff --git a/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json b/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json
index 3933ccb6d3..4f8e15b9fc 100644
--- a/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json
+++ b/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json
@@ -2944,6 +2944,39 @@
"eventhubs_eventhub_consumergroup_get"
]
},
+ {
+ "name": "get_azure_device_provisioning_service",
+ "description": "Get details about an Azure Device Provisioning Service instance, including service state, endpoints, ID scope, allocation policy, network access, data residency, and linked IoT Hub metadata. Connection strings, authorization policies, and keys are not returned.",
+ "toolMetadata": {
+ "destructive": {
+ "value": false,
+ "description": "This tool only retrieves information about an Azure Device Provisioning Service instance without deleting or modifying existing resources."
+ },
+ "idempotent": {
+ "value": true,
+ "description": "Calling this tool multiple times with the same parameters returns the current resource details without causing side effects."
+ },
+ "openWorld": {
+ "value": false,
+ "description": "This tool interacts only with Azure Resource Manager."
+ },
+ "readOnly": {
+ "value": true,
+ "description": "This tool only reads Azure Device Provisioning Service resource information."
+ },
+ "secret": {
+ "value": false,
+ "description": "This tool excludes connection strings, authorization policies, and keys from its response."
+ },
+ "localRequired": {
+ "value": false,
+ "description": "This tool is available in both local and remote server modes."
+ }
+ },
+ "mappedToolList": [
+ "deviceprovisioning_service_get"
+ ]
+ },
{
"name": "get_azure_iot_hub",
"description": "Get details about an Azure IoT Hub.",
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/cspell.yaml b/tools/Azure.Mcp.Tools.DeviceProvisioning/cspell.yaml
new file mode 100644
index 0000000000..bcae32a40b
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/cspell.yaml
@@ -0,0 +1,9 @@
+# Spell-check overrides for this toolset/server.
+# Inherits the global config at the repo root.
+version: "0.2"
+import:
+ - ../../.vscode/cspell.json
+words:
+ - idscope
+ - iothubs
+ - provisioningserviceowner
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Azure.Mcp.Tools.DeviceProvisioning.csproj b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Azure.Mcp.Tools.DeviceProvisioning.csproj
new file mode 100644
index 0000000000..ca6bc32dd3
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Azure.Mcp.Tools.DeviceProvisioning.csproj
@@ -0,0 +1,17 @@
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Commands/DeviceProvisioningJsonContext.cs b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Commands/DeviceProvisioningJsonContext.cs
new file mode 100644
index 0000000000..b1f3cb4613
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Commands/DeviceProvisioningJsonContext.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json.Serialization;
+using Azure.Mcp.Tools.DeviceProvisioning.Commands.Service;
+using Azure.Mcp.Tools.DeviceProvisioning.Models;
+
+namespace Azure.Mcp.Tools.DeviceProvisioning.Commands;
+
+[JsonSerializable(typeof(DeviceProvisioningProperties))]
+[JsonSerializable(typeof(DeviceProvisioningServiceDescription))]
+[JsonSerializable(typeof(LinkedIoTHubDescription))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(DeviceProvisioningServiceGetCommand.DeviceProvisioningServiceGetCommandResult))]
+[JsonSourceGenerationOptions(
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
+internal sealed partial class DeviceProvisioningJsonContext : JsonSerializerContext
+{
+}
+
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Commands/Service/DeviceProvisioningServiceGetCommand.cs b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Commands/Service/DeviceProvisioningServiceGetCommand.cs
new file mode 100644
index 0000000000..37bd66f2af
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Commands/Service/DeviceProvisioningServiceGetCommand.cs
@@ -0,0 +1,114 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Azure.Mcp.Core.Commands.Subscription;
+using Azure.Mcp.Core.Services.Azure.Subscription;
+using Azure.Mcp.Tools.DeviceProvisioning.Models;
+using Azure.Mcp.Tools.DeviceProvisioning.Options.Service;
+using Azure.Mcp.Tools.DeviceProvisioning.Services;
+using Microsoft.Extensions.Logging;
+using Microsoft.Mcp.Core.Commands;
+using Microsoft.Mcp.Core.Models.Command;
+
+namespace Azure.Mcp.Tools.DeviceProvisioning.Commands.Service;
+
+[CommandMetadata(
+ Id = "3bf66226-33b8-438b-9f71-3abe62a6bf6c",
+ Name = "get",
+ Title = "Get Device Provisioning Service",
+ Description = """
+ Get an Azure Device Provisioning Service instance by name in a resource group.
+ Returns resource identity, SKU, service state, endpoints, ID scope, allocation policy,
+ public network access, data residency, and linked IoT Hub metadata.
+ Connection strings, authorization policies, and keys are not returned.
+ """,
+ OperationPlane = ToolOperationPlane.Control,
+ Destructive = false,
+ Idempotent = true,
+ OpenWorld = false,
+ ReadOnly = true,
+ Secret = false,
+ LocalRequired = false)]
+public sealed class DeviceProvisioningServiceGetCommand(
+ ILogger logger,
+ IDeviceProvisioningService service,
+ ISubscriptionResolver subscriptionResolver)
+ : SubscriptionCommand(subscriptionResolver)
+{
+ private readonly ILogger _logger = logger;
+ private readonly IDeviceProvisioningService _service = service;
+
+ public override void ValidateOptions(
+ DeviceProvisioningServiceGetOptions options,
+ ValidationResult validationResult)
+ {
+ base.ValidateOptions(options, validationResult);
+
+ if (!IsValidServiceName(options.Service))
+ {
+ validationResult.Errors.Add(
+ "--service must be 3-64 characters long, contain only letters, numbers, or hyphens, and start and end with a letter or number.");
+ }
+ }
+
+ public override async Task ExecuteAsync(
+ CommandContext context,
+ DeviceProvisioningServiceGetOptions options,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ var provisioningService = await _service.GetService(
+ options.Service,
+ options.ResourceGroup,
+ options.Subscription!,
+ options.Tenant,
+ cancellationToken);
+
+ context.Response.Results = ResponseResult.Create(
+ new DeviceProvisioningServiceGetCommandResult(
+ provisioningService,
+ AreResultsTruncated: false),
+ DeviceProvisioningJsonContext.Default.DeviceProvisioningServiceGetCommandResult);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(
+ ex,
+ "Error getting Device Provisioning Service '{ServiceName}' in resource group '{ResourceGroup}' and subscription '{Subscription}'.",
+ options.Service,
+ options.ResourceGroup,
+ options.Subscription);
+ HandleException(context, ex);
+ }
+
+ return context.Response;
+ }
+
+ public record DeviceProvisioningServiceGetCommandResult(
+ DeviceProvisioningServiceDescription DeviceProvisioningService,
+ bool AreResultsTruncated);
+
+ private static bool IsValidServiceName(string value)
+ {
+ if (value.Length is < 3 or > 64 ||
+ value[0] == '-' ||
+ value[^1] == '-')
+ {
+ return false;
+ }
+
+ foreach (var ch in value)
+ {
+ var isAlphaNumeric = (ch >= 'a' && ch <= 'z') ||
+ (ch >= 'A' && ch <= 'Z') ||
+ (ch >= '0' && ch <= '9');
+ if (!isAlphaNumeric && ch != '-')
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/src/DeviceProvisioningSetup.cs b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/DeviceProvisioningSetup.cs
new file mode 100644
index 0000000000..223519366a
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/DeviceProvisioningSetup.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Azure.Mcp.Tools.DeviceProvisioning.Commands.Service;
+using Azure.Mcp.Tools.DeviceProvisioning.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Mcp.Core.Areas;
+using Microsoft.Mcp.Core.Commands;
+
+namespace Azure.Mcp.Tools.DeviceProvisioning;
+
+public class DeviceProvisioningSetup : IAreaSetup
+{
+ public string Name => "deviceprovisioning";
+
+ public string Title => "Manage Azure Device Provisioning Service";
+
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddSingleton();
+ services.AddSingleton();
+ }
+
+ public CommandGroup RegisterCommands(IServiceProvider serviceProvider)
+ {
+ var deviceProvisioning = new CommandGroup(
+ Name,
+ "Azure Device Provisioning Service operations.",
+ Title);
+
+ var service = new CommandGroup(
+ "service",
+ "Azure Device Provisioning Service resource operations.");
+ deviceProvisioning.AddSubGroup(service);
+ service.AddCommand(serviceProvider);
+
+ return deviceProvisioning;
+ }
+}
+
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Models/DeviceProvisioningProperties.cs b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Models/DeviceProvisioningProperties.cs
new file mode 100644
index 0000000000..7f33a78156
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Models/DeviceProvisioningProperties.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.Mcp.Tools.DeviceProvisioning.Models;
+
+public class DeviceProvisioningProperties
+{
+ public string? State { get; set; }
+
+ public string? ProvisioningState { get; set; }
+
+ public string? ServiceOperationsHostName { get; set; }
+
+ public string? DeviceProvisioningHostName { get; set; }
+
+ public string? IdScope { get; set; }
+
+ public string? AllocationPolicy { get; set; }
+
+ public string? PublicNetworkAccess { get; set; }
+
+ public bool? EnableDataResidency { get; set; }
+
+ public List? IotHubs { get; set; }
+}
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Models/DeviceProvisioningServiceDescription.cs b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Models/DeviceProvisioningServiceDescription.cs
new file mode 100644
index 0000000000..719d5e0810
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Models/DeviceProvisioningServiceDescription.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.Mcp.Tools.DeviceProvisioning.Models;
+
+public record DeviceProvisioningServiceDescription(
+ string Id,
+ string Name,
+ string Location,
+ string ResourceGroup,
+ string SubscriptionId,
+ string Sku,
+ long Capacity,
+ string State,
+ string ProvisioningState,
+ string ServiceOperationsHostName,
+ string DeviceProvisioningHostName,
+ string IdScope,
+ string AllocationPolicy,
+ string PublicNetworkAccess,
+ bool? EnableDataResidency,
+ List IotHubs);
+
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Models/LinkedIoTHubDescription.cs b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Models/LinkedIoTHubDescription.cs
new file mode 100644
index 0000000000..4a1b51307e
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Models/LinkedIoTHubDescription.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.Mcp.Tools.DeviceProvisioning.Models;
+
+public record LinkedIoTHubDescription(
+ string Name,
+ string Location,
+ bool? ApplyAllocationPolicy,
+ int? AllocationWeight);
+
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Models/LinkedIoTHubProperties.cs b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Models/LinkedIoTHubProperties.cs
new file mode 100644
index 0000000000..0a18551502
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Models/LinkedIoTHubProperties.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.Mcp.Tools.DeviceProvisioning.Models;
+
+public class LinkedIoTHubProperties
+{
+ public string? Name { get; set; }
+
+ public string? Location { get; set; }
+
+ public bool? ApplyAllocationPolicy { get; set; }
+
+ public int? AllocationWeight { get; set; }
+}
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Options/Service/DeviceProvisioningServiceGetOptions.cs b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Options/Service/DeviceProvisioningServiceGetOptions.cs
new file mode 100644
index 0000000000..e0c2a0ff1c
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Options/Service/DeviceProvisioningServiceGetOptions.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Azure.Mcp.Core.Options;
+using Microsoft.Mcp.Core.Options;
+
+namespace Azure.Mcp.Tools.DeviceProvisioning.Options.Service;
+
+public sealed class DeviceProvisioningServiceGetOptions : ISubscriptionOption
+{
+ [Option(Description = "The name of the Azure Device Provisioning Service instance.")]
+ public required string Service { get; set; }
+
+ [Option(Description = OptionDescriptions.ResourceGroup)]
+ public required string ResourceGroup { get; set; }
+
+ [Option(Description = OptionDescriptions.Subscription)]
+ public string? Subscription { get; set; }
+
+ [Option(Description = OptionDescriptions.Tenant)]
+ public string? Tenant { get; set; }
+}
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Services/DeviceProvisioningService.cs b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Services/DeviceProvisioningService.cs
new file mode 100644
index 0000000000..3aa5cffefe
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Services/DeviceProvisioningService.cs
@@ -0,0 +1,94 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Azure.Core;
+using Azure.Mcp.Core.Services.Azure;
+using Azure.Mcp.Tools.DeviceProvisioning.Commands;
+using Azure.Mcp.Tools.DeviceProvisioning.Models;
+using Azure.ResourceManager.Resources;
+using Microsoft.Extensions.Logging;
+
+namespace Azure.Mcp.Tools.DeviceProvisioning.Services;
+
+public class DeviceProvisioningService(
+ IAzureService azureService,
+ ILogger logger)
+ : BaseAzureService(azureService), IDeviceProvisioningService
+{
+ private readonly ILogger _logger =
+ logger ?? throw new ArgumentNullException(nameof(logger));
+
+ public async Task GetService(
+ string serviceName,
+ string resourceGroup,
+ string subscription,
+ string? tenant = null,
+ CancellationToken cancellationToken = default)
+ {
+ ValidateRequiredParameters(
+ (nameof(subscription), subscription),
+ (nameof(resourceGroup), resourceGroup),
+ (nameof(serviceName), serviceName));
+
+ try
+ {
+ var subscriptionResource = await AzureService.GetSubscription(
+ subscription,
+ tenant,
+ cancellationToken: cancellationToken);
+ var armClient = await CreateArmClientAsync(
+ tenant,
+ cancellationToken: cancellationToken);
+ var serviceResourceId = new ResourceIdentifier(
+ $"/subscriptions/{subscriptionResource.Data.SubscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Devices/provisioningServices/{serviceName}");
+ var service = await armClient
+ .GetGenericResource(serviceResourceId)
+ .GetAsync(cancellationToken);
+
+ return ConvertToDescription(service.Value.Data);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(
+ ex,
+ "Error retrieving Device Provisioning Service '{ServiceName}' in resource group '{ResourceGroup}' and subscription '{Subscription}'.",
+ serviceName,
+ resourceGroup,
+ subscription);
+ throw;
+ }
+ }
+
+ private static DeviceProvisioningServiceDescription ConvertToDescription(
+ GenericResourceData service)
+ {
+ var properties = service.Properties?.ToObjectFromJson(
+ DeviceProvisioningJsonContext.Default.DeviceProvisioningProperties);
+ var linkedHubs = properties?.IotHubs?
+ .Select(hub => new LinkedIoTHubDescription(
+ hub.Name ?? string.Empty,
+ hub.Location ?? string.Empty,
+ hub.ApplyAllocationPolicy,
+ hub.AllocationWeight))
+ .ToList() ?? [];
+
+ return new DeviceProvisioningServiceDescription(
+ service.Id.ToString(),
+ service.Name,
+ service.Location.ToString(),
+ service.Id?.ResourceGroupName ?? string.Empty,
+ service.Id?.SubscriptionId ?? string.Empty,
+ service.Sku?.Name ?? string.Empty,
+ service.Sku?.Capacity ?? 0,
+ properties?.State ?? string.Empty,
+ properties?.ProvisioningState ?? string.Empty,
+ properties?.ServiceOperationsHostName ?? string.Empty,
+ properties?.DeviceProvisioningHostName ?? string.Empty,
+ properties?.IdScope ?? string.Empty,
+ properties?.AllocationPolicy ?? string.Empty,
+ properties?.PublicNetworkAccess ?? string.Empty,
+ properties?.EnableDataResidency,
+ linkedHubs);
+ }
+}
+
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Services/IDeviceProvisioningService.cs b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Services/IDeviceProvisioningService.cs
new file mode 100644
index 0000000000..c9048d4187
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/src/Services/IDeviceProvisioningService.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Azure.Mcp.Tools.DeviceProvisioning.Models;
+
+namespace Azure.Mcp.Tools.DeviceProvisioning.Services;
+
+public interface IDeviceProvisioningService
+{
+ Task GetService(
+ string serviceName,
+ string resourceGroup,
+ string subscription,
+ string? tenant = null,
+ CancellationToken cancellationToken = default);
+}
+
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/Azure.Mcp.Tools.DeviceProvisioning.Tests/Azure.Mcp.Tools.DeviceProvisioning.Tests.csproj b/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/Azure.Mcp.Tools.DeviceProvisioning.Tests/Azure.Mcp.Tools.DeviceProvisioning.Tests.csproj
new file mode 100644
index 0000000000..8da6b72ed3
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/Azure.Mcp.Tools.DeviceProvisioning.Tests/Azure.Mcp.Tools.DeviceProvisioning.Tests.csproj
@@ -0,0 +1,19 @@
+
+
+ true
+ Exe
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/Azure.Mcp.Tools.DeviceProvisioning.Tests/DeviceProvisioningCommandTests.cs b/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/Azure.Mcp.Tools.DeviceProvisioning.Tests/DeviceProvisioningCommandTests.cs
new file mode 100644
index 0000000000..674650a150
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/Azure.Mcp.Tools.DeviceProvisioning.Tests/DeviceProvisioningCommandTests.cs
@@ -0,0 +1,65 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json;
+using Microsoft.Mcp.Tests;
+using Microsoft.Mcp.Tests.Client;
+using Microsoft.Mcp.Tests.Client.Helpers;
+using Microsoft.Mcp.Tests.Generated.Models;
+using Xunit;
+
+namespace Azure.Mcp.Tools.DeviceProvisioning.Tests;
+
+public class DeviceProvisioningCommandTests(
+ ITestOutputHelper output,
+ TestProxyFixture fixture,
+ LiveServerFixture liveServerFixture)
+ : RecordedCommandTestsBase(output, fixture, liveServerFixture)
+{
+ // Resource-group names can include the recording user's alias.
+ public override List BodyKeySanitizers =>
+ [
+ ..base.BodyKeySanitizers,
+ new(new BodyKeySanitizerBody("$..resourcegroup"))
+ ];
+
+ public override List BodyRegexSanitizers =>
+ [
+ ..base.BodyRegexSanitizers,
+ new(new BodyRegexSanitizerBody
+ {
+ Regex = "resource[Gg]roups/(?[^/?\"]+)",
+ GroupForReplace = "resourceGroup",
+ Value = "Sanitized"
+ })
+ ];
+
+ [Fact]
+ public async Task ShouldGetDeviceProvisioningService()
+ {
+ var result = await CallToolAsync(
+ "deviceprovisioning_service_get",
+ new()
+ {
+ { "service", Settings.ResourceBaseName },
+ { "resource-group", Settings.ResourceGroupName },
+ { "subscription", Settings.SubscriptionId },
+ { "tenant", Settings.TenantId }
+ });
+
+ Assert.NotNull(result);
+
+ var payload = result.Value;
+ var provisioningService = payload.AssertProperty("deviceProvisioningService");
+ Assert.Equal(JsonValueKind.Object, provisioningService.ValueKind);
+ provisioningService.AssertProperty("name");
+ Assert.NotEmpty(
+ provisioningService.AssertProperty("idScope").GetString() ?? string.Empty);
+
+ var linkedHubs = provisioningService.AssertProperty("iotHubs");
+ Assert.Equal(JsonValueKind.Array, linkedHubs.ValueKind);
+
+ var areResultsTruncated = payload.AssertProperty("areResultsTruncated");
+ Assert.False(areResultsTruncated.GetBoolean());
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/Azure.Mcp.Tools.DeviceProvisioning.Tests/Models/DeviceProvisioningPropertiesTests.cs b/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/Azure.Mcp.Tools.DeviceProvisioning.Tests/Models/DeviceProvisioningPropertiesTests.cs
new file mode 100644
index 0000000000..61b1a2548f
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/Azure.Mcp.Tools.DeviceProvisioning.Tests/Models/DeviceProvisioningPropertiesTests.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json;
+using Azure.Mcp.Tools.DeviceProvisioning.Commands;
+using Xunit;
+
+namespace Azure.Mcp.Tools.DeviceProvisioning.Tests.Models;
+
+public class DeviceProvisioningPropertiesTests
+{
+ [Fact]
+ public void Deserialize_IgnoresSecretsAndPreservesSafeLinkedHubMetadata()
+ {
+ const string json = """
+ {
+ "state": "Active",
+ "authorizationPolicies": [
+ {
+ "keyName": "provisioningserviceowner",
+ "primaryKey": "secret-primary",
+ "secondaryKey": "secret-secondary"
+ }
+ ],
+ "iotHubs": [
+ {
+ "name": "hub1.azure-devices.net",
+ "location": "eastus",
+ "applyAllocationPolicy": true,
+ "allocationWeight": 1,
+ "connectionString": "HostName=hub1.azure-devices.net;SharedAccessKey=secret"
+ }
+ ]
+ }
+ """;
+
+ var properties = JsonSerializer.Deserialize(
+ json,
+ DeviceProvisioningJsonContext.Default.DeviceProvisioningProperties);
+
+ Assert.NotNull(properties);
+ Assert.Equal("Active", properties.State);
+ var linkedHub = Assert.Single(properties.IotHubs!);
+ Assert.Equal("hub1.azure-devices.net", linkedHub.Name);
+ Assert.Equal("eastus", linkedHub.Location);
+
+ var serialized = JsonSerializer.Serialize(
+ properties,
+ DeviceProvisioningJsonContext.Default.DeviceProvisioningProperties);
+ Assert.DoesNotContain("authorizationPolicies", serialized);
+ Assert.DoesNotContain("primaryKey", serialized);
+ Assert.DoesNotContain("connectionString", serialized);
+ Assert.DoesNotContain("secret", serialized);
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/Azure.Mcp.Tools.DeviceProvisioning.Tests/Service/DeviceProvisioningServiceGetCommandTests.cs b/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/Azure.Mcp.Tools.DeviceProvisioning.Tests/Service/DeviceProvisioningServiceGetCommandTests.cs
new file mode 100644
index 0000000000..f2a7dd76d9
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/Azure.Mcp.Tools.DeviceProvisioning.Tests/Service/DeviceProvisioningServiceGetCommandTests.cs
@@ -0,0 +1,200 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Net;
+using Azure.Mcp.Tests.Commands;
+using Azure.Mcp.Tools.DeviceProvisioning.Commands;
+using Azure.Mcp.Tools.DeviceProvisioning.Commands.Service;
+using Azure.Mcp.Tools.DeviceProvisioning.Models;
+using Azure.Mcp.Tools.DeviceProvisioning.Services;
+using NSubstitute;
+using NSubstitute.ExceptionExtensions;
+using Xunit;
+
+namespace Azure.Mcp.Tools.DeviceProvisioning.Tests.Service;
+
+public class DeviceProvisioningServiceGetCommandTests
+ : SubscriptionCommandUnitTestsBase
+{
+ [Fact]
+ public void Constructor_InitializesCommandCorrectly()
+ {
+ var command = Command.GetCommand();
+ Assert.Equal("get", command.Name);
+ Assert.NotNull(command.Description);
+ Assert.NotEmpty(command.Description);
+ }
+
+ [Theory]
+ [InlineData("--subscription sub123 --service dps1", false)]
+ [InlineData("--subscription sub123 --resource-group rg1 --service dps1", true)]
+ [InlineData("--subscription sub123", false)]
+ public async Task ExecuteAsync_ValidatesInputCorrectly(
+ string args,
+ bool shouldSucceed)
+ {
+ if (shouldSucceed)
+ {
+ Service.GetService(
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any())
+ .Returns(CreateDescription());
+ }
+
+ var response = await ExecuteCommandAsync(args);
+
+ Assert.Equal(
+ shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest,
+ response.Status);
+ if (!shouldSucceed)
+ {
+ Assert.Contains("required", response.Message.ToLowerInvariant());
+ }
+ }
+
+ [Theory]
+ [InlineData("ab")]
+ [InlineData("-dps-name")]
+ [InlineData("dps-name-")]
+ [InlineData("dps!name")]
+ [InlineData("dps' OR 1=1")]
+ public async Task ExecuteAsync_RejectsInvalidServiceName(string invalidName)
+ {
+ var response = await ExecuteCommandAsync(
+ "--subscription",
+ "sub123",
+ "--resource-group",
+ "rg1",
+ "--service",
+ invalidName);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.Status);
+ Assert.Contains(
+ "--service must be 3-64 characters long",
+ response.Message);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_RejectsOversizedServiceName()
+ {
+ var response = await ExecuteCommandAsync(
+ "--subscription",
+ "sub123",
+ "--resource-group",
+ "rg1",
+ "--service",
+ new string('a', 65));
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.Status);
+ Assert.Contains(
+ "--service must be 3-64 characters long",
+ response.Message);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_DeserializationValidation()
+ {
+ Service.GetService(
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any())
+ .Returns(CreateDescription());
+
+ var response = await ExecuteCommandAsync(
+ "--subscription",
+ "sub123",
+ "--resource-group",
+ "rg1",
+ "--service",
+ "dps1");
+
+ var result = ValidateAndDeserializeResponse(
+ response,
+ DeviceProvisioningJsonContext.Default.DeviceProvisioningServiceGetCommandResult);
+ Assert.Equal("dps1", result.DeviceProvisioningService.Name);
+ Assert.Single(result.DeviceProvisioningService.IotHubs);
+ Assert.False(result.AreResultsTruncated);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_HandlesServiceErrors()
+ {
+ Service.GetService(
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any())
+ .ThrowsAsync(new Exception("Test error"));
+
+ var response = await ExecuteCommandAsync(
+ "--subscription",
+ "sub123",
+ "--resource-group",
+ "rg1",
+ "--service",
+ "dps1");
+
+ Assert.Equal(HttpStatusCode.InternalServerError, response.Status);
+ Assert.Contains("Test error", response.Message);
+ Assert.Contains("troubleshooting", response.Message);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_HandlesNotFound()
+ {
+ Service.GetService(
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any())
+ .ThrowsAsync(new RequestFailedException(
+ (int)HttpStatusCode.NotFound,
+ "Resource not found"));
+
+ var response = await ExecuteCommandAsync(
+ "--subscription",
+ "sub123",
+ "--resource-group",
+ "rg1",
+ "--service",
+ "dps1");
+
+ Assert.Equal(HttpStatusCode.NotFound, response.Status);
+ Assert.Contains("Resource not found", response.Message);
+ }
+
+ private static DeviceProvisioningServiceDescription CreateDescription()
+ {
+ return new DeviceProvisioningServiceDescription(
+ Id: "/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.Devices/provisioningServices/dps1",
+ Name: "dps1",
+ Location: "eastus",
+ ResourceGroup: "rg1",
+ SubscriptionId: "sub123",
+ Sku: "S1",
+ Capacity: 1,
+ State: "Active",
+ ProvisioningState: "Succeeded",
+ ServiceOperationsHostName: "dps1.azure-devices-provisioning.net",
+ DeviceProvisioningHostName: "global.azure-devices-provisioning.net",
+ IdScope: "0ne00000001",
+ AllocationPolicy: "Hashed",
+ PublicNetworkAccess: "Enabled",
+ EnableDataResidency: false,
+ IotHubs:
+ [
+ new LinkedIoTHubDescription(
+ "hub1.azure-devices.net",
+ "eastus",
+ true,
+ 1)
+ ]);
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/Azure.Mcp.Tools.DeviceProvisioning.Tests/assets.json b/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/Azure.Mcp.Tools.DeviceProvisioning.Tests/assets.json
new file mode 100644
index 0000000000..eb7c83f909
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/Azure.Mcp.Tools.DeviceProvisioning.Tests/assets.json
@@ -0,0 +1,6 @@
+{
+ "AssetsRepo": "Azure/azure-sdk-assets",
+ "AssetsRepoPrefixPath": "",
+ "TagPrefix": "Azure.Mcp.Tools.DeviceProvisioning.Tests",
+ "Tag": "Azure.Mcp.Tools.DeviceProvisioning.Tests_c0c784a6b9"
+}
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/test-resources-post.ps1 b/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/test-resources-post.ps1
new file mode 100644
index 0000000000..858659fe63
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/test-resources-post.ps1
@@ -0,0 +1,17 @@
+param(
+ [string] $TenantId,
+ [string] $TestApplicationId,
+ [string] $ResourceGroupName,
+ [string] $BaseName,
+ [hashtable] $DeploymentOutputs,
+ [hashtable] $AdditionalParameters
+)
+
+$ErrorActionPreference = "Stop"
+
+. "$PSScriptRoot/../../../eng/common/scripts/common.ps1"
+. "$PSScriptRoot/../../../eng/scripts/helpers/TestResourcesHelpers.ps1"
+
+New-TestSettings @PSBoundParameters -OutputPath $PSScriptRoot
+
+Write-Host "Azure Device Provisioning Service test setup complete."
diff --git a/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/test-resources.bicep b/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/test-resources.bicep
new file mode 100644
index 0000000000..9fa27bb803
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DeviceProvisioning/tests/test-resources.bicep
@@ -0,0 +1,39 @@
+targetScope = 'resourceGroup'
+
+@minLength(3)
+@maxLength(64)
+@description('The base resource name.')
+param baseName string = resourceGroup().name
+
+@description('The client OID to grant access to test resources.')
+param testApplicationOid string = deployer().objectId
+
+@description('The location of the resource. By default, this is the same as the resource group.')
+param location string = resourceGroup().location
+
+resource provisioningService 'Microsoft.Devices/provisioningServices@2022-12-12' = {
+ name: baseName
+ location: location
+ sku: {
+ name: 'S1'
+ capacity: 1
+ }
+ properties: {}
+}
+
+resource readerRoleDefinition 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = {
+ scope: subscription()
+ name: 'acdd72a7-3385-48ef-bd42-f606fba81ae7'
+}
+
+resource readerRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(readerRoleDefinition.id, testApplicationOid, resourceGroup().id)
+ scope: resourceGroup()
+ properties: {
+ principalId: testApplicationOid
+ roleDefinitionId: readerRoleDefinition.id
+ }
+}
+
+output DEVICE_PROVISIONING_SERVICE_NAME string = provisioningService.name
+