Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions servers/Azure.Mcp.Server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,7 @@ Example prompts that generate Azure CLI commands:

### 📣 Azure Event Grid

* "Create an Event Grid topic named 'my-topic' in resource group 'my-resourcegroup' in eastus"
* "List all Event Grid topics in subscription 'my-subscription'"
* "Show me the Event Grid topics in my subscription"
* "List all Event Grid topics in resource group 'my-resourcegroup' in my subscription"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changes:
- section: "Features Added"
description: "Added `eventgrid topic create` command to create Azure Event Grid topics in a specified resource group and location"
7 changes: 7 additions & 0 deletions servers/Azure.Mcp.Server/docs/azmcp-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -2300,6 +2300,13 @@ azmcp deviceregistry namespace list --subscription <subscription> \
### Azure Event Grid Operations

```bash
# Create an Azure Event Grid topic in a specified resource group and location
# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp eventgrid topic create --subscription <subscription> \
--resource-group <resource-group> \
--topic <topic> \
--location <location>

# List all Event Grid topics in a subscription or resource group
# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp eventgrid topic list --subscription <subscription> \
Expand Down
3 changes: 3 additions & 0 deletions servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,9 @@ This file contains prompts used for end-to-end testing to ensure each tool is in
| eventgrid_events_publish | Publish an event to Event Grid topic <topic_name> using <event_schema> with the following data <event_data> |
| eventgrid_events_publish | Publish event to my Event Grid topic <topic_name> with the following events <event_data> |
| eventgrid_events_publish | Send an event to Event Grid topic <topic_name> in resource group <resource_group_name> with <event_data> |
| eventgrid_topic_create | Create an Event Grid topic named <topic_name> in resource group <resource_group_name> in eastus |
| eventgrid_topic_create | Create a new Event Grid topic called <topic_name> in subscription <subscription> and resource group <resource_group_name> |
| eventgrid_topic_create | Set up an Event Grid topic <topic_name> in resource group <resource_group_name> in location westus2 |
| eventgrid_topic_list | List all Event Grid topics in my subscription |
| eventgrid_topic_list | Show me the Event Grid topics in my subscription |
| eventgrid_topic_list | List all Event Grid topics in subscription <subscription> |
Expand Down
33 changes: 33 additions & 0 deletions servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -2083,6 +2083,39 @@
"eventgrid_events_publish"
]
},
{
"name": "create_azure_eventgrid_topic",
"description": "Create Azure Event Grid topics in a specified resource group and location for event-driven messaging architectures.",
"toolMetadata": {
"destructive": {
"value": false,
"description": "This tool performs only additive updates without deleting or modifying existing resources."
},
Comment on lines +2090 to +2093
"idempotent": {
"value": true,
"description": "Running this operation multiple times with the same arguments produces the same result without additional effects."
},
Comment on lines +2094 to +2097
"openWorld": {
"value": false,
"description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities."
},
"readOnly": {
"value": false,
"description": "This tool may modify its environment by creating, updating, or deleting data."
},
"secret": {
"value": false,
"description": "This tool does not handle sensitive or secret information."
},
"localRequired": {
"value": false,
"description": "This tool is available in both local and remote server modes."
}
},
"mappedToolList": [
"eventgrid_topic_create"
]
},
{
"name": "get_azure_file_shares",
"description": "Get and list Azure File Shares. Retrieve details of specific file shares or list all file shares in a subscription or resource group.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ namespace Azure.Mcp.Tools.EventGrid.Commands;
[JsonSerializable(typeof(CloudEvent))] // For CloudEvent schema input deserialization
[JsonSerializable(typeof(EventGridEventInput))] // For EventGrid schema input deserialization
[JsonSerializable(typeof(CustomEvent))] // For custom event schema input deserialization
[JsonSerializable(typeof(TopicCreateCommand.TopicCreateCommandResult))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
internal sealed partial class EventGridJsonContext : JsonSerializerContext
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Azure.Mcp.Tools.EventGrid.Options;
using Azure.Mcp.Tools.EventGrid.Options.Topic;
using Azure.Mcp.Tools.EventGrid.Services;
using Microsoft.Mcp.Core.Commands;
using Microsoft.Mcp.Core.Extensions;
using Microsoft.Mcp.Core.Models.Command;
using Microsoft.Mcp.Core.Models.Option;

namespace Azure.Mcp.Tools.EventGrid.Commands.Topic;

[CommandMetadata(
Id = "f8a3b2c1-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
Name = "create",
Title = "Create Event Grid Topic",
Description = "Create an Azure Event Grid topic in a specified resource group and location. Returns the created topic's endpoint, provisioning state, and configuration details.",
Destructive = false,
Idempotent = true,
Comment on lines +19 to +20
OpenWorld = false,
ReadOnly = false,
Secret = false,
LocalRequired = false)]
public sealed class TopicCreateCommand(ILogger<TopicCreateCommand> logger, IEventGridService eventGridService) : BaseEventGridCommand<TopicCreateOptions>
{
private readonly ILogger<TopicCreateCommand> _logger = logger;
private readonly IEventGridService _eventGridService = eventGridService;

protected override void RegisterOptions(Command command)
{
base.RegisterOptions(command);
command.Options.Add(EventGridOptionDefinitions.TopicName.AsRequired());
command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired());
command.Options.Add(EventGridOptionDefinitions.Location.AsRequired());
}
Comment on lines +33 to +36

protected override TopicCreateOptions BindOptions(ParseResult parseResult)
{
var options = base.BindOptions(parseResult);
options.Topic = parseResult.GetValueOrDefault(EventGridOptionDefinitions.TopicName);
options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup);
options.Location = parseResult.GetValueOrDefault(EventGridOptionDefinitions.Location);
return options;
}

public override async Task<CommandResponse> ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken)
{
if (!Validate(parseResult.CommandResult, context.Response).IsValid)
{
return context.Response;
}

var options = BindOptions(parseResult);

try
{
var topic = await _eventGridService.CreateTopicAsync(
options.Topic!,
options.ResourceGroup!,
options.Location!,
options.Subscription!,
options.Tenant,
options.RetryPolicy,
cancellationToken);

context.Response.Results = ResponseResult.Create(new TopicCreateCommandResult(topic), EventGridJsonContext.Default.TopicCreateCommandResult);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error creating Event Grid topic. Topic: {Topic}, ResourceGroup: {ResourceGroup}, Subscription: {Subscription}.",
options.Topic, options.ResourceGroup, options.Subscription);
HandleException(context, ex);
}

return context.Response;
}

internal record TopicCreateCommandResult(EventGridTopicInfo? Topic);
}
2 changes: 2 additions & 0 deletions tools/Azure.Mcp.Tools.EventGrid/src/EventGridSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddSingleton<TopicListCommand>();
services.AddSingleton<SubscriptionListCommand>();
services.AddSingleton<EventGridPublishCommand>();
services.AddSingleton<TopicCreateCommand>();
}

public CommandGroup RegisterCommands(IServiceProvider serviceProvider)
Expand All @@ -47,6 +48,7 @@ public CommandGroup RegisterCommands(IServiceProvider serviceProvider)

// Register Topic commands
topics.AddCommand<TopicListCommand>(serviceProvider);
topics.AddCommand<TopicCreateCommand>(serviceProvider);

// Register Subscription commands
subscriptions.AddCommand<SubscriptionListCommand>(serviceProvider);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Azure.Mcp.Tools.EventGrid.Options.Topic;

public class TopicCreateOptions : BaseEventGridOptions
{
public string? Topic { get; set; }
public string? Location { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -533,4 +533,31 @@ private static async Task AddSubscriptionsFromSystemTopic(
}
}

public async Task<EventGridTopicInfo?> CreateTopicAsync(
string topic,
string resourceGroup,
string location,
string subscription,
string? tenant = null,
RetryPolicyOptions? retryPolicy = null,
CancellationToken cancellationToken = default)
Comment on lines +536 to +543
{
var subscriptionResource = await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy, cancellationToken);
var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken);

var topicData = new EventGridTopicData(new Azure.Core.AzureLocation(location))
{
InputSchema = EventGridInputSchema.EventGridSchema,
PublicNetworkAccess = EventGridPublicNetworkAccess.Enabled
};
Comment on lines +545 to +552

var operation = await resourceGroupResource.Value.GetEventGridTopics().CreateOrUpdateAsync(
Azure.WaitUntil.Completed,
topic,
topicData,
cancellationToken);

return CreateTopicInfo(operation.Value.Data);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,13 @@ Task<EventPublishResult> PublishEventAsync(
string? tenant = null,
RetryPolicyOptions? retryPolicy = null,
CancellationToken cancellationToken = default);

Task<EventGridTopicInfo?> CreateTopicAsync(
string topic,
string resourceGroup,
string location,
string subscription,
string? tenant = null,
RetryPolicyOptions? retryPolicy = null,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -441,4 +441,27 @@ public async Task Should_publish_with_default_schema_when_not_specified()
Assert.Equal("Success", status);
Assert.Equal(1, publishedEventCount);
}

[Fact]
public async Task Should_create_eventgrid_topic()
{
var topicName = RegisterOrRetrieveVariable("create_topic_name", $"topic-{Guid.NewGuid():N}"[..24]);

var result = await CallToolAsync(
"eventgrid_topic_create",
new()
{
{ "subscription", Settings.SubscriptionId },
{ "resource-group", Settings.ResourceGroupName },
{ "location", "eastus" },
{ "topic", topicName },
{ "tenant", Settings.TenantId }
});

var topic = result.AssertProperty("topic");
Assert.Equal(JsonValueKind.Object, topic.ValueKind);
topic.AssertProperty("name");
topic.AssertProperty("endpoint");
Assert.Equal("Succeeded", topic.GetProperty("provisioningState").GetString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Net;
using Azure.Mcp.Tools.EventGrid.Commands;
using Azure.Mcp.Tools.EventGrid.Commands.Topic;
using Azure.Mcp.Tools.EventGrid.Services;
using Microsoft.Mcp.Core.Options;
using Microsoft.Mcp.Tests.Client;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;

namespace Azure.Mcp.Tools.EventGrid.Tests.Topic;

public class TopicCreateCommandTests : CommandUnitTestsBase<TopicCreateCommand, IEventGridService>
{
[Fact]
public void Constructor_InitializesCommandCorrectly()
{
var command = Command.GetCommand();
Assert.Equal("create", command.Name);
Assert.NotNull(command.Description);
Assert.NotEmpty(command.Description);
}

[Fact]
public async Task ExecuteAsync_ValidParameters_ReturnsCreatedTopic()
{
var expectedTopic = new Models.EventGridTopicInfo(
"test-topic", "eastus",
"https://test-topic.eastus.eventgrid.azure.net/api/events",
"Succeeded", "Enabled", "EventGridSchema");

Service.CreateTopicAsync(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<RetryPolicyOptions>(),
Arg.Any<CancellationToken>())
.Returns(Task.FromResult<Models.EventGridTopicInfo?>(expectedTopic));

var response = await ExecuteCommandAsync(
"--topic", "test-topic",
"--resource-group", "test-rg",
"--location", "eastus",
"--subscription", "sub123");

var result = ValidateAndDeserializeResponse(response, EventGridJsonContext.Default.TopicCreateCommandResult);
Assert.NotNull(result.Topic);
Assert.Equal("test-topic", result.Topic.Name);
Assert.Equal("eastus", result.Topic.Location);
}

[Theory]
[InlineData("--topic test --resource-group rg --location eastus --subscription sub", true)]
[InlineData("--topic test --resource-group rg --subscription sub", false)] // missing location
[InlineData("--topic test --location eastus --subscription sub", false)] // missing resource-group
[InlineData("--resource-group rg --location eastus --subscription sub", false)] // missing topic
[InlineData("", false)] // missing args
public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed)
{
if (shouldSucceed)
{
Service.CreateTopicAsync(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<RetryPolicyOptions>(),
Arg.Any<CancellationToken>())
.Returns(Task.FromResult<Models.EventGridTopicInfo?>(
new Models.EventGridTopicInfo("test", "eastus", null, "Succeeded", "Enabled", "EventGridSchema")));
}

var response = await ExecuteCommandAsync(args);

Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status);
}

[Fact]
public async Task ExecuteAsync_HandlesServiceErrors()
{
Service.CreateTopicAsync(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<RetryPolicyOptions>(),
Arg.Any<CancellationToken>())
.ThrowsAsync(new Exception("Test error"));

var response = await ExecuteCommandAsync(
"--topic", "test-topic",
"--resource-group", "test-rg",
"--location", "eastus",
"--subscription", "sub123");

Assert.Equal(HttpStatusCode.InternalServerError, response.Status);
Assert.Contains("Test error", response.Message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "",
"TagPrefix": "Azure.Mcp.Tools.EventGrid.Tests",
"Tag": "Azure.Mcp.Tools.EventGrid.Tests_f3b2b0c29f"
"Tag": "Azure.Mcp.Tools.EventGrid.Tests_c4ec5088cd"
}
Loading