Skip to content
Merged
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
24 changes: 15 additions & 9 deletions Microsoft.Azure.Functions.Extensions.Mcp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ VisualStudioVersion = 17.12.35728.132
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Extensions.Mcp", "src\Microsoft.Azure.Functions.Extensions.Mcp\Extensions.Mcp.csproj", "{1FEB9DC5-EB89-9E3E-296F-2B699FF6978B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Worker.Extensions.Mcp", "src\Microsoft.Azure.Functions.Worker.Extensions.Mcp\Worker.Extensions.Mcp.csproj", "{BE61B45A-FB64-14F4-0693-B1CC7B69BB08}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "tests\TestApp\TestApp.csproj", "{92A2B0B4-9582-CDF6-C08D-9593033805AC}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3CC0C099-5116-4299-A0F5-A5ACC469A06C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A86BD8D1-5BC0-4E98-BC5B-2FCE551CEE8F}"
ProjectSection(SolutionItems) = preProject
test\TestApp\TestApp.csproj = test\TestApp\TestApp.csproj
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Extensions.Mcp.Tests", "test\Extensions.Mcp.Tests\Extensions.Mcp.Tests.csproj", "{0B903129-99CA-BA6B-C53A-D70938315220}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -27,17 +32,18 @@ Global
{BE61B45A-FB64-14F4-0693-B1CC7B69BB08}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BE61B45A-FB64-14F4-0693-B1CC7B69BB08}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE61B45A-FB64-14F4-0693-B1CC7B69BB08}.Release|Any CPU.Build.0 = Release|Any CPU
{92A2B0B4-9582-CDF6-C08D-9593033805AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{92A2B0B4-9582-CDF6-C08D-9593033805AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{92A2B0B4-9582-CDF6-C08D-9593033805AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{92A2B0B4-9582-CDF6-C08D-9593033805AC}.Release|Any CPU.Build.0 = Release|Any CPU
{0B903129-99CA-BA6B-C53A-D70938315220}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0B903129-99CA-BA6B-C53A-D70938315220}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0B903129-99CA-BA6B-C53A-D70938315220}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0B903129-99CA-BA6B-C53A-D70938315220}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}
{92A2B0B4-9582-CDF6-C08D-9593033805AC} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{1FEB9DC5-EB89-9E3E-296F-2B699FF6978B} = {3CC0C099-5116-4299-A0F5-A5ACC469A06C}
{BE61B45A-FB64-14F4-0693-B1CC7B69BB08} = {3CC0C099-5116-4299-A0F5-A5ACC469A06C}
{0B903129-99CA-BA6B-C53A-D70938315220} = {A86BD8D1-5BC0-4E98-BC5B-2FCE551CEE8F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6FC8154B-B3AB-4044-A301-8587814DF78A}
Expand Down
3 changes: 3 additions & 0 deletions src/Microsoft.Azure.Functions.Extensions.Mcp/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly:InternalsVisibleTo("Extensions.Mcp.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001005148be37ac1d9f58bd40a2e472c9d380d635b6048278f7d47480b08c928858f0f7fe17a6e4ce98da0e7a7f0b8c308aecd9e9b02d7e9680a5b5b75ac7773cec096fbbc64aebd429e77cb5f89a569a79b28e9c76426783f624b6b70327eb37341eb498a2c3918af97c4860db6cdca4732787150841e395a29cfacb959c1fd971c1")]
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ internal class ClientStateManager
{
private readonly static bool _isCoreToolsEnvironment = Environment.GetEnvironmentVariable("FUNCTIONS_CORETOOLS_ENVIRONMENT") is not null;

public static bool TryParseUriState(string clientState, [NotNullWhen(true)] out string? clientId, [NotNullWhen(true)] out string? instanceId)
public static bool TryParseUriState(string clientState, [NotNullWhen(true)] out string? clientId, [NotNullWhen(true)] out string? instanceId, bool isEncrypted = true)
{
// When running locally, we use a plain client state format
if (!_isCoreToolsEnvironment)
// If not encrypted, or running locally, we use a plain client state format
if (isEncrypted && !_isCoreToolsEnvironment)
{
clientState = TokenUtility.ReadUriState(clientState);
}

return TryParsePlainClientState(clientState, out clientId, out instanceId);
}

public static string FormatUriState(string clientId, string instanceId)
public static string FormatUriState(string clientId, string instanceId, bool isEncrypted = true)
{
var uriState = $"{clientId}|{instanceId}";

// When running locally, we use a plain client state format
if (_isCoreToolsEnvironment)
// If not encrypted, or running locally, we use a plain client state format
if (!isEncrypted || _isCoreToolsEnvironment)
{
return uriState;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Microsoft.Azure.Functions.Extensions.Mcp;

namespace Microsoft.Extensions.Hosting;
namespace Microsoft.Azure.Functions.Extensions.Mcp;

internal class DefaultMcpInstanceIdProvider : IMcpInstanceIdProvider
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.Storage.Queues" Version="12.22.0" />
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.41" />
Expand Down
33 changes: 33 additions & 0 deletions test/Extensions.Mcp.Tests/ClientStateManagerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Xunit;

namespace Microsoft.Azure.Functions.Extensions.Mcp.Tests;

public class ClientStateManagerTests
{
[Fact]
public void FormatUriState_And_TryParseUriState_RoundTrip_Success()
{
var clientId = "client";
var instanceId = "instance";
var state = ClientStateManager.FormatUriState(clientId, instanceId, isEncrypted: false);

Assert.True(ClientStateManager.TryParseUriState(state, out var parsedClientId, out var parsedInstanceId, isEncrypted: false));
Assert.Equal(clientId, parsedClientId);
Assert.Equal(instanceId, parsedInstanceId);
}

[Fact]
public void TryParseUriState_InvalidFormat_ReturnsFalse()
{
Assert.False(ClientStateManager.TryParseUriState("", out _, out _, isEncrypted: false));
Assert.False(ClientStateManager.TryParseUriState("onlyonepart", out _, out _, isEncrypted: false));
Assert.False(ClientStateManager.TryParseUriState("a|b|c", out _, out _, isEncrypted: false));
Assert.False(ClientStateManager.TryParseUriState("valid|format|extra", out _, out _, isEncrypted: false));

Assert.True(ClientStateManager.TryParseUriState("client|instance", out var parsedClientId, out var parsedInstanceId, isEncrypted: false));
Assert.Equal("client", parsedClientId);
Assert.Equal("instance", parsedInstanceId);

Assert.False(ClientStateManager.TryParseUriState("invalidformat", out _, out _, isEncrypted: false));
}
}
25 changes: 25 additions & 0 deletions test/Extensions.Mcp.Tests/DefaultMcpInstanceIdProviderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Xunit;

namespace Microsoft.Azure.Functions.Extensions.Mcp.Tests;

public class DefaultMcpInstanceIdProviderTests
{
[Fact]
public void InstanceId_IsNotNullOrEmpty()
{
var provider = new DefaultMcpInstanceIdProvider();

var instanceId = provider.InstanceId;

Assert.False(string.IsNullOrEmpty(instanceId));
}

[Fact]
public void InstanceId_IsUnique()
{
var provider1 = new DefaultMcpInstanceIdProvider();
var provider2 = new DefaultMcpInstanceIdProvider();

Assert.NotEqual(provider1.InstanceId, provider2.InstanceId);
}
}
28 changes: 28 additions & 0 deletions test/Extensions.Mcp.Tests/Extensions.Mcp.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<Import Project="..\..\build\Common.props" />

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Azure.Functions.Extensions.Mcp\Extensions.Mcp.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
23 changes: 23 additions & 0 deletions test/Extensions.Mcp.Tests/McpBackplaneMessageTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Microsoft.Azure.Functions.Extensions.Mcp.Backplane;
using ModelContextProtocol.Protocol.Messages;
using Xunit;

namespace Microsoft.Azure.Functions.Extensions.Mcp.Tests;

public class McpBackplaneMessageTests
{
[Fact]
public void Properties_CanBeSetAndRead()
{
var dummy = new TestJsonRpcMessage();
var msg = new McpBackplaneMessage
{
ClientId = "client",
Message = dummy
};
Assert.Equal("client", msg.ClientId);
Assert.Same(dummy, msg.Message);
}

private class TestJsonRpcMessage : JsonRpcMessage { }
}
17 changes: 17 additions & 0 deletions test/Extensions.Mcp.Tests/McpOptionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.Azure.Functions.Extensions.Mcp.Configuration;
using Xunit;

namespace Microsoft.Azure.Functions.Extensions.Mcp.Tests;

public class McpOptionsTests
{
[Fact]
public void DefaultValues_AreSetCorrectly()
{
var options = new McpOptions();

Assert.Equal("Azure Functions MCP server", options.ServerName);
Assert.Equal("1.0.0", options.ServerVersion);
Assert.Null(options.Instructions);
}
}
16 changes: 16 additions & 0 deletions test/Extensions.Mcp.Tests/McpToolPropertyAttributeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Xunit;

namespace Microsoft.Azure.Functions.Extensions.Mcp.Tests;

public class McpToolPropertyAttributeTests
{
[Fact]
public void Constructor_SetsPropertiesCorrectly()
{
var attribute = new McpToolPropertyAttribute("name", "string", "description");

Assert.Equal("name", attribute.PropertyName);
Assert.Equal("string", attribute.PropertyType);
Assert.Equal("description", attribute.Description);
}
}
16 changes: 16 additions & 0 deletions test/Extensions.Mcp.Tests/McpToolTriggerAttributeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Xunit;

namespace Microsoft.Azure.Functions.Extensions.Mcp.Tests;

public class McpToolTriggerAttributeTests
{
[Fact]
public void Constructor_SetsPropertiesCorrectly()
{
var attribute = new McpToolTriggerAttribute("TestTool", "TestDescription");

Assert.Equal("TestTool", attribute.ToolName);
Assert.Equal("TestDescription", attribute.Description);
Assert.Null(attribute.ToolProperties);
}
}
20 changes: 20 additions & 0 deletions test/Extensions.Mcp.Tests/TokenUtilityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Security.Cryptography;
using Xunit;

namespace Microsoft.Azure.Functions.Extensions.Mcp.Tests;

public class TokenUtilityTests
{
[Fact]
public void ProtectAndReadUriState_RoundTrip_Success()
{
var state = "test-state";
var key = new byte[32];
RandomNumberGenerator.Fill(key);

var token = TokenUtility.ProtectUriState(state, key);
var result = TokenUtility.ReadUriState(token, key);

Assert.Equal(state, result);
}
}
33 changes: 33 additions & 0 deletions test/Extensions.Mcp.Tests/TokenUtilityTests_Extra.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Security.Cryptography;
using Xunit;

namespace Microsoft.Azure.Functions.Extensions.Mcp.Tests;

public class TokenUtilityTests_Extra
{
[Fact]
public void ToKeyBytes_HexAndBase64_Success()
{
var hex = new string('a', 64);
var base64 = Convert.ToBase64String(new byte[32]);
var hexBytes = TokenUtility.ToKeyBytes(hex);
var base64Bytes = TokenUtility.ToKeyBytes(base64);
Assert.Equal(32, hexBytes.Length);
Assert.Equal(32, base64Bytes.Length);
}

[Fact]
public void TryGetEncryptionKey_ReturnsFalse_WhenNotSet()
{
var original = Environment.GetEnvironmentVariable("WEBSITE_AUTH_ENCRYPTION_KEY");
Environment.SetEnvironmentVariable("WEBSITE_AUTH_ENCRYPTION_KEY", null);
try
{
Assert.False(TokenUtility.TryGetEncryptionKey(out _));
}
finally
{
Environment.SetEnvironmentVariable("WEBSITE_AUTH_ENCRYPTION_KEY", original);
}
}
}
26 changes: 26 additions & 0 deletions test/Extensions.Mcp.Tests/UtilityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Xunit;

namespace Microsoft.Azure.Functions.Extensions.Mcp.Tests;

public class UtilityTests
{
[Fact]
public void CreateId_GeneratesUniqueId()
{
var id1 = Utility.CreateId();
var id2 = Utility.CreateId();

Assert.False(string.IsNullOrEmpty(id1));
Assert.False(string.IsNullOrEmpty(id2));
Assert.NotEqual(id1, id2);
}

[Fact]
public void CreateId_UsesOnlyValidCharacters()
{
const string validChars = "abcdefghijklmnopqrstuvwxyz0123456789";
var id = Utility.CreateId();

Assert.All(id, c => Assert.Contains(c, validChars));
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.