Skip to content

Commit aa7a279

Browse files
committed
adding communication sms send tool
1 parent 9e56469 commit aa7a279

19 files changed

+775
-0
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<ItemGroup>
4+
<PackageReference Include="Azure.Communication.Sms" />
5+
<PackageReference Include="Azure.Identity" />
6+
</ItemGroup>
7+
8+
<ItemGroup>
9+
<ProjectReference Include="..\..\..\..\core\src\AzureMcp.Core\AzureMcp.Core.csproj" />
10+
</ItemGroup>
11+
12+
</Project>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.CommandLine;
5+
using System.CommandLine.Parsing;
6+
using System.Diagnostics.CodeAnalysis;
7+
using AzureMcp.Communication.Options;
8+
using AzureMcp.Core.Commands;
9+
10+
namespace Azure.Mcp.Tools.Communication.Commands;
11+
12+
public abstract class BaseCommunicationCommand<
13+
[DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] TOptions>
14+
: GlobalCommand<TOptions>
15+
where TOptions : BaseCommunicationOptions, new()
16+
{
17+
protected readonly Option<string> _connectionStringOption = CommunicationOptionDefinitions.ConnectionString;
18+
19+
protected override void RegisterOptions(Command command)
20+
{
21+
base.RegisterOptions(command);
22+
command.AddOption(_connectionStringOption);
23+
}
24+
25+
protected override TOptions BindOptions(ParseResult parseResult)
26+
{
27+
var options = base.BindOptions(parseResult);
28+
options.ConnectionString = parseResult.GetValueForOption(_connectionStringOption);
29+
return options;
30+
}
31+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.CommandLine;
5+
using System.CommandLine.Parsing;
6+
using AzureMcp.Communication.Models;
7+
using AzureMcp.Communication.Options;
8+
using AzureMcp.Communication.Options.Sms;
9+
using AzureMcp.Communication.Services;
10+
using AzureMcp.Core.Commands;
11+
using AzureMcp.Core.Models.Command;
12+
using Microsoft.Extensions.Logging;
13+
14+
namespace Azure.Mcp.Tools.Communication.Commands.Sms;
15+
16+
public sealed class SmsSendCommand(ILogger<SmsSendCommand> logger) : BaseCommunicationCommand<SmsSendOptions>
17+
{
18+
private const string CommandTitle = "Send SMS Message";
19+
private readonly ILogger<SmsSendCommand> _logger = logger;
20+
21+
// Define options from OptionDefinitions
22+
private readonly Option<string> _fromOption = CommunicationOptionDefinitions.From;
23+
private readonly Option<string[]> _toOption = CommunicationOptionDefinitions.To;
24+
private readonly Option<string> _messageOption = CommunicationOptionDefinitions.Message;
25+
private readonly Option<bool> _enableDeliveryReportOption = CommunicationOptionDefinitions.EnableDeliveryReport;
26+
private readonly Option<string> _tagOption = CommunicationOptionDefinitions.Tag;
27+
28+
public override string Name => "send";
29+
30+
public override string Description =>
31+
"""
32+
Send an SMS message using Azure Communication Services.
33+
34+
Sends SMS messages to one or more recipients using your Communication Services resource.
35+
Supports delivery reporting and custom tags for message tracking.
36+
37+
Required options:
38+
- --connection-string: Azure Communication Services connection string
39+
- --from: SMS-enabled phone number (E.164 format, e.g., +14255550123)
40+
- --to: Recipient phone number(s) (E.164 format)
41+
- --message: SMS message content
42+
43+
Returns: Array of SMS send results with message IDs and delivery status.
44+
""";
45+
46+
public override string Title => CommandTitle;
47+
48+
public override ToolMetadata Metadata => new()
49+
{
50+
Destructive = false,
51+
ReadOnly = false
52+
};
53+
54+
protected override void RegisterOptions(Command command)
55+
{
56+
base.RegisterOptions(command);
57+
command.AddOption(_fromOption);
58+
command.AddOption(_toOption);
59+
command.AddOption(_messageOption);
60+
command.AddOption(_enableDeliveryReportOption);
61+
command.AddOption(_tagOption);
62+
}
63+
64+
protected override SmsSendOptions BindOptions(ParseResult parseResult)
65+
{
66+
var options = base.BindOptions(parseResult);
67+
options.From = parseResult.GetValueForOption(_fromOption);
68+
options.To = parseResult.GetValueForOption(_toOption);
69+
options.Message = parseResult.GetValueForOption(_messageOption);
70+
options.EnableDeliveryReport = parseResult.GetValueForOption(_enableDeliveryReportOption);
71+
options.Tag = parseResult.GetValueForOption(_tagOption);
72+
return options;
73+
}
74+
75+
public override async Task<CommandResponse> ExecuteAsync(CommandContext context, ParseResult parseResult)
76+
{
77+
var options = BindOptions(parseResult);
78+
79+
try
80+
{
81+
// Required validation step
82+
if (!Validate(parseResult.CommandResult, context.Response).IsValid)
83+
{
84+
return context.Response;
85+
}
86+
87+
// Get the Communication service from DI
88+
var communicationService = context.GetService<ICommunicationService>();
89+
90+
// Call service operation with required parameters
91+
var results = await communicationService.SendSmsAsync(
92+
options.ConnectionString!,
93+
options.From!,
94+
options.To!,
95+
options.Message!,
96+
options.EnableDeliveryReport,
97+
options.Tag,
98+
options.RetryPolicy);
99+
100+
// Set results
101+
context.Response.Results = results?.Count > 0 ?
102+
ResponseResult.Create(
103+
new SmsSendCommandResult(results),
104+
CommunicationJsonContext.Default.SmsSendCommandResult) :
105+
null;
106+
}
107+
catch (Exception ex)
108+
{
109+
// Log error with all relevant context
110+
_logger.LogError(ex,
111+
"Error sending SMS. From: {From}, To: {To}, Message Length: {MessageLength}, Options: {@Options}",
112+
options.From, options.To != null ? string.Join(",", options.To) : "null",
113+
options.Message?.Length ?? 0, options);
114+
HandleException(context, ex);
115+
}
116+
117+
return context.Response;
118+
}
119+
120+
// Implementation-specific error handling
121+
protected override string GetErrorMessage(Exception ex) => ex switch
122+
{
123+
ArgumentException argEx => $"Invalid parameter: {argEx.Message}",
124+
Azure.RequestFailedException reqEx when reqEx.Status == 401 =>
125+
"Authentication failed. Please verify your connection string is correct and has not expired.",
126+
Azure.RequestFailedException reqEx when reqEx.Status == 403 =>
127+
"Authorization failed. Ensure your Communication Services resource has SMS permissions and the phone number is provisioned.",
128+
Azure.RequestFailedException reqEx when reqEx.Status == 400 =>
129+
$"Bad request: {reqEx.Message}. Please verify phone numbers are in E.164 format and message content is valid.",
130+
Azure.RequestFailedException reqEx => reqEx.Message,
131+
_ => base.GetErrorMessage(ex)
132+
};
133+
134+
protected override int GetStatusCode(Exception ex) => ex switch
135+
{
136+
ArgumentException => 400,
137+
Azure.RequestFailedException reqEx => reqEx.Status,
138+
_ => base.GetStatusCode(ex)
139+
};
140+
141+
// Strongly-typed result record
142+
internal record SmsSendCommandResult(List<SmsResult> Results);
143+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
using AzureMcp.Communication.Commands.Sms;
6+
using AzureMcp.Communication.Models;
7+
8+
namespace AzureMcp.Communication;
9+
10+
[JsonSerializable(typeof(SmsResult))]
11+
[JsonSerializable(typeof(SmsSendCommand.SmsSendCommandResult))]
12+
[JsonSourceGenerationOptions(
13+
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
14+
GenerationMode = JsonSourceGenerationMode.Metadata)]
15+
internal partial class CommunicationJsonContext : JsonSerializerContext
16+
{
17+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using AzureMcp.Communication.Commands.Sms;
5+
using AzureMcp.Communication.Services;
6+
using AzureMcp.Core.Areas;
7+
using AzureMcp.Core.Commands;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.Extensions.Logging;
10+
11+
namespace Azure.Mcp.Tools.Communication;
12+
13+
public class CommunicationSetup : IAreaSetup
14+
{
15+
public void ConfigureServices(IServiceCollection services)
16+
{
17+
services.AddSingleton<ICommunicationService, CommunicationService>();
18+
}
19+
20+
public void RegisterCommands(CommandGroup rootGroup, ILoggerFactory loggerFactory)
21+
{
22+
// Create Communication command group
23+
var communication = new CommandGroup("communication",
24+
"Communication services operations - Commands for managing Azure Communication Services including SMS messaging, email, and voice calling capabilities.");
25+
rootGroup.AddSubGroup(communication);
26+
27+
// Create SMS subgroup
28+
var sms = new CommandGroup("sms", "SMS messaging operations - Commands for sending SMS messages using Azure Communication Services.");
29+
communication.AddSubGroup(sms);
30+
31+
// Register SMS commands
32+
sms.AddCommand("send", new SmsSendCommand(loggerFactory.CreateLogger<SmsSendCommand>()));
33+
}
34+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Azure.Mcp.Tools.Communication.Models;
7+
8+
public class SmsResult
9+
{
10+
[JsonPropertyName("messageId")]
11+
public string? MessageId { get; set; }
12+
13+
[JsonPropertyName("to")]
14+
public string? To { get; set; }
15+
16+
[JsonPropertyName("successful")]
17+
public bool Successful { get; set; }
18+
19+
[JsonPropertyName("httpStatusCode")]
20+
public int HttpStatusCode { get; set; }
21+
22+
[JsonPropertyName("errorMessage")]
23+
public string? ErrorMessage { get; set; }
24+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
using AzureMcp.Core.Options;
6+
7+
namespace AzureMcp.Communication.Options;
8+
9+
public class BaseCommunicationOptions : GlobalOptions
10+
{
11+
[JsonPropertyName(CommunicationOptionDefinitions.ConnectionStringName)]
12+
public string? ConnectionString { get; set; }
13+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.CommandLine;
5+
6+
namespace Azure.Mcp.Tools.Communication.Options;
7+
8+
public static class CommunicationOptionDefinitions
9+
{
10+
public const string ConnectionStringName = "connection-string";
11+
public const string FromName = "from";
12+
public const string ToName = "to";
13+
public const string MessageName = "message";
14+
public const string EnableDeliveryReportName = "enable-delivery-report";
15+
public const string TagName = "tag";
16+
17+
public static readonly Option<string> ConnectionString = new(
18+
$"--{ConnectionStringName}",
19+
"The connection string for the Azure Communication Services resource. You can find this in the Azure portal under your Communication Services resource."
20+
)
21+
{
22+
IsRequired = true
23+
};
24+
25+
public static readonly Option<string> From = new(
26+
$"--{FromName}",
27+
"The SMS-enabled phone number associated with your Communication Services resource (in E.164 format, e.g., +14255550123). Can also be a short code or alphanumeric sender ID."
28+
)
29+
{
30+
IsRequired = true
31+
};
32+
33+
public static readonly Option<string[]> To = new(
34+
$"--{ToName}",
35+
"The recipient phone number(s) in E.164 international standard format (e.g., +14255550123). Multiple numbers can be provided."
36+
)
37+
{
38+
IsRequired = true,
39+
AllowMultipleArgumentsPerToken = true
40+
};
41+
42+
public static readonly Option<string> Message = new(
43+
$"--{MessageName}",
44+
"The SMS message content to send to the recipient(s)."
45+
)
46+
{
47+
IsRequired = true
48+
};
49+
50+
public static readonly Option<bool> EnableDeliveryReport = new(
51+
$"--{EnableDeliveryReportName}",
52+
() => false,
53+
"Whether to enable delivery reporting for the SMS message. When enabled, events are emitted when delivery is successful."
54+
);
55+
56+
public static readonly Option<string> Tag = new(
57+
$"--{TagName}",
58+
"Optional custom tag to apply to the SMS message for tracking purposes."
59+
);
60+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Azure.Mcp.Tools.Communication.Options.Sms;
7+
8+
public class SmsSendOptions : BaseCommunicationOptions
9+
{
10+
[JsonPropertyName(CommunicationOptionDefinitions.FromName)]
11+
public string? From { get; set; }
12+
13+
[JsonPropertyName(CommunicationOptionDefinitions.ToName)]
14+
public string[]? To { get; set; }
15+
16+
[JsonPropertyName(CommunicationOptionDefinitions.MessageName)]
17+
public string? Message { get; set; }
18+
19+
[JsonPropertyName(CommunicationOptionDefinitions.EnableDeliveryReportName)]
20+
public bool EnableDeliveryReport { get; set; }
21+
22+
[JsonPropertyName(CommunicationOptionDefinitions.TagName)]
23+
public string? Tag { get; set; }
24+
}

0 commit comments

Comments
 (0)