diff --git a/AzureSignalR.sln b/AzureSignalR.sln index bc87fa030..21f7d7cfd 100644 --- a/AzureSignalR.sln +++ b/AzureSignalR.sln @@ -86,6 +86,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagementPublisher", "samp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatSample.RazorPages", "samples\ChatSample\ChatSample.RazorPages\ChatSample.RazorPages.csproj", "{D7A38BB7-6416-4E15-AD87-D525F203F549}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatMcp", "samples\ChatSample\ChatMcp\ChatMcp.csproj", "{DCF069DE-1C30-EE2B-D0ED-3355E998847B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -196,6 +198,10 @@ Global {D7A38BB7-6416-4E15-AD87-D525F203F549}.Debug|Any CPU.Build.0 = Debug|Any CPU {D7A38BB7-6416-4E15-AD87-D525F203F549}.Release|Any CPU.ActiveCfg = Release|Any CPU {D7A38BB7-6416-4E15-AD87-D525F203F549}.Release|Any CPU.Build.0 = Release|Any CPU + {DCF069DE-1C30-EE2B-D0ED-3355E998847B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DCF069DE-1C30-EE2B-D0ED-3355E998847B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DCF069DE-1C30-EE2B-D0ED-3355E998847B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DCF069DE-1C30-EE2B-D0ED-3355E998847B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -229,6 +235,7 @@ Global {82C1FF3D-EC6C-4B21-B6A4-E69E8D75D0D0} = {2429FBD8-1FCE-4C42-AA28-DF32F7249E77} {0F32E624-7AC8-4CA7-8ED9-E1A877442020} = {C965ED06-6A17-4329-B3C6-811830F4F4ED} {D7A38BB7-6416-4E15-AD87-D525F203F549} = {C965ED06-6A17-4329-B3C6-811830F4F4ED} + {DCF069DE-1C30-EE2B-D0ED-3355E998847B} = {C965ED06-6A17-4329-B3C6-811830F4F4ED} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7945A4E4-ACDB-4F6E-95CA-6AC6E7C2CD59} diff --git a/samples/ChatSample/ChatMcp/ChatMcp.csproj b/samples/ChatSample/ChatMcp/ChatMcp.csproj new file mode 100644 index 000000000..238cdc2a8 --- /dev/null +++ b/samples/ChatSample/ChatMcp/ChatMcp.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + enable + true + + + + + + + + + + + + + diff --git a/samples/ChatSample/ChatMcp/ChatProxy.cs b/samples/ChatSample/ChatMcp/ChatProxy.cs new file mode 100644 index 000000000..dd7689df6 --- /dev/null +++ b/samples/ChatSample/ChatMcp/ChatProxy.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Threading.Tasks; + +using ChatSample; +using Microsoft.AspNetCore.SignalR.Client; + +namespace ChatMcp; + +public sealed class ChatProxy(HubConnection connection) : IChatHub +{ + public async Task BroadcastMessage(string name, string message) + { + await connection.InvokeAsync(nameof(IChatHub.BroadcastMessage), name, message); + } + + Task IChatHub.Echo(string name, string message) + { + // we dont need this ability in the MCP tool. + throw new NotSupportedException(); + } +} diff --git a/samples/ChatSample/ChatMcp/ChatTool.cs b/samples/ChatSample/ChatMcp/ChatTool.cs new file mode 100644 index 000000000..4968f9d34 --- /dev/null +++ b/samples/ChatSample/ChatMcp/ChatTool.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.ComponentModel; +using System.Threading.Tasks; + +using ModelContextProtocol.Server; + +namespace ChatMcp; + +[McpServerToolType] +public class ChatTool(ChatProxy proxy) +{ + [McpServerTool] + [Description("Broadcast a message to all clients.")] + public async Task BroadcastMessage( + [Description("The text message.")] + string message) + { + await proxy.BroadcastMessage("MCP", message); + } +} diff --git a/samples/ChatSample/ChatMcp/Program.cs b/samples/ChatSample/ChatMcp/Program.cs new file mode 100644 index 000000000..96045c920 --- /dev/null +++ b/samples/ChatSample/ChatMcp/Program.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; + +using ChatMcp; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +var url = Environment.GetEnvironmentVariable("ServerUrl"); +if (string.IsNullOrEmpty(url)) +{ + url = "http://localhost:5050/chat"; +} +if (!Uri.TryCreate(url, UriKind.Absolute, out var serverUrl)) +{ + Console.WriteLine($"Invalid ServerUrl: {url}"); + return; +} + +await using var connection = new HubConnectionBuilder() + .WithUrl(url) + .WithAutomaticReconnect() + .Build(); +await connection.StartAsync(); + +var builder = Host.CreateApplicationBuilder(args); +builder.Logging.AddConsole(consoleLogOptions => +{ + // Configure all logs to go to stderr + consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace; +}); + +builder.Services + .AddSingleton(new ChatProxy(connection)) + .AddMcpServer() + .WithStdioServerTransport() + .WithToolsFromAssembly(); +await builder.Build().RunAsync(); diff --git a/samples/ChatSample/ChatSample/ChatHub.cs b/samples/ChatSample/ChatSample/ChatHub.cs index 042036873..d5143d165 100644 --- a/samples/ChatSample/ChatSample/ChatHub.cs +++ b/samples/ChatSample/ChatSample/ChatHub.cs @@ -10,17 +10,17 @@ namespace ChatSample; -public class ChatHub(IHubContext context) : Hub +public class ChatHub(IHubContext context) : Hub, IChatHub { - public void BroadcastMessage(string name, string message) + public async Task BroadcastMessage(string name, string message) { - Clients.All.SendAsync("broadcastMessage", name, message); + await Clients.All.SendAsync("broadcastMessage", name, message); Console.WriteLine("Broadcasting..."); } - public void Echo(string name, string message) + public async Task Echo(string name, string message) { - Clients.Caller.SendAsync("echo", name, + await Clients.Caller.SendAsync("echo", name, $"{message} (echo from server, Client IP: {Context.GetHttpContext().Connection.RemoteIpAddress}"); Console.WriteLine("Echo..."); } diff --git a/samples/ChatSample/ChatSample/IChatHub.cs b/samples/ChatSample/ChatSample/IChatHub.cs new file mode 100644 index 000000000..4c520d6d8 --- /dev/null +++ b/samples/ChatSample/ChatSample/IChatHub.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading.Tasks; + +namespace ChatSample; + +public interface IChatHub +{ + Task BroadcastMessage(string name, string message); + Task Echo(string name, string message); +} diff --git a/samples/ChatSample/ChatSample/Startup.cs b/samples/ChatSample/ChatSample/Startup.cs index 2050bc0ad..b4e3b70b2 100644 --- a/samples/ChatSample/ChatSample/Startup.cs +++ b/samples/ChatSample/ChatSample/Startup.cs @@ -45,6 +45,8 @@ private enum AuthTypes { VisualStudio = 0, + VisualStudioCode, + ApplicationWithCertificate, ApplicationWithClientSecret, @@ -66,7 +68,8 @@ public void ConfigureServices(IServiceCollection services) { TokenCredential credential = AuthType switch { - AuthTypes.VisualStudio => new VisualStudioCodeCredential(), + AuthTypes.VisualStudio => new VisualStudioCredential(), + AuthTypes.VisualStudioCode => new VisualStudioCodeCredential(), AuthTypes.ApplicationWithCertificate => new ClientCertificateCredential(TenantId, AppClientId, "path-to-cert-file"), AuthTypes.ApplicationWithClientSecret => new ClientSecretCredential(TenantId, AppClientId, "client-secret-value"), AuthTypes.ApplicationWithFederatedIdentity => GetClientAssertionCredential(TenantId, AppClientId, MsiClientId),